@blocklet/payment-react
Version:
Reusable react components for payment kit v2
685 lines (676 loc) • 21.4 kB
JavaScript
;
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);
}
}
`;