@blocklet/payment-react
Version:
Reusable react components for payment kit v2
737 lines (736 loc) • 24.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
module.exports = SuccessView;
var _jsxRuntime = require("react/jsx-runtime");
var _react = require("react");
var _material = require("@mui/material");
var _Check = _interopRequireDefault(require("@mui/icons-material/Check"));
var _CheckCircle = _interopRequireDefault(require("@mui/icons-material/CheckCircle"));
var _VerifiedUser = _interopRequireDefault(require("@mui/icons-material/VerifiedUser"));
var _ErrorOutline = _interopRequireDefault(require("@mui/icons-material/ErrorOutline"));
var _OpenInNew = _interopRequireDefault(require("@mui/icons-material/OpenInNew"));
var _ArrowBack = _interopRequireDefault(require("@mui/icons-material/ArrowBack"));
var _ufo = require("ufo");
var _context = require("@arcblock/ux/lib/Locale/context");
var _paymentReactHeadless = require("@blocklet/payment-react-headless");
var _util = require("../../libs/util");
var _format = require("../utils/format");
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const scaleIn = (0, _material.keyframes)`
from { transform: scale(0); opacity: 0; }
60% { transform: scale(1.15); }
to { transform: scale(1); opacity: 1; }
`;
const fadeUp = (0, _material.keyframes)`
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
`;
function useConfetti(containerRef, enabled) {
const firedRef = (0, _react.useRef)(false);
(0, _react.useEffect)(() => {
if (!enabled || firedRef.current || !containerRef.current) return void 0;
firedRef.current = true;
const container = containerRef.current;
const canvas = document.createElement("canvas");
canvas.style.cssText = "position:absolute;inset:0;pointer-events:none;z-index:10;";
canvas.width = container.offsetWidth;
canvas.height = container.offsetHeight;
container.appendChild(canvas);
const ctx = canvas.getContext("2d");
if (!ctx) return void 0;
const colors = ["#3b82f6", "#60a5fa", "#34d399", "#fbbf24", "#f472b6", "#a78bfa", "#f97316"];
const pieces = [];
for (let i = 0; i < 80; i++) {
pieces.push({
x: canvas.width / 2 + (Math.random() - 0.5) * 60,
y: canvas.height * 0.35,
vx: (Math.random() - 0.5) * 12,
vy: -Math.random() * 14 - 4,
w: Math.random() * 8 + 4,
h: Math.random() * 6 + 2,
color: colors[Math.floor(Math.random() * colors.length)],
rot: Math.random() * Math.PI * 2,
rv: (Math.random() - 0.5) * 0.3,
opacity: 1
});
}
let frame;
const gravity = 0.25;
const friction = 0.99;
const animate = () => {
ctx.clearRect(0, 0, canvas.width, canvas.height);
let alive = false;
for (const p of pieces) {
p.vy += gravity;
p.vx *= friction;
p.x += p.vx;
p.y += p.vy;
p.rot += p.rv;
if (p.y > canvas.height * 0.6) {
p.opacity -= 0.02;
}
if (p.opacity > 0) {
alive = true;
ctx.save();
ctx.globalAlpha = p.opacity;
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
ctx.fillStyle = p.color;
ctx.fillRect(-p.w / 2, -p.h / 2, p.w, p.h);
ctx.restore();
}
}
if (alive) {
frame = requestAnimationFrame(animate);
} else {
canvas.remove();
}
};
const timer = setTimeout(() => {
frame = requestAnimationFrame(animate);
}, 400);
return () => {
clearTimeout(timer);
cancelAnimationFrame(frame);
canvas.remove();
};
}, [enabled, containerRef]);
}
function getCustomMessage(session) {
return session?.payment_link?.after_completion?.hosted_confirmation?.custom_message;
}
function getPayee(session) {
const items = session?.line_items || [];
for (const item of items) {
if (item?.price?.product?.statement_descriptor) {
return item.price.product.statement_descriptor;
}
}
return session?.app_name || session?.payment_link?.app_name || "";
}
function getVendorLabel(vendor, isFailed, t) {
const name = vendor.name || vendor.title;
const isCompleted = vendor.status === "delivered";
if (vendor.vendorType === "didnames") {
if (isFailed) return t("payment.checkout.vendor.didnames.failed", {
name
});
if (isCompleted) return t("payment.checkout.vendor.didnames.completed", {
name
});
return t("payment.checkout.vendor.didnames.processing", {
name
});
}
if (isFailed) return t("payment.checkout.vendor.launcher.failed", {
name
});
if (isCompleted) return t("payment.checkout.vendor.launcher.completed", {
name
});
return t("payment.checkout.vendor.launcher.processing", {
name
});
}
function HeroSuccessIcon() {
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
sx: {
position: "relative",
mb: 1,
animation: `${scaleIn} 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) both`
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
sx: {
position: "absolute",
inset: -24,
background: theme => theme.palette.mode === "dark" ? "radial-gradient(circle, rgba(59,130,246,0.12) 0%, transparent 70%)" : "radial-gradient(circle, rgba(59,130,246,0.08) 0%, transparent 70%)",
borderRadius: "50%",
pointerEvents: "none"
}
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
sx: {
position: "relative",
width: {
xs: 100,
md: 120
},
height: {
xs: 100,
md: 120
},
borderRadius: {
xs: "28px",
md: "32px"
},
background: theme => theme.palette.mode === "dark" ? "linear-gradient(135deg, rgba(59,130,246,0.15) 0%, rgba(255,255,255,0.04) 100%)" : "linear-gradient(135deg, #eff6ff 0%, #ffffff 100%)",
border: "1px solid",
borderColor: theme => theme.palette.mode === "dark" ? "rgba(59,130,246,0.2)" : "rgba(59,130,246,0.12)",
boxShadow: theme => theme.palette.mode === "dark" ? "0 10px 30px -5px rgba(0,0,0,0.3)" : "0 10px 30px -5px rgba(59,130,246,0.1)",
display: "flex",
alignItems: "center",
justifyContent: "center"
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
sx: {
position: "absolute",
inset: 0,
borderRadius: "inherit",
background: theme => theme.palette.mode === "dark" ? "linear-gradient(to top-right, rgba(59,130,246,0.1), transparent)" : "linear-gradient(to top-right, rgba(59,130,246,0.06), transparent)",
pointerEvents: "none"
}
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_VerifiedUser.default, {
sx: {
fontSize: {
xs: 52,
md: 64
},
color: "primary.main",
filter: theme => theme.palette.mode === "dark" ? "drop-shadow(0 0 12px rgba(59,130,246,0.4))" : "drop-shadow(0 0 12px rgba(59,130,246,0.2))"
}
})]
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
sx: {
position: "absolute",
bottom: {
xs: -6,
md: -8
},
right: {
xs: -6,
md: -8
},
width: {
xs: 32,
md: 38
},
height: {
xs: 32,
md: 38
},
borderRadius: {
xs: "12px",
md: "14px"
},
bgcolor: "background.paper",
boxShadow: "0 4px 12px rgba(0,0,0,0.1)",
border: "1px solid",
borderColor: theme => theme.palette.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(59,130,246,0.08)",
display: "flex",
alignItems: "center",
justifyContent: "center"
},
children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_CheckCircle.default, {
sx: {
fontSize: {
xs: 20,
md: 24
},
color: "success.main"
}
})
})]
});
}
function PaymentReceipt({
session,
t
}) {
const {
currencies
} = (0, _paymentReactHeadless.usePaymentMethodContext)();
const amountTotal = session?.amount_total;
const currencyId = session?.currency_id;
const subData = session?.subscription_data;
if (Number(subData?.trial_period_days || 0) > 0) return null;
if (!amountTotal || amountTotal === "0") return null;
const currency = currencies.find(c => c.id === currencyId) || null;
if (!currency) return null;
const formatted = (0, _format.formatTokenAmount)(amountTotal, currency);
if (!formatted || formatted === "0") return null;
const amountStr = `${formatted} ${currency.symbol || ""}`.trim();
const fullText = t("payment.checkout.completed.summary.paid", {
amount: amountStr
});
const idx = fullText.indexOf(amountStr);
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
sx: {
fontSize: {
xs: 15,
md: 16
},
color: "text.secondary",
fontWeight: 500,
lineHeight: 1.6,
textAlign: "center",
maxWidth: 440,
animation: `${fadeUp} 0.5s ease 0.3s both`
},
children: idx >= 0 ? /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
children: [fullText.slice(0, idx), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
component: "span",
sx: {
fontWeight: 700,
color: "text.primary"
},
children: amountStr
}), fullText.slice(idx + amountStr.length)]
}) : fullText
});
}
function VendorProgressItemV2({
vendor,
t
}) {
const [displayProgress, setDisplayProgress] = (0, _react.useState)(0);
const animationRef = (0, _react.useRef)();
const startAnimation = (0, _react.useCallback)(() => {
const realProgress = vendor.progress || 0;
let startTime;
let startProgress;
const animate = currentTime => {
if (!startTime) {
startTime = currentTime;
startProgress = displayProgress;
}
const elapsed = currentTime - startTime;
let newProgress;
if (realProgress === 100) {
newProgress = 100;
} else if (realProgress === 0) {
newProgress = Math.min(startProgress + elapsed / 1e3, 99);
} else if (realProgress > startProgress) {
const progress = Math.min(elapsed / 1e3, 1);
newProgress = startProgress + (realProgress - startProgress) * progress;
} else {
newProgress = Math.min(startProgress + elapsed / 1e3, 99);
}
newProgress = Math.round(newProgress);
setDisplayProgress(pre => Math.min(pre > newProgress ? pre : newProgress, 100));
if (realProgress === 100) return;
if (newProgress < 99 && realProgress < 100) {
animationRef.current = requestAnimationFrame(animate);
}
};
if (animationRef.current) cancelAnimationFrame(animationRef.current);
animationRef.current = requestAnimationFrame(animate);
}, [vendor.progress, displayProgress]);
(0, _react.useEffect)(() => {
startAnimation();
return () => {
if (animationRef.current) cancelAnimationFrame(animationRef.current);
};
}, [startAnimation]);
const isCompleted = displayProgress >= 100;
const isFailed = vendor.status === "failed";
const nameText = getVendorLabel(vendor, isFailed, t);
if (!vendor.name && !vendor.title) {
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
sx: {
mb: 1.5
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
direction: "row",
justifyContent: "space-between",
alignItems: "center",
sx: {
mb: 0.75
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Skeleton, {
variant: "rounded",
height: 14,
width: 150
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Skeleton, {
variant: "rounded",
height: 14,
width: 50
})]
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Skeleton, {
variant: "rounded",
height: 6,
width: "100%"
})]
});
}
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
sx: {
mb: 1.5
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
direction: "row",
justifyContent: "space-between",
alignItems: "center",
sx: {
mb: 0.75
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, {
sx: {
fontSize: 13,
fontWeight: 600,
color: isFailed ? "error.main" : "text.secondary",
display: "flex",
alignItems: "center"
},
children: [nameText, isCompleted && !isFailed && /* @__PURE__ */(0, _jsxRuntime.jsx)(_Check.default, {
sx: {
color: "success.main",
ml: 0.5,
fontSize: 16
}
})]
}), !isCompleted && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
sx: {
fontSize: 12,
fontWeight: 600,
color: isFailed ? "error.main" : "text.secondary"
},
children: t("payment.checkout.vendor.progress", {
progress: isFailed ? 0 : displayProgress
})
})]
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.LinearProgress, {
variant: "determinate",
value: isFailed ? 100 : displayProgress || 0,
sx: {
height: 6,
borderRadius: 3,
bgcolor: theme => theme.palette.mode === "dark" ? "rgba(255,255,255,0.08)" : "grey.200",
"& .MuiLinearProgress-bar": {
borderRadius: 3,
// eslint-disable-next-line no-nested-ternary
bgcolor: isFailed ? "error.main" : isCompleted ? "success.main" : "primary.main",
transition: "background-color 0.3s linear"
}
}
})]
});
}
function VendorProgressPanel({
vendorStatus,
pageInfo = void 0,
locale,
t
}) {
return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
sx: {
width: "100%",
maxWidth: 420,
p: 2.5,
borderRadius: 3,
bgcolor: theme => theme.palette.mode === "dark" ? "rgba(255,255,255,0.04)" : "grey.50",
border: "1px solid",
borderColor: "divider",
animation: `${fadeUp} 0.5s ease 0.3s both`
},
children: [(vendorStatus.vendors || []).map((vendor, idx) => /* @__PURE__ */(0, _jsxRuntime.jsx)(VendorProgressItemV2, {
vendor,
t
}, vendor.title || `vendor-${idx}`)), vendorStatus.hasFailed && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
sx: {
fontSize: 13,
fontWeight: 600,
color: "warning.main",
mt: 1
},
children: t("payment.checkout.vendor.failedMsg")
}), vendorStatus.isAllCompleted && pageInfo?.success_message?.[locale] && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
sx: {
fontSize: 14,
fontWeight: 600,
color: "text.primary",
mt: 1
},
children: pageInfo.success_message[locale]
})]
});
}
function SubscriptionLinks({
mode,
subscriptions,
subscriptionId = void 0,
payee,
prefix,
t
}) {
if (!["subscription", "setup"].includes(mode)) return null;
if (subscriptions.length > 1) {
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
sx: {
width: "100%",
maxWidth: 420,
borderRadius: 3,
bgcolor: theme => theme.palette.mode === "dark" ? "rgba(255,255,255,0.04)" : "grey.50",
border: "1px solid",
borderColor: "divider",
overflow: "hidden",
animation: `${fadeUp} 0.5s ease 0.35s both`
},
children: subscriptions.map((sub, idx) => /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, {
children: [idx > 0 && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Divider, {}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
direction: "row",
alignItems: "center",
justifyContent: "space-between",
sx: {
px: 2.5,
py: 1.5
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
sx: {
fontSize: 13,
fontWeight: 500,
color: "text.secondary",
flex: 1,
minWidth: 0
},
noWrap: true,
children: sub.description || sub.id
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
sx: {
flex: 1,
borderBottom: "1px dashed",
borderColor: "grey.300",
mx: 2,
minWidth: 20
}
}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Link, {
href: (0, _ufo.joinURL)(prefix, `/customer/subscription/${sub.id}`),
underline: "none",
sx: {
display: "flex",
alignItems: "center",
gap: 0.5,
fontSize: 13,
fontWeight: 600,
color: "primary.main",
flexShrink: 0
},
children: [t("payment.checkout.next.view"), /* @__PURE__ */(0, _jsxRuntime.jsx)(_OpenInNew.default, {
sx: {
fontSize: 14
}
})]
})]
})]
}, sub.id))
});
}
if (subscriptionId) {
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, {
variant: "contained",
href: (0, _ufo.joinURL)(prefix, `/customer/subscription/${subscriptionId}`),
endIcon: /* @__PURE__ */(0, _jsxRuntime.jsx)(_OpenInNew.default, {
sx: {
fontSize: 16
}
}),
sx: {
width: "100%",
maxWidth: 320,
height: 52,
borderRadius: "16px",
textTransform: "none",
fontWeight: 700,
fontSize: {
xs: 16,
md: 17
},
letterSpacing: "0.02em",
color: theme => (0, _format.primaryContrastColor)(theme),
boxShadow: "0 8px 24px -4px rgba(59,130,246,0.25)",
"&:hover": {
boxShadow: "0 12px 28px -4px rgba(59,130,246,0.35)"
},
animation: `${fadeUp} 0.5s ease 0.35s both`
},
children: t("payment.checkout.next.subscription", {
payee
})
});
}
return null;
}
function InvoiceLink({
mode,
invoiceId = void 0,
prefix,
t
}) {
if (mode !== "payment" || !invoiceId) return null;
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, {
variant: "contained",
href: (0, _ufo.joinURL)(prefix, `/customer/invoice/${invoiceId}`),
endIcon: /* @__PURE__ */(0, _jsxRuntime.jsx)(_OpenInNew.default, {
sx: {
fontSize: 16
}
}),
sx: {
width: "100%",
maxWidth: 320,
height: 52,
borderRadius: "16px",
textTransform: "none",
fontWeight: 700,
fontSize: {
xs: 16,
md: 17
},
letterSpacing: "0.02em",
color: theme => (0, _format.primaryContrastColor)(theme),
boxShadow: "0 8px 24px -4px rgba(59,130,246,0.25)",
"&:hover": {
boxShadow: "0 12px 28px -4px rgba(59,130,246,0.35)"
},
animation: `${fadeUp} 0.5s ease 0.35s both`
},
children: t("payment.checkout.next.invoice")
});
}
function SuccessView({
submit,
session
}) {
const {
t,
locale
} = (0, _context.useLocaleContext)();
const {
vendorStatus
} = submit;
const payee = getPayee(session);
const prefix = (0, _util.getPrefix)();
const mode = session?.mode || "payment";
const resultSession = submit.result?.checkoutSession;
const subscriptions = session?.subscriptions || [];
const subscriptionId = session?.subscription_id || resultSession?.subscription_id;
const invoiceId = session?.invoice_id || submit.result?.checkoutSession?.invoice_id || submit.result?.checkoutSession?.payment_intent?.invoice_id;
const pageInfo = session?.metadata?.page_info;
const customMessage = getCustomMessage(session);
const submitType = session?.submit_type;
const messageKey = submitType === "donate" ? "payment.checkout.completed.donate" : `payment.checkout.completed.${mode}`;
const headline = customMessage || t(messageKey);
const isVendorProcessing = vendorStatus && !vendorStatus.isAllCompleted && !vendorStatus.hasFailed;
const containerRef = (0, _react.useRef)(null);
const showConfetti = !vendorStatus?.hasFailed;
useConfetti(containerRef, showConfetti);
return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, {
ref: containerRef,
sx: {
position: "relative",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
flex: 1,
minHeight: 400,
p: {
xs: 3,
md: 4
},
textAlign: "center",
overflow: "hidden"
},
children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
spacing: 3,
alignItems: "center",
sx: {
width: "100%",
maxWidth: 440
},
children: [vendorStatus?.hasFailed ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_ErrorOutline.default, {
sx: {
fontSize: 64,
color: "warning.main",
animation: `${scaleIn} 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) both`
}
}) : /* @__PURE__ */(0, _jsxRuntime.jsx)(HeroSuccessIcon, {}), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, {
spacing: 1,
alignItems: "center",
sx: {
animation: `${fadeUp} 0.5s ease 0.2s both`
},
children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
sx: {
fontSize: {
xs: 28,
md: 36
},
fontWeight: 800,
color: "text.primary",
lineHeight: 1.2,
letterSpacing: "-0.02em"
},
children: headline
}), payee && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, {
sx: {
fontSize: {
xs: 14,
md: 16
},
color: "text.secondary",
fontWeight: 500,
lineHeight: 1.6,
maxWidth: 360
},
children: t("payment.checkout.completed.tip", {
payee
})
})]
}), /* @__PURE__ */(0, _jsxRuntime.jsx)(PaymentReceipt, {
session: resultSession || session,
t
}), vendorStatus && /* @__PURE__ */(0, _jsxRuntime.jsx)(VendorProgressPanel, {
vendorStatus,
pageInfo,
locale,
t
}), vendorStatus?.hasFailed && vendorStatus.error && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Alert, {
severity: "warning",
sx: {
maxWidth: 420,
borderRadius: 3,
width: "100%"
},
children: vendorStatus.error
}), !isVendorProcessing && /* @__PURE__ */(0, _jsxRuntime.jsx)(SubscriptionLinks, {
mode,
subscriptions,
subscriptionId,
payee,
prefix,
t
}), !isVendorProcessing && /* @__PURE__ */(0, _jsxRuntime.jsx)(InvoiceLink, {
mode,
invoiceId,
prefix,
t
}), !isVendorProcessing && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, {
variant: "text",
startIcon: /* @__PURE__ */(0, _jsxRuntime.jsx)(_ArrowBack.default, {
sx: {
fontSize: 16
}
}),
onClick: () => {
if (window.history.length > 1) {
window.history.back();
} else {
window.location.href = "/";
}
},
sx: {
textTransform: "none",
fontWeight: 600,
fontSize: 14,
color: "text.secondary",
"&:hover": {
bgcolor: "action.hover"
},
animation: `${fadeUp} 0.5s ease 0.45s both`
},
children: t("common.back")
})]
})
});
}