UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

803 lines (802 loc) 28.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); module.exports = AutoTopup; exports.waitForAutoRechargeComplete = void 0; var _jsxRuntime = require("react/jsx-runtime"); var _react = require("react"); var _material = require("@mui/material"); var _iconsMaterial = require("@mui/icons-material"); var _reactHookForm = require("react-hook-form"); var _context = require("@arcblock/ux/lib/Locale/context"); var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast")); var _Dialog = _interopRequireDefault(require("@arcblock/ux/lib/Dialog")); var _ahooks = require("ahooks"); var _reactRouterDom = require("react-router-dom"); var _ufo = require("ufo"); var _pWaitFor = _interopRequireDefault(require("p-wait-for")); var _DID = _interopRequireDefault(require("@arcblock/ux/lib/DID")); var _switchButton = _interopRequireDefault(require("../switch-button")); var _api = _interopRequireDefault(require("../../libs/api")); var _util = require("../../libs/util"); var _payment = require("../../contexts/payment"); var _navigation = require("../../libs/navigation"); var _collapse = _interopRequireDefault(require("../collapse")); var _input = _interopRequireDefault(require("../input")); var _stripe = _interopRequireDefault(require("../../payment/form/stripe")); var _productCard = _interopRequireDefault(require("./product-card")); var _label = _interopRequireDefault(require("../label")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const fetchConfig = async (customerId, currencyId) => { const { data } = await _api.default.get(`/api/auto-recharge-configs/customer/${customerId}`, { params: { currency_id: currencyId } }); return data; }; const fetchCurrencyBalance = async (currencyId, payerAddress) => { const { data } = await _api.default.get("/api/customers/payer-token", { params: { currencyId, payerAddress } }); return data; }; const DEFAULT_VALUES = { enabled: false, threshold: "100", quantity: 1, payment_method_id: "", recharge_currency_id: "", price_id: "", daily_max_amount: 0, daily_max_attempts: 0 }; const waitForAutoRechargeComplete = async configId => { let result; await (0, _pWaitFor.default)(async () => { const { data } = await _api.default.get(`/api/auto-recharge-configs/retrieve/${configId}`); result = data; return !!result.payment_settings?.payment_method_options?.[result.paymentMethod.type]?.payer; }, { interval: 2e3, timeout: 3 * 60 * 1e3 }); return result; }; exports.waitForAutoRechargeComplete = waitForAutoRechargeComplete; function PaymentMethodDisplay({ config, onChangePaymentMethod, paymentMethod, currency }) { const { t } = (0, _context.useLocaleContext)(); const [changePaymentMethod, setChangePaymentMethod] = (0, _react.useState)(false); const navigate = (0, _reactRouterDom.useNavigate)(); const paymentInfo = config?.payment_settings?.payment_method_options?.[paymentMethod.type || ""]; const { data: balanceInfo, loading: balanceLoading } = (0, _ahooks.useRequest)(async () => { if (paymentMethod.type === "stripe") { return null; } const result = await fetchCurrencyBalance(currency.id, paymentInfo?.payer); return result; }, { refreshDeps: [currency.id, paymentInfo?.payer], ready: !!currency.id && !!paymentInfo?.payer }); const handleChangeToggle = () => { const newChange = !changePaymentMethod; setChangePaymentMethod(newChange); onChangePaymentMethod(newChange); }; if (!paymentInfo) { return null; } const handleRecharge = e => { const url = (0, _ufo.joinURL)((0, _util.getPrefix)(), `/customer/recharge/${currency.id}?rechargeAddress=${paymentInfo?.payer}`); const link = (0, _navigation.createLink)(url, true); (0, _navigation.handleNavigation)(e, link, navigate); }; const renderPaymentMethodInfo = () => { if (paymentMethod.type === "stripe") { return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.CreditCard, { fontSize: "small", color: "primary" }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { variant: "body2", children: ["**** **** **** ", paymentInfo?.card_last4 || "****"] }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { variant: "body2", sx: { color: "text.secondary", textTransform: "uppercase" }, children: paymentInfo?.card_brand || "CARD" }), paymentInfo?.exp_time && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { variant: "body2", sx: { color: "text.secondary", borderLeft: "1px solid", borderColor: "divider", pl: 1 }, children: paymentInfo?.exp_time })] }); } return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { spacing: 1, sx: { borderRadius: 1, backgroundColor: theme => theme.palette.mode === "dark" ? "grey.100" : "grey.50", p: 2 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_DID.default, { did: paymentInfo?.payer, responsive: false, compact: true, copyable: false }), (balanceInfo || balanceLoading) && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 1, sx: { alignItems: "center" }, children: [balanceLoading ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CircularProgress, { size: 14, sx: { mr: 0.5 } }) : /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.AccountBalanceWalletOutlined, { fontSize: "small", sx: { color: "text.lighter" } }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { variant: "body2", sx: { color: "text.primary" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)("strong", { children: [(0, _util.formatBNStr)(balanceInfo?.token || "0", currency?.decimal), " "] }), currency?.symbol || ""] })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Button, { size: "small", variant: "text", onClick: handleRecharge, sx: { fontSize: "smaller", color: "primary.main" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.AddOutlined, { fontSize: "small" }), t("payment.autoTopup.addFunds")] })] })] }); }; return /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { sx: { p: 2, border: "1px solid", borderColor: changePaymentMethod ? "primary.main" : "divider", borderRadius: 1, bgcolor: changePaymentMethod ? "primary.50" : "background.paper" }, children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { spacing: 2, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { flexDirection: { xs: "column", sm: "row" }, alignItems: { xs: "flex-start", sm: "center" }, justifyContent: { xs: "flex-start", sm: "space-between" } }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { variant: "subtitle2", sx: { display: "flex", alignItems: "center", gap: 1, whiteSpace: "nowrap" }, children: t("payment.autoTopup.currentPaymentMethod") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, { size: "small", startIcon: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.SwapHoriz, {}), onClick: handleChangeToggle, variant: "text", sx: { color: "primary.main", whiteSpace: "nowrap", alignSelf: { xs: "flex-end", sm: "center" } }, children: changePaymentMethod ? t("payment.autoTopup.keepCurrent") : t("payment.autoTopup.changePaymentMethod") })] }), changePaymentMethod ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { variant: "body2", sx: { color: "text.secondary" }, children: t("payment.autoTopup.changePaymentMethodTip") }) : renderPaymentMethodInfo()] }) }); } function AutoTopup({ open, onClose, currencyId, onSuccess = () => {}, onError = () => {}, defaultEnabled = void 0 }) { const { t, locale } = (0, _context.useLocaleContext)(); const { session, connect, settings } = (0, _payment.usePaymentContext)(); const [changePaymentMethod, setChangePaymentMethod] = (0, _react.useState)(false); const [state, setState] = (0, _ahooks.useSetState)({ loading: false, submitting: false, authorizationRequired: false, stripeContext: { client_secret: "", intent_type: "", status: "", public_key: "", customer: {} } }); const currencies = (0, _util.flattenPaymentMethods)(settings.paymentMethods); const methods = (0, _reactHookForm.useForm)({ defaultValues: { enabled: defaultEnabled || DEFAULT_VALUES.enabled, threshold: DEFAULT_VALUES.threshold, quantity: DEFAULT_VALUES.quantity, payment_method_id: DEFAULT_VALUES.payment_method_id, recharge_currency_id: DEFAULT_VALUES.recharge_currency_id, price_id: DEFAULT_VALUES.price_id, daily_limits: { max_attempts: DEFAULT_VALUES.daily_max_attempts, max_amount: DEFAULT_VALUES.daily_max_amount } } }); const { handleSubmit, setValue, watch } = methods; const enabled = watch("enabled"); const quantity = watch("quantity"); const rechargeCurrencyId = watch("recharge_currency_id"); const handleClose = () => { setState({ loading: false, submitting: false, authorizationRequired: false }); onClose(); }; const { data: config } = (0, _ahooks.useRequest)(() => fetchConfig(session?.user?.did, currencyId), { refreshDeps: [session?.user?.did, currencyId], ready: !!session?.user?.did && !!currencyId, onError: error => { _Toast.default.error((0, _util.formatError)(error)); }, onSuccess: data => { setValue("enabled", defaultEnabled || data.enabled); setValue("threshold", data.threshold); setValue("quantity", data.quantity); setValue("payment_method_id", data.payment_method_id); setValue("recharge_currency_id", data.recharge_currency_id || data.price?.currency_id); setValue("price_id", data.price_id); setValue("daily_limits", { max_amount: data.daily_limits?.max_amount || 0, max_attempts: data.daily_limits?.max_attempts || 0 }); } }); const filterCurrencies = (0, _react.useMemo)(() => { return currencies.filter(c => config?.price?.currency_options?.find(o => o.currency_id === c.id)); }, [currencies, config]); const handleConnected = async () => { try { const result = await waitForAutoRechargeComplete(config?.id); if (result) { setState({ submitting: false, authorizationRequired: false }); onSuccess?.(config); handleClose(); _Toast.default.success(t("payment.autoTopup.saveSuccess")); } } catch (err) { _Toast.default.error((0, _util.formatError)(err)); } finally { setState({ submitting: false, authorizationRequired: false }); } }; const handleDisable = async () => { try { const submitData = { ...config, enabled: false }; if (!config?.enabled) { return; } const { data } = await _api.default.post("/api/auto-recharge-configs/submit", submitData); onSuccess?.(data); _Toast.default.success(t("payment.autoTopup.disableSuccess")); } catch (error) { _Toast.default.error((0, _util.formatError)(error)); onError?.(error); } }; const handleEnableChange = async checked => { setValue("enabled", checked); if (!checked) { await handleDisable(); } }; const handleAuthorizationRequired = authData => { setState({ authorizationRequired: true }); if (authData.stripeContext) { setState({ stripeContext: { client_secret: authData.stripeContext.client_secret, intent_type: authData.stripeContext.intent_type, status: authData.stripeContext.status, public_key: authData.paymentMethod.settings.stripe.publishable_key, customer: authData.customer } }); } else if (authData.delegation) { handleDidConnect(); } }; const handleDidConnect = () => { try { setState({ submitting: true }); connect.open({ containerEl: void 0, saveConnect: false, locale, action: "auto-recharge-auth", prefix: (0, _ufo.joinURL)((0, _util.getPrefix)(), "/api/did"), extraParams: { autoRechargeConfigId: config?.id }, messages: { scan: t("payment.autoTopup.authTip"), title: t("payment.autoTopup.authTitle"), confirm: t("common.connect.confirm") }, onSuccess: async () => { connect.close(); await handleConnected(); }, onClose: () => { connect.close(); setState({ submitting: false, authorizationRequired: false }); }, onError: err => { setState({ submitting: false, authorizationRequired: false }); _Toast.default.error((0, _util.formatError)(err)); } }); } catch (error) { setState({ submitting: false, authorizationRequired: false }); _Toast.default.error((0, _util.formatError)(error)); } }; const handleFormSubmit = async formData => { setState({ submitting: true }); try { const submitData = { customer_id: session?.user?.did, enabled: formData.enabled, threshold: formData.threshold, currency_id: currencyId, recharge_currency_id: formData.recharge_currency_id, price_id: formData.price_id, quantity: formData.quantity, daily_limits: { max_attempts: formData.daily_limits.max_attempts || 0, max_amount: formData.daily_limits.max_amount || "0" }, change_payment_method: changePaymentMethod }; const { data } = await _api.default.post("/api/auto-recharge-configs/submit", submitData); if (data.balanceResult && !data.balanceResult.sufficient) { await handleAuthorizationRequired({ ...data.balanceResult, paymentMethod: data.paymentMethod, customer: data.customer }); return; } setState({ submitting: false, authorizationRequired: false }); onSuccess?.(data); handleClose(); _Toast.default.success(t("payment.autoTopup.saveSuccess")); } catch (error) { setState({ submitting: false, authorizationRequired: false }); _Toast.default.error((0, _util.formatError)(error)); onError?.(error); } }; const onSubmit = formData => { handleFormSubmit(formData); }; const rechargeCurrency = filterCurrencies.find(c => c.id === rechargeCurrencyId); const selectedMethod = settings.paymentMethods.find(method => { return method.payment_currencies.find(c => c.id === rechargeCurrencyId); }); const showStripeForm = state.authorizationRequired && selectedMethod?.type === "stripe"; const onStripeConfirm = async () => { await handleConnected(); }; const onStripeCancel = () => { setState({ submitting: false, authorizationRequired: false }); }; return /* @__PURE__ */(0, _jsxRuntime.jsx)(_Dialog.default, { open, onClose: handleClose, maxWidth: "sm", fullWidth: true, className: "base-dialog", title: t("payment.autoTopup.title"), actions: enabled ? /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", spacing: 2, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Button, { variant: "outlined", onClick: handleClose, disabled: state.submitting, children: t("common.cancel") }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Button, { variant: "contained", onClick: () => handleSubmit(onSubmit)(), disabled: state.loading || state.authorizationRequired || state.submitting, children: [state.submitting && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CircularProgress, { size: 20, sx: { mr: 1 } }), t("payment.autoTopup.saveConfiguration")] })] }) : null, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_reactHookForm.FormProvider, { ...methods, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { component: "form", onSubmit: handleSubmit(onSubmit), children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { gap: 2 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Alert, { severity: "info", children: t("payment.autoTopup.tip") }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { spacing: 2, direction: "row", sx: { alignItems: "center" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { variant: "subtitle1", children: t("payment.autoTopup.enableLabel") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.FormControlLabel, { control: /* @__PURE__ */(0, _jsxRuntime.jsx)(_switchButton.default, { checked: enabled, onChange: e => handleEnableChange(e.target.checked) }), label: "" })] }), enabled && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { sx: { pt: 1 }, children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { spacing: 2.5, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { gap: 2, flexDirection: { xs: "column", sm: "row" }, alignItems: { xs: "flex-start", sm: "center" }, justifyContent: { xs: "flex-start", sm: "space-between" }, ".MuiTextField-root": { width: { xs: "100%", sm: "auto" } } }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_label.default, { boxSx: { width: "fit-content", whiteSpace: "nowrap", marginBottom: 0 }, children: t("payment.autoTopup.triggerThreshold") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_reactHookForm.Controller, { name: "threshold", control: methods.control, rules: { required: t("payment.checkout.required"), min: { value: 0, message: t("payment.autoTopup.thresholdMinError") } }, render: ({ field }) => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.TextField, { ...field, type: "number", placeholder: t("payment.autoTopup.thresholdPlaceholder"), sx: { input: { minWidth: 80 } }, slotProps: { input: { endAdornment: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.InputAdornment, { position: "end", children: config?.currency?.name }) }, htmlInput: { min: 0, step: 0.01 } } }) })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { gap: 2, flexDirection: { xs: "column", sm: "row" }, alignItems: { xs: "flex-start", sm: "center" }, justifyContent: { xs: "flex-start", sm: "space-between" } }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_label.default, { boxSx: { width: "fit-content", whiteSpace: "nowrap", marginBottom: 0 }, children: t("payment.autoTopup.purchaseBelow") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_reactHookForm.Controller, { name: "recharge_currency_id", control: methods.control, render: ({ field }) => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Select, { ...field, value: field.value, onChange: e => field.onChange(e.target.value), size: "small", children: filterCurrencies.map(x => /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.MenuItem, { value: x?.id, children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "row", sx: { alignItems: "center", gap: 1 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Avatar, { src: x?.logo, sx: { width: 20, height: 20 }, alt: x?.symbol }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { sx: { color: "text.secondary" }, children: [x?.symbol, " (", x?.method?.name, ")"] })] }) }, x?.id)) }) })] }), config?.price?.product && /* @__PURE__ */(0, _jsxRuntime.jsx)(_productCard.default, { product: config.price.product, price: config.price, creditCurrency: config.currency, currency: filterCurrencies.find(c => c.id === rechargeCurrencyId) || filterCurrencies[0], quantity, onQuantityChange: newQuantity => setValue("quantity", newQuantity), maxQuantity: 9999, minQuantity: 1 }), config && rechargeCurrency && /* @__PURE__ */(0, _jsxRuntime.jsx)(PaymentMethodDisplay, { config, onChangePaymentMethod: setChangePaymentMethod, currency: rechargeCurrency, paymentMethod: selectedMethod }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_collapse.default, { trigger: t("payment.autoTopup.advanced"), children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { gap: 2, pl: 2, pr: 1 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_input.default, { name: "daily_limits.max_amount", label: t("payment.autoTopup.dailyLimits.maxAmount"), type: "number", placeholder: t("payment.autoTopup.dailyLimits.maxAmountPlaceholder"), tooltip: t("payment.autoTopup.dailyLimits.maxAmountDescription"), inputProps: { min: 0, step: 0.01 }, slotProps: { input: { endAdornment: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.InputAdornment, { position: "end", children: filterCurrencies.find(c => c.id === rechargeCurrencyId)?.symbol || "" }) } }, sx: { maxWidth: { xs: "100%", sm: "220px" } }, layout: "horizontal" }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_input.default, { name: "daily_limits.max_attempts", label: t("payment.autoTopup.dailyLimits.maxAttempts"), type: "number", placeholder: t("payment.autoTopup.dailyLimits.maxAttemptsPlaceholder"), tooltip: t("payment.autoTopup.dailyLimits.maxAttemptsDescription"), inputProps: { min: 0, step: 1 }, sx: { maxWidth: { xs: "100%", sm: "220px" } }, layout: "horizontal" })] }) })] }) }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { spacing: 2, children: showStripeForm && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { sx: { mt: 2 }, children: state.stripeContext && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { sx: { mt: 2 }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_stripe.default, { clientSecret: state.stripeContext.client_secret, intentType: state.stripeContext.intent_type, publicKey: state.stripeContext.public_key, customer: state.stripeContext.customer, mode: "setup", onConfirm: onStripeConfirm, onCancel: onStripeCancel }) }) }) })] })] }) }) }) }); }