UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

685 lines (676 loc) 21.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Root = void 0; module.exports = DonationForm; var _jsxRuntime = require("react/jsx-runtime"); var _context = require("@arcblock/ux/lib/Locale/context"); var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast")); var _Header = _interopRequireDefault(require("@blocklet/ui-react/lib/Header")); var _iconsMaterial = require("@mui/icons-material"); var _material = require("@mui/material"); var _system = require("@mui/system"); var _util = require("@ocap/util"); var _ahooks = require("ahooks"); var _react = require("react"); var _reactHookForm = require("react-hook-form"); var _ufo = require("ufo"); var _payment = require("../contexts/payment"); var _api = _interopRequireDefault(require("../libs/api")); var _util2 = require("../libs/util"); var _error = _interopRequireDefault(require("./error")); var _form = _interopRequireDefault(require("./form")); var _success = _interopRequireDefault(require("./success")); var _mobile = require("../hooks/mobile"); var _productDonation = _interopRequireDefault(require("./product-donation")); var _confirm = _interopRequireDefault(require("../components/confirm")); var _paymentBeneficiaries = _interopRequireDefault(require("../components/payment-beneficiaries")); var _donation = _interopRequireDefault(require("./skeleton/donation")); var _phoneValidator = require("../libs/phone-validator"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const getBenefits = async (id, url) => { const { data } = await _api.default.get(`/api/payment-links/${id}/benefits?${url ? `url=${url}` : ""}`); return data; }; function PaymentInner({ checkoutSession, paymentMethods, paymentLink, paymentIntent, customer, completed = false, mode, onPaid, onError, onChange, action, formRender = {}, benefits }) { const { t } = (0, _context.useLocaleContext)(); const { settings, session } = (0, _payment.usePaymentContext)(); const { isMobile } = (0, _mobile.useMobile)(); const [state, setState] = (0, _ahooks.useSetState)({ checkoutSession, submitting: false, paying: false, paid: false, paymentIntent, stripeContext: void 0, customer, customerLimited: false, stripePaying: false }); const query = (0, _util2.getQueryParams)(window.location.href); const defaultCurrencyId = query.currencyId || state.checkoutSession.currency_id || state.checkoutSession.line_items[0]?.price.currency_id; const defaultMethodId = paymentMethods.find(m => m.payment_currencies.some(c => c.id === defaultCurrencyId))?.id; const items = state.checkoutSession.line_items; const donationSettings = paymentLink?.donation_settings; const [benefitsState, setBenefitsState] = (0, _ahooks.useSetState)({ open: false, amount: "0" }); const methods = (0, _reactHookForm.useForm)({ defaultValues: { customer_name: customer?.name || session?.user?.fullName || "", customer_email: customer?.email || session?.user?.email || "", customer_phone: (0, _phoneValidator.formatPhone)(customer?.phone || session?.user?.phone || ""), payment_method: defaultMethodId, payment_currency: defaultCurrencyId, billing_address: Object.assign({ country: session?.user?.address?.country || "", state: session?.user?.address?.province || "", city: session?.user?.address?.city || "", line1: session?.user?.address?.line1 || "", line2: session?.user?.address?.line2 || "", postal_code: session?.user?.address?.postalCode || "" }, customer?.address || {}, { country: (0, _util2.isValidCountry)(customer?.address?.country || session?.user?.address?.country || "") ? customer?.address?.country : "us" }) } }); (0, _react.useEffect)(() => { if (!(0, _util2.isMobileSafari)()) { return () => {}; } let scrollTop = 0; const focusinHandler = () => { scrollTop = window.scrollY; }; const focusoutHandler = () => { window.scrollTo(0, scrollTop); }; document.body.addEventListener("focusin", focusinHandler); document.body.addEventListener("focusout", focusoutHandler); return () => { document.body.removeEventListener("focusin", focusinHandler); document.body.removeEventListener("focusout", focusoutHandler); }; }, []); (0, _react.useEffect)(() => { if (!methods || query.currencyId) { return; } if (state.checkoutSession.currency_id !== defaultCurrencyId) { methods.setValue("payment_currency", state.checkoutSession.currency_id); } }, [state.checkoutSession, defaultCurrencyId, query.currencyId]); const currencyId = (0, _reactHookForm.useWatch)({ control: methods.control, name: "payment_currency", defaultValue: defaultCurrencyId }); const currency = (0, _util2.findCurrency)(paymentMethods, currencyId) || settings.baseCurrency; (0, _react.useEffect)(() => { if (onChange) { onChange(methods.getValues()); } }, [currencyId]); const onChangeAmount = async ({ priceId, amount }) => { const amountStr = (0, _util.fromTokenToUnit)(amount, currency.decimal).toString(); setBenefitsState({ amount: amountStr }); try { if (!amountStr || Number(amountStr) === 0) { return; } const { data } = await _api.default.put(`/api/checkout-sessions/${state.checkoutSession.id}/amount`, { priceId, amount: amountStr }); setState({ checkoutSession: data }); } catch (err) { console.error(err); _Toast.default.error((0, _util2.formatError)(err)); } }; const handlePaid = result => { setState({ checkoutSession: result.checkoutSession }); onPaid(result); }; const renderBenefits = () => { if (!benefits) { return null; } if (benefits.length === 1) { return t("payment.checkout.donation.benefits.one", { name: benefits[0].name }); } return t("payment.checkout.donation.benefits.multiple", { count: benefits.length }); }; return /* @__PURE__ */(0, _jsxRuntime.jsx)(_reactHookForm.FormProvider, { ...methods, children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { className: "cko-container", sx: { gap: { sm: mode === "standalone" ? 0 : mode === "inline" ? 4 : 8 } }, children: [completed ? /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_success.default, { mode, payee: (0, _util2.getStatementDescriptor)(state.checkoutSession.line_items), action: state.checkoutSession.mode, invoiceId: state.checkoutSession.invoice_id, subscriptionId: state.checkoutSession.subscription_id, message: paymentLink?.after_completion?.hosted_confirmation?.custom_message || t("payment.checkout.completed.donate") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Divider, { sx: { mt: { xs: "16px", md: "-24px" }, mb: { xs: "16px", md: "16px" } } }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { direction: "row", sx: { justifyContent: "flex-end", alignItems: "center", flexWrap: "wrap", gap: 1 }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, { variant: "outlined", size: "large", onClick: formRender?.onCancel, sx: { width: "fit-content", minWidth: 120 }, children: t("common.close") }) })] }) : /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [benefitsState.open && /* @__PURE__ */(0, _jsxRuntime.jsx)(_paymentBeneficiaries.default, { data: benefits, currency, totalAmount: benefitsState.amount }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { display: benefitsState.open ? "none" : "block" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", sx: { justifyContent: "space-between", alignItems: "center", mb: 2 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { title: t("payment.checkout.orderSummary"), sx: { color: "text.primary", fontSize: "18px", fontWeight: "500", lineHeight: "24px" }, children: t("payment.checkout.donation.tipAmount") }), !isMobile && donationSettings?.amount?.presets && donationSettings.amount.presets.length > 0 && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { sx: { color: "text.secondary", fontSize: "13px", display: "flex", alignItems: "center", gap: 0.5, opacity: 0.8 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { component: "span", sx: { border: "1px solid", borderColor: "divider", borderRadius: 0.75, px: 0.75, py: 0.25, fontSize: "12px", lineHeight: 1, color: "text.secondary", fontWeight: "400", bgcolor: "transparent" }, children: "Tab" }), t("payment.checkout.donation.tabHint")] })] }), items.map(x => /* @__PURE__ */(0, _jsxRuntime.jsx)(_productDonation.default, { item: x, settings: donationSettings, onChange: onChangeAmount, currency }, `${x.price_id}-${currency.id}`))] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Divider, { sx: { mt: { xs: "32px", md: "0" }, mb: { xs: "16px", md: "-8px" } } }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", sx: { justifyContent: "space-between", alignItems: "center", flexWrap: "wrap", gap: 1 }, children: [benefits && benefits.length > 0 && (benefitsState.open ? /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { onClick: () => setBenefitsState({ open: false }), sx: { cursor: "pointer", color: "text.secondary", "&:hover": { color: "text.primary" }, display: "flex", alignItems: "center" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.ArrowBackOutlined, { className: "benefits-arrow", sx: { fontSize: "18px", mr: 0.5 } }), t("common.back")] }) : /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { onClick: () => setBenefitsState({ open: true }), sx: { display: "flex", gap: 0.5, alignItems: "center", color: "text.secondary", cursor: "pointer", "& .benefits-arrow": { opacity: 0, transform: "translateX(-4px)", transition: "all 0.2s" }, "&:hover": { color: "text.primary", "& .benefits-arrow": { opacity: 1, transform: "translateX(0)" } } }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.HelpOutlineOutlined, { sx: { fontSize: "18px" } }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { variant: "body2", children: renderBenefits() }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.ArrowForwardOutlined, { className: "benefits-arrow", sx: { fontSize: "18px" } })] })), benefitsState.open ? null : /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { sx: { display: "flex", gap: 2, flex: { xs: 1, md: "auto" }, justifyContent: "flex-end", whiteSpace: "nowrap" }, children: [formRender?.cancel, /* @__PURE__ */(0, _jsxRuntime.jsx)(_form.default, { currencyId, checkoutSession: state.checkoutSession, paymentMethods, paymentIntent, paymentLink, customer, onPaid: handlePaid, onError, mode, action, onlyShowBtn: true, isDonation: true })] })] })] }), state.customerLimited && /* @__PURE__ */(0, _jsxRuntime.jsx)(_confirm.default, { onConfirm: () => window.open((0, _ufo.joinURL)((0, _util2.getPrefix)(), `/customer/invoice/past-due?referer=${encodeURIComponent(window.location.href)}`), "_self"), onCancel: () => setState({ customerLimited: false }), confirm: t("payment.customer.pastDue.alert.confirm"), title: t("payment.customer.pastDue.alert.title"), message: t("payment.customer.pastDue.alert.description"), color: "primary" })] }) }); } function DonationForm({ checkoutSession, paymentMethods, paymentIntent, paymentLink, customer, completed = false, error = null, mode, onPaid, onError, onChange, goBack, action, showCheckoutSummary = true, formRender = {}, id }) { const { t } = (0, _context.useLocaleContext)(); const { refresh, livemode, setLivemode } = (0, _payment.usePaymentContext)(); const { isMobile } = (0, _mobile.useMobile)(); const [delay, setDelay] = (0, _react.useState)(false); const isMobileSafariEnv = (0, _util2.isMobileSafari)(); const paymentLinkId = id.startsWith("plink_") ? id : void 0; const { data: benefits, loading: benefitLoading } = (0, _ahooks.useRequest)(() => { if (paymentLinkId) { return getBenefits(paymentLinkId); } return Promise.resolve([]); }, { refreshDeps: [paymentLinkId || paymentLink?.id], ready: !!paymentLinkId || !!paymentLink?.id }); (0, _ahooks.useMount)(() => { setTimeout(() => { setDelay(true); }, 500); }); (0, _react.useEffect)(() => { if (checkoutSession) { if (livemode !== checkoutSession.livemode) { setLivemode(checkoutSession.livemode); } } }, [checkoutSession, livemode, setLivemode, refresh]); const renderContent = () => { const footer = /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Divider, { sx: { mt: { xs: "16px", md: "-24px" }, mb: { xs: "16px", md: "-16px" } } }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { direction: "row", sx: { justifyContent: "flex-end", alignItems: "center", flexWrap: "wrap", gap: 1 }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, { variant: "outlined", size: "large", onClick: formRender?.onCancel, children: t("common.cancel") }) })] }); if (error) { return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_error.default, { mode, title: "Oops", description: (0, _util2.formatError)(error), button: null }), footer] }); } if (!checkoutSession || !delay || !paymentLink || benefitLoading) { return /* @__PURE__ */(0, _jsxRuntime.jsx)(_donation.default, {}); } if (checkoutSession?.expires_at <= Math.round(Date.now() / 1e3)) { return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_error.default, { mode, title: t("payment.checkout.expired.title"), description: t("payment.checkout.expired.description"), button: null }), footer] }); } if (!checkoutSession.line_items.length) { return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_error.default, { mode, title: t("payment.checkout.emptyItems.title"), description: t("payment.checkout.emptyItems.description"), button: null }), footer] }); } return /* @__PURE__ */(0, _jsxRuntime.jsx)(PaymentInner, { formRender, checkoutSession, paymentMethods, paymentLink, paymentIntent, completed: completed || checkoutSession.status === "complete", customer, onPaid, onError, onChange, goBack, mode, action, showCheckoutSummary, benefits }); }; return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { display: "flex", flexDirection: "column", height: mode === "standalone" ? "100vh" : "auto", overflow: isMobileSafariEnv ? "visible" : "hidden" }, children: [mode === "standalone" ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_Header.default, { meta: void 0, addons: void 0, sessionManagerProps: void 0, homeLink: void 0, theme: void 0, hideNavMenu: void 0, maxWidth: false, sx: { borderBottom: "1px solid", borderColor: "divider" } }) : null, /* @__PURE__ */(0, _jsxRuntime.jsxs)(Root, { mode, sx: { flex: 1, overflow: { xs: isMobileSafariEnv ? "visible" : "auto", md: "hidden" }, ...(isMobile && mode === "standalone" ? { ".cko-payment-submit-btn": { position: "fixed", bottom: 20, left: 0, right: 0, zIndex: 999, backgroundColor: "background.paper", padding: "10px", textAlign: "center", button: { maxWidth: 542 } } } : {}) }, children: [goBack && /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.ArrowBackOutlined, { sx: { mr: 0.5, color: "text.secondary", alignSelf: "flex-start", margin: "16px 0", cursor: "pointer" }, onClick: goBack, fontSize: "medium" }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { className: "cko-container", sx: { gap: { sm: mode === "standalone" ? 0 : mode === "inline" ? 4 : 8 } }, children: renderContent() })] })] }); } const Root = exports.Root = (0, _system.styled)(_material.Box)` box-sizing: border-box; display: flex; flex-direction: column; align-items: center; overflow: hidden; position: relative; .cko-container { overflow: hidden; width: 100%; display: flex; flex-direction: column; justify-content: center; position: relative; flex: 1; } .cko-overview { position: relative; flex-direction: column; display: ${props => props.mode.endsWith("-minimal") ? "none" : "flex"}; background: ${({ theme }) => theme.palette.background.default}; min-height: 'auto'; } .cko-footer { display: ${props => props.mode.endsWith("-minimal") ? "none" : "block"}; text-align: center; margin-top: 20px; } .cko-payment-form { .MuiFormLabel-root { color: ${({ theme }) => theme.palette.grey.A700}; font-weight: 500; margin-top: 12px; margin-bottom: 4px; } .MuiBox-root { margin: 0; } .MuiFormHelperText-root { margin-left: 14px; } } @media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) { padding-top: 0; overflow: auto; .cko-container { flex-direction: column; justify-content: flex-start; gap: 0; overflow: visible; min-width: 200px; } .cko-overview { width: 100%; min-height: auto; flex: none; } .cko-footer { position: relative; margin-bottom: 4px; margin-top: 0; bottom: 0; left: 0; transform: translateX(0); } } `;