UNPKG

@blocklet/payment-react

Version:

Reusable react components for payment kit v2

1,400 lines (1,399 loc) 60.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); module.exports = PaymentForm; exports.waitForCheckoutComplete = exports.hasDidWallet = void 0; var _jsxRuntime = require("react/jsx-runtime"); require("react-international-phone/style.css"); var _context = require("@arcblock/ux/lib/Locale/context"); var _reactInternationalPhone = require("react-international-phone"); var _Toast = _interopRequireDefault(require("@arcblock/ux/lib/Toast")); var _material = require("@mui/material"); var _ahooks = require("ahooks"); var _pWaitFor = _interopRequireDefault(require("p-wait-for")); var _react = require("react"); var _reactHookForm = require("react-hook-form"); var _ufo = require("ufo"); var _useBus = require("use-bus"); var _isEmail = _interopRequireDefault(require("validator/es/lib/isEmail")); var _util = require("@ocap/util"); var _DID = _interopRequireDefault(require("@arcblock/ux/lib/DID")); var _isEmpty = _interopRequireDefault(require("lodash/isEmpty")); var _iconsMaterial = require("@mui/icons-material"); var _withTracker = require("@arcblock/ux/lib/withTracker"); var _trim = _interopRequireDefault(require("lodash/trim")); var _input = _interopRequireDefault(require("../../components/input")); var _label = _interopRequireDefault(require("../../components/label")); var _payment = require("../../contexts/payment"); var _subscription = require("../../hooks/subscription"); var _api = _interopRequireDefault(require("../../libs/api")); var _util2 = require("../../libs/util"); var _address = _interopRequireDefault(require("./address")); var _currency = _interopRequireDefault(require("./currency")); var _phone = _interopRequireDefault(require("./phone")); var _stripe = _interopRequireDefault(require("./stripe")); var _mobile = require("../../hooks/mobile"); var _phoneValidator = require("../../libs/phone-validator"); var _loadingButton = _interopRequireDefault(require("../../components/loading-button")); var _overDueInvoicePayment = _interopRequireDefault(require("../../components/over-due-invoice-payment")); var _currency2 = require("../../libs/currency"); var _confirm = _interopRequireDefault(require("../../components/confirm")); var _serviceSuspendedDialog = _interopRequireDefault(require("../../components/service-suspended-dialog")); var _priceChangeConfirm = _interopRequireDefault(require("../../components/price-change-confirm")); var _validator = require("../../libs/validator"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const generateIdempotencyKey = (sessionId, currencyId) => { return `${sessionId}-${currencyId}-${Date.now()}-${Math.random().toString(36).substring(2, 10)}`; }; const waitForCheckoutComplete = async sessionId => { let result; await (0, _pWaitFor.default)(async () => { const { data } = await _api.default.get(`/api/checkout-sessions/retrieve/${sessionId}`); if (data.paymentIntent && data.paymentIntent.status === "requires_action" && data.paymentIntent.last_payment_error) { throw new Error(data.paymentIntent.last_payment_error.message); } result = data; return ( // eslint-disable-next-line @typescript-eslint/return-await data.checkoutSession?.status === "complete" && ["paid", "no_payment_required"].includes(data.checkoutSession?.payment_status) ); }, { interval: 2e3, timeout: 3 * 60 * 1e3 }); return result; }; exports.waitForCheckoutComplete = waitForCheckoutComplete; const hasDidWallet = user => { const connected = user?.connectedAccounts || user?.extraConfigs?.connectedAccounts || []; return connected.some(x => x.provider === "wallet"); }; exports.hasDidWallet = hasDidWallet; const setUserFormValues = (userInfo, currentValues, setValue, options = {}) => { const { preferExisting = true, shouldValidate = false } = options; const basicFields = { customer_name: userInfo.name || userInfo.fullName, customer_email: userInfo.email, customer_phone: (0, _phoneValidator.formatPhone)(userInfo.phone) }; const addressFields = { "billing_address.state": userInfo.address?.state || userInfo.address?.province, "billing_address.line1": userInfo.address?.line1, "billing_address.line2": userInfo.address?.line2, "billing_address.city": userInfo.address?.city, "billing_address.postal_code": userInfo.address?.postal_code || userInfo.address?.postalCode, "billing_address.country": userInfo.address?.country || "us" }; if (options.showPhone) { addressFields["billing_address.country"] = userInfo.metadata?.phone?.country || userInfo.address?.country; } const allFields = { ...addressFields, ...basicFields }; const updatedFields = { ...currentValues, billing_address: { ...currentValues.billing_address } }; Object.entries(allFields).forEach(([field, value]) => { if (!preferExisting || !currentValues[field.split(".")[0]]) { setValue(field, value, { shouldValidate }); if (field.startsWith("billing_address.")) { const addressKey = field.replace("billing_address.", ""); updatedFields.billing_address[addressKey] = value; } else { updatedFields[field] = value; } } }); return updatedFields; }; function PaymentForm({ checkoutSession, paymentMethods, paymentIntent, paymentLink, customer, onPaid, onError, onQuoteUpdated = void 0, onPaymentIntentUpdate = void 0, // mode, action, onlyShowBtn = false, isDonation = false, rateUnavailable = false }) { const { t, locale } = (0, _context.useLocaleContext)(); const { isMobile } = (0, _mobile.useMobile)(); const { session, connect, payable, setPaymentState } = (0, _payment.usePaymentContext)(); const subscription = (0, _subscription.useSubscription)("events"); const formErrorPosition = "bottom"; const { control, getValues, setValue, handleSubmit, formState: { errors }, trigger } = (0, _reactHookForm.useFormContext)(); const errorRef = (0, _react.useRef)(null); const processingRef = (0, _react.useRef)(false); const idempotencyKeyRef = (0, _react.useRef)(""); const sessionFingerprintRef = (0, _react.useRef)(""); const quantityInventoryStatus = (0, _react.useMemo)(() => { let status = true; for (const item of checkoutSession.line_items) { if ((0, _util2.formatQuantityInventory)(item.price, item.quantity)) { status = false; break; } } return status; }, [checkoutSession]); const [state, setState] = (0, _ahooks.useSetState)({ submitting: false, paying: false, paid: false, paymentIntent, stripeContext: void 0, customer, customerLimited: false, serviceSuspended: false, stripePaying: false, fastCheckoutInfo: null, creditInsufficientInfo: null, showEditForm: false, priceChangeConfirm: null }); const currencies = (0, _util2.flattenPaymentMethods)(paymentMethods); const searchParams = (0, _util2.getQueryParams)(window.location.href); const onCheckoutComplete = (0, _ahooks.useMemoizedFn)(async ({ response }) => { if (response.id === checkoutSession.id && state.paid === false) { await handleConnected(); } }); (0, _react.useEffect)(() => { if (subscription) { subscription.on("checkout.session.completed", onCheckoutComplete); } }, [subscription]); (0, _react.useEffect)(() => { setPaymentState({ paying: state.submitting || state.paying, stripePaying: state.stripePaying }); }, [state.submitting, state.paying, state.stripePaying]); const mergeUserInfo = (customerInfo, userInfo) => { return { ...(userInfo || {}), name: customerInfo?.name || customerInfo?.fullName || userInfo?.name || userInfo?.fullName, fullName: customerInfo?.name || customerInfo?.fullName || userInfo?.name || userInfo?.fullName, email: customerInfo?.email || userInfo?.email, phone: customerInfo?.phone || userInfo?.phone, address: { ...(userInfo?.address || {}), ...(customerInfo?.address || {}), country: customerInfo?.address?.country || userInfo?.address?.country, state: customerInfo?.address?.state || userInfo?.address?.province, line1: customerInfo?.address?.line1 || userInfo?.address?.line1, line2: customerInfo?.address?.line2 || userInfo?.address?.line2, city: customerInfo?.address?.city || userInfo?.address?.city, postal_code: customerInfo?.address?.postal_code || userInfo?.address?.postalCode }, metadata: { ...(userInfo?.metadata || {}), phone: { country: customerInfo?.address?.country || userInfo?.metadata?.phone?.country, phoneNumber: customerInfo?.phone || userInfo?.metadata?.phone?.phoneNumber } } }; }; (0, _react.useEffect)(() => { const initUserInfo = async () => { if (session?.user) { const values = getValues(); let userInfo = session.user; try { const { data: customerInfo } = await _api.default.get("/api/customers/me?skipSummary=1&fallback=1"); userInfo = mergeUserInfo(customerInfo, userInfo); } catch (err) { userInfo = mergeUserInfo(customer || {}, userInfo); console.error(err); } const formValues = setUserFormValues(userInfo, values, setValue, { preferExisting: false, showPhone: checkoutSession.phone_number_collection?.enabled }); const isValid = validateUserInfo(formValues); setState({ showEditForm: !isValid }); } else { setUserFormValues({ name: "", email: "", phone: "", address: { state: "", line1: "", line2: "", city: "", postal_code: "" } }, {}, setValue, { preferExisting: false, showPhone: checkoutSession.phone_number_collection?.enabled }); } }; if (state.submitting) { return; } initUserInfo(); }, [session?.user, checkoutSession.phone_number_collection?.enabled]); const paymentMethod = (0, _reactHookForm.useWatch)({ control, name: "payment_method" }); const paymentCurrencyId = (0, _reactHookForm.useWatch)({ control, name: "payment_currency" }); const afterUserLoggedIn = (0, _ahooks.useMemoizedFn)(() => { handleSubmit(onFormSubmit, onFormError)(); }); const payee = (0, _util2.getStatementDescriptor)(checkoutSession.line_items); let buttonText = ""; if (paymentLink?.donation_settings) { if (action) { buttonText = action; } else { buttonText = t("payment.checkout.donate"); } } else { buttonText = t(`payment.checkout.${checkoutSession.mode}`); } buttonText = session?.user || isDonation ? buttonText : t("payment.checkout.connect", { action: buttonText }); const method = paymentMethods.find(x => x.id === paymentMethod); const paymentCurrency = currencies.find(x => x.id === paymentCurrencyId); const showStake = method.type === "arcblock" && !checkoutSession.subscription_data?.no_stake; const hasDynamicPricing = (0, _react.useMemo)(() => (checkoutSession.line_items || []).some(item => { const price = item.upsell_price || item.price; return price && price?.pricing_type === "dynamic"; }), [checkoutSession.line_items]); const rateUnavailableForDynamic = hasDynamicPricing && rateUnavailable; const canPay = payable && !rateUnavailableForDynamic; const isDonationMode = checkoutSession?.submit_type === "donate" && isDonation; const [priceUpdateInfo, setPriceUpdateInfo] = (0, _ahooks.useSetState)({ open: false, total: "", usd: null, hasQuotes: false, baseCurrency: "USD", oldTotal: "", reason: "recalculated" }); const normalizeExchangeRate = (0, _ahooks.useMemoizedFn)(rate => { if (!rate) { return null; } const value = Number(rate); if (!Number.isFinite(value)) { return null; } return value.toFixed(8); }); const getExchangeRateFromSession = (0, _ahooks.useMemoizedFn)(sessionData => { if (!sessionData?.line_items?.length) { return null; } for (const item of sessionData.line_items) { const rate = item?.exchange_rate; if (rate) { return rate; } } return null; }); const quoteAutoRetryRef = (0, _react.useRef)(false); const lastRetryKeyRef = (0, _react.useRef)(""); const buildRetryKey = (0, _ahooks.useMemoizedFn)(sessionData => { if (!sessionData?.line_items?.length) { return ""; } return sessionData.line_items.map(item => { const priceId = item.price_id || item.price?.id || ""; const quoteId = item?.quote_id || ""; const quotedAmount = item?.quoted_amount || ""; const exchangeRate = item?.exchange_rate || ""; return `${priceId}:${quoteId}:${quotedAmount}:${exchangeRate}`; }).join("|"); }); const buildPriceUpdateSummary = (0, _ahooks.useMemoizedFn)(sessionData => { if (!paymentCurrency) { return { total: "", usd: null, hasQuotes: false, baseCurrency: "USD", totalUnit: null }; } const lineItems = sessionData.line_items || []; let baseCurrency = "USD"; for (const item of lineItems) { const price = item.upsell_price || item.price; const base = price?.base_currency; if (base) { baseCurrency = base; break; } } const hasQuotes = lineItems.some(item => item?.quoted_amount && item?.exchange_rate); if (!hasQuotes) { return { total: "", usd: null, hasQuotes: false, baseCurrency, totalUnit: null }; } let trialInDays = Number(sessionData?.subscription_data?.trial_period_days || 0); const trialCurrencyIds = (sessionData?.subscription_data?.trial_currency || "").split(",").map(_trim.default).filter(Boolean); if (trialCurrencyIds.length > 0 && paymentCurrencyId && trialCurrencyIds.includes(paymentCurrencyId) === false) { trialInDays = 0; } const { total } = (0, _util2.getCheckoutAmount)(lineItems, paymentCurrency, trialInDays > 0); const discountAmount = new _util.BN(sessionData.total_details?.amount_discount || "0"); const totalUnit = new _util.BN(total).sub(discountAmount); const normalizedTotalUnit = totalUnit.isNeg() ? new _util.BN(0) : totalUnit; const totalDisplay = `${(0, _util2.formatNumber)((0, _util.fromUnitToToken)(normalizedTotalUnit.toString(), paymentCurrency.decimal), 6)} ${paymentCurrency.symbol}`; const itemUsdReferences = lineItems.map(item => { const price = item.upsell_price || item.price; const baseAmount = price?.base_amount; const hasBaseAmount = baseAmount !== void 0 && baseAmount !== null; if (hasBaseAmount) { return (0, _util2.getUsdAmountFromBaseAmount)(baseAmount, item.quantity || 0); } const exchangeRate = item?.exchange_rate; const quotedAmount = item?.quoted_amount; if (!exchangeRate || !quotedAmount) { return null; } return (0, _util2.getUsdAmountFromTokenUnits)(new _util.BN(quotedAmount), paymentCurrency.decimal, exchangeRate); }); const usdValues = itemUsdReferences.filter(value => Boolean(value)); if (!usdValues.length) { return { total: totalDisplay, usd: null, hasQuotes, baseCurrency, totalUnit: normalizedTotalUnit }; } const sumUnit = usdValues.reduce((acc, value) => acc.add(new _util.BN((0, _util.fromTokenToUnit)(value, 8))), new _util.BN(0)); const totalUsdReference = (0, _util.fromUnitToToken)(sumUnit.toString(), 8); return { total: totalDisplay, usd: (0, _util2.formatUsdAmount)(totalUsdReference, locale), hasQuotes, baseCurrency, totalUnit: normalizedTotalUnit }; }); const compareTotals = (0, _ahooks.useMemoizedFn)((prevSession, nextSession) => { const prev = buildPriceUpdateSummary(prevSession); const next = buildPriceUpdateSummary(nextSession); if (!prev.totalUnit || !next.totalUnit) { return { changed: false, prev, next }; } const diff = next.totalUnit.sub(prev.totalUnit).abs(); const epsilon = new _util.BN(1); return { changed: diff.gt(epsilon), prev, next }; }); const applyQuoteUpdate = (0, _ahooks.useMemoizedFn)((payload, options = {}) => { if (!payload?.checkoutSession) { return; } onQuoteUpdated?.({ checkoutSession: payload.checkoutSession, quotes: payload.quotes, rateUnavailable: payload.rateUnavailable, rateError: payload.rateError }); const { changed, prev, next } = compareTotals(checkoutSession, payload.checkoutSession); const previousRate = normalizeExchangeRate(getExchangeRateFromSession(checkoutSession)); const nextRate = normalizeExchangeRate(getExchangeRateFromSession(payload.checkoutSession)); const rateChanged = !!(previousRate && nextRate && previousRate !== nextRate); const shouldShowModal = (options.forceConfirm || changed) && next.hasQuotes; if (shouldShowModal) { setPriceUpdateInfo({ open: true, total: next.total, usd: next.usd, hasQuotes: next.hasQuotes, baseCurrency: next.baseCurrency, oldTotal: prev.total, reason: options.reason || (rateChanged ? "rateChanged" : "recalculated") }); return; } setPriceUpdateInfo({ open: false }); const retryKey = buildRetryKey(payload.checkoutSession); if (retryKey && retryKey !== lastRetryKeyRef.current) { lastRetryKeyRef.current = retryKey; quoteAutoRetryRef.current = true; } }); const validateUserInfo = values => { if (!values) { return false; } const fieldValidation = checkoutSession.metadata?.page_info?.field_validation; const customerName2 = values.customer_name; if (!customerName2 || customerName2.trim() === "") { return false; } const customerEmail2 = values.customer_email; if (!customerEmail2 || !(0, _isEmail.default)(customerEmail2)) { return false; } const nameValidation = (0, _validator.getFieldValidation)("customer_name", fieldValidation, locale); if (nameValidation.pattern) { const pattern = nameValidation.pattern.value; if (!pattern.test(customerName2)) { return false; } } const emailValidation = (0, _validator.getFieldValidation)("customer_email", fieldValidation, locale); if (emailValidation.pattern) { const pattern = emailValidation.pattern.value; if (!pattern.test(customerEmail2)) { return false; } } const billingAddress2 = values.billing_address || {}; const { postal_code: postalCode, country, state: stateValue, line1, city } = billingAddress2; if (!postalCode || !(0, _validator.validatePostalCode)(postalCode, country)) { return false; } const postalCodeValidation = (0, _validator.getFieldValidation)("billing_address.postal_code", fieldValidation, locale); if (postalCodeValidation.pattern) { const pattern = postalCodeValidation.pattern.value; if (!pattern.test(postalCode)) { return false; } } if (!stateValue) { return false; } const stateValidation = (0, _validator.getFieldValidation)("billing_address.state", fieldValidation, locale); if (stateValidation.pattern) { const pattern = stateValidation.pattern.value; if (!pattern.test(stateValue)) { return false; } } if (checkoutSession.phone_number_collection?.enabled) { const customerPhone2 = values.customer_phone; if (!customerPhone2 || customerPhone2.trim() === "") { return false; } const phoneValidation = (0, _validator.getFieldValidation)("customer_phone", fieldValidation, locale); if (phoneValidation.pattern) { const pattern = phoneValidation.pattern.value; if (!pattern.test(customerPhone2)) { return false; } } } const addressMode = checkoutSession.billing_address_collection; if (addressMode === "required") { if (!country || !stateValue || !line1 || !city || !postalCode) { return false; } const line1Validation = (0, _validator.getFieldValidation)("billing_address.line1", fieldValidation, locale); if (line1Validation.pattern) { const pattern = line1Validation.pattern.value; if (!pattern.test(line1)) { return false; } } const cityValidation = (0, _validator.getFieldValidation)("billing_address.city", fieldValidation, locale); if (cityValidation.pattern) { const pattern = cityValidation.pattern.value; if (!pattern.test(city)) { return false; } } } return true; }; const customerName = (0, _reactHookForm.useWatch)({ control, name: "customer_name" }); const customerEmail = (0, _reactHookForm.useWatch)({ control, name: "customer_email" }); const customerPhone = (0, _reactHookForm.useWatch)({ control, name: "customer_phone" }); const billingAddress = (0, _reactHookForm.useWatch)({ control, name: "billing_address" }); const showForm = session?.user ? state.showEditForm : false; (0, _react.useEffect)(() => { if (!quoteAutoRetryRef.current) { return; } if (state.submitting || state.paying) { return; } quoteAutoRetryRef.current = false; onAction(); }, [state.submitting, state.paying]); const handleConnected = async () => { if (processingRef.current) { return; } processingRef.current = true; setState({ paying: true }); try { const result = await waitForCheckoutComplete(checkoutSession.id); if (state.paid === false) { setState({ paid: true, paying: false }); onPaid(result); } } catch (err) { const errorMessage = (0, _util2.formatError)(err); const payFailedEvent = { action: "payFailed", // @ts-ignore 后续升级的话就会报错了,移除这个 lint 即可 mode: checkoutSession.mode, errorMessage, success: false }; _withTracker.ReactGA.event(payFailedEvent.action, payFailedEvent); _Toast.default.error(errorMessage); } finally { setState({ paying: false }); processingRef.current = false; } }; (0, _react.useEffect)(() => { if (errorRef.current && !(0, _isEmpty.default)(errors) && isMobile) { errorRef.current.scrollIntoView({ behavior: "smooth" }); } }, [errors, isMobile]); const onUserLoggedIn = async () => { const { data: profile } = await _api.default.get("/api/customers/me?fallback=1&skipSummary=1"); if (profile) { const values = getValues(); const userInfo = mergeUserInfo(profile, session?.user); const formValues = setUserFormValues(userInfo, values, setValue, { preferExisting: false, showPhone: checkoutSession.phone_number_collection?.enabled, shouldValidate: true }); const isValid = validateUserInfo(formValues); setState({ showEditForm: !isValid }); await trigger(); } }; const handleFastCheckoutConfirm = async () => { if (!state.fastCheckoutInfo) return; setState({ fastCheckoutInfo: { ...state.fastCheckoutInfo, loading: true } }); try { const result = await _api.default.post(`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`, {}); if (result.data.paymentIntent) { onPaymentIntentUpdate?.(result.data.paymentIntent); } if (result.data.fastPaid) { setState({ fastCheckoutInfo: null, paying: true }); await handleConnected(); } else { _Toast.default.error(t("payment.checkout.fastPay.failed")); setState({ fastCheckoutInfo: null, paying: true }); openConnect(); } } catch (err) { console.error(err); const errorCode = err?.response?.data?.code; if (["QUOTE_LOCK_EXPIRED", "QUOTE_AMOUNT_MISMATCH", "QUOTE_EXPIRED_OR_USED", "QUOTE_NOT_FOUND", "QUOTE_REQUIRED"].includes(errorCode)) { try { const { data: refreshed } = await _api.default.get(`/api/checkout-sessions/retrieve/${checkoutSession.id}`, { params: { forceRefresh: "1" } }); if (refreshed?.checkoutSession) { applyQuoteUpdate(refreshed, { reason: "rateChanged" }); _Toast.default.info(t("payment.checkout.quote.updated.pleaseRetry") || "Price updated, please resubmit"); } } catch (refreshError) { console.error(refreshError); _Toast.default.error((0, _util2.formatError)(refreshError)); } finally { setState({ fastCheckoutInfo: null }); } return; } if (errorCode === "QUOTE_UPDATED") { const payload = err?.response?.data; if (payload?.checkoutSession) { applyQuoteUpdate(payload); } setState({ fastCheckoutInfo: null }); return; } if (errorCode === "RATE_UNAVAILABLE") { const payload = err?.response?.data; if (payload?.checkoutSession) { onQuoteUpdated?.({ checkoutSession: payload.checkoutSession, quotes: payload.quotes, rateUnavailable: payload.rateUnavailable, rateError: payload.rateError }); } setState({ fastCheckoutInfo: null }); return; } _Toast.default.error((0, _util2.formatError)(err)); setState({ fastCheckoutInfo: null }); } }; const handleFastCheckoutCancel = () => { setState({ fastCheckoutInfo: null }); }; const handleCreditInsufficientClose = () => { setState({ creditInsufficientInfo: null }); }; const handlePriceUpdateConfirm = () => { setPriceUpdateInfo({ open: false }); quoteAutoRetryRef.current = true; }; const handlePriceUpdateCancel = () => { setPriceUpdateInfo({ open: false }); }; const handlePriceChangeConfirm = () => { const formData = state.priceChangeConfirm?.formData; setState({ priceChangeConfirm: null }); if (formData) { onFormSubmit(formData); } }; const handlePriceChangeCancel = () => { setState({ priceChangeConfirm: null }); }; const openConnect = () => { try { if (!["arcblock", "ethereum", "base"].includes(method.type)) { return; } setState({ paying: true }); connect.open({ locale, containerEl: void 0, action: checkoutSession.mode, prefix: (0, _ufo.joinURL)((0, _util2.getPrefix)(), "/api/did"), saveConnect: false, useSocket: (0, _util2.isCrossOrigin)() === false, extraParams: { checkoutSessionId: checkoutSession.id, sessionUserDid: session?.user?.did }, onSuccess: async () => { connect.close(); await handleConnected(); }, onClose: () => { connect.close(); setState({ submitting: false, paying: false }); }, onError: err => { console.error(err); setState({ submitting: false, paying: false }); onError(err); }, messages: { title: t("payment.checkout.connectModal.title", { action: buttonText }), scan: t("payment.checkout.connectModal.scan"), confirm: t("payment.checkout.connectModal.confirm"), cancel: t("payment.checkout.connectModal.cancel") } }); } catch (err) { _Toast.default.error((0, _util2.formatError)(err)); } }; const onFormSubmit = async data => { if (state.submitting) { return; } const userInfo = session.user; if (!userInfo.sourceAppPid) { const hasVendorConfig = checkoutSession.line_items?.some(item => !!item?.price?.product?.vendor_config?.length); if (hasVendorConfig) { _Toast.default.error(t("payment.checkout.vendor.accountRequired")); return; } } setState({ submitting: true }); try { let result; const previewRate = checkoutSession.line_items?.find(item => item?.exchange_rate)?.exchange_rate || void 0; const items = checkoutSession.line_items || []; const itemsSig = items.map(i => `${i.upsell_price_id || i.price_id}:${i.quantity}`).join("|"); const fingerprint = `${checkoutSession.id}-${paymentCurrency?.id}-${itemsSig}`; if (fingerprint !== sessionFingerprintRef.current || !idempotencyKeyRef.current) { sessionFingerprintRef.current = fingerprint; idempotencyKeyRef.current = generateIdempotencyKey(checkoutSession.id, paymentCurrency?.id || ""); } const payload = { ...data, // Final Freeze: Include these for new quote creation at submit idempotency_key: idempotencyKeyRef.current, preview_rate: previewRate || void 0, price_confirmed: state.priceChangeConfirm?.formData ? true : void 0 }; if (isDonationMode) { result = await _api.default.put(`/api/checkout-sessions/${checkoutSession.id}/donate-submit`, payload); } else { result = await _api.default.put(`/api/checkout-sessions/${checkoutSession.id}/submit`, payload); } setState({ paymentIntent: result.data.paymentIntent, stripeContext: result.data.stripeContext, customer: result.data.customer, submitting: false, customerLimited: false }); if (result.data.paymentIntent) { onPaymentIntentUpdate?.(result.data.paymentIntent); } if (["arcblock", "ethereum", "base"].includes(method.type)) { if (result.data.noPaymentRequired) { try { const confirmResult = await _api.default.post(`/api/checkout-sessions/${checkoutSession.id}/fast-checkout-confirm`, {}); if (confirmResult.data.paymentIntent) { onPaymentIntentUpdate?.(confirmResult.data.paymentIntent); } if (confirmResult.data.fastPaid || confirmResult.data.checkoutSession?.status === "complete") { setState({ paying: true }); await handleConnected(); } else { openConnect(); } } catch (confirmErr) { console.error("noPaymentRequired confirm failed", confirmErr); openConnect(); } return; } if (paymentCurrency?.type === "credit") { if (result.data.creditSufficient === true) { setState({ fastCheckoutInfo: { open: true, loading: false, sourceType: "credit", amount: result.data.fastPayInfo?.amount || "0", payer: result.data.fastPayInfo?.payer, availableCredit: result.data.fastPayInfo?.amount || "0", balance: result.data.fastPayInfo?.token?.balance || "0" } }); } else { setState({ creditInsufficientInfo: { open: true } }); } } else if ((result.data.balance?.sufficient || result.data.delegation?.sufficient) && !isDonationMode && result.data.fastPayInfo) { setState({ fastCheckoutInfo: { open: true, loading: false, sourceType: result.data.fastPayInfo.type, amount: result.data.fastPayInfo.amount, payer: result.data.fastPayInfo.payer } }); } else { openConnect(); } } if (["stripe"].includes(method.type)) { if (result.data.stripeContext?.status === "succeeded") { setState({ paying: true }); } else { setTimeout(() => { setState({ stripePaying: true }); }, 200); } } } catch (err) { console.error(err); let shouldToast = true; const errorCode = err.response?.data?.code; if (errorCode) { if (!["QUOTE_UPDATED", "RATE_UNAVAILABLE", "QUOTE_LOCK_EXPIRED", "QUOTE_AMOUNT_MISMATCH", "QUOTE_EXPIRED_OR_USED", "QUOTE_NOT_FOUND", "QUOTE_REQUIRED", "QUOTE_MAX_PAYABLE_EXCEEDED"].includes(errorCode)) { (0, _useBus.dispatch)(`error.${errorCode}`); } if (["QUOTE_LOCK_EXPIRED", "QUOTE_AMOUNT_MISMATCH", "QUOTE_EXPIRED_OR_USED", "QUOTE_NOT_FOUND", "QUOTE_REQUIRED", "QUOTE_MAX_PAYABLE_EXCEEDED", "quote_validation_failed"].includes(errorCode)) { shouldToast = false; try { const { data: refreshed } = await _api.default.get(`/api/checkout-sessions/retrieve/${checkoutSession.id}`, { params: { forceRefresh: "1" } }); if (refreshed?.checkoutSession) { applyQuoteUpdate(refreshed, { reason: "rateChanged" }); _Toast.default.info(t("payment.checkout.quote.updated.pleaseRetry") || "Price updated, please resubmit"); } } catch (refreshError) { console.error(refreshError); _Toast.default.error((0, _util2.formatError)(refreshError)); } } if (errorCode === "QUOTE_UPDATED") { shouldToast = false; const payload = err.response?.data; if (payload?.checkoutSession) { applyQuoteUpdate(payload); } } if (errorCode === "RATE_UNAVAILABLE") { shouldToast = false; const payload = err.response?.data; if (payload?.checkoutSession) { onQuoteUpdated?.({ checkoutSession: payload.checkoutSession, quotes: payload.quotes, rateUnavailable: payload.rateUnavailable, rateError: payload.rateError }); } } if (errorCode === "PRICE_UNAVAILABLE") { shouldToast = false; _Toast.default.error(t("payment.checkout.priceChange.unavailable", { fallback: "Unable to fetch exchange rate. Please try again later." })); } if (errorCode === "PRICE_UNSTABLE") { shouldToast = false; _Toast.default.error(t("payment.checkout.priceChange.unstable", { fallback: "Price is volatile. Please try again later." })); } if (errorCode === "PRICE_CHANGED") { shouldToast = false; const errorData = err.response?.data; setState({ priceChangeConfirm: { open: true, changePercent: errorData?.change_percent || 0, formData: data // Save form data for retry } }); } if (errorCode === "UNIFIED_APP_REQUIRED") { shouldToast = false; _Toast.default.error(t("payment.checkout.vendor.accountRequired")); } if (errorCode === "CUSTOMER_LIMITED") { shouldToast = false; setState({ customerLimited: true }); } if (errorCode === "STOP_ACCEPTING_ORDERS") { shouldToast = false; setState({ serviceSuspended: true }); } } if (shouldToast) { _Toast.default.error((0, _util2.formatError)(err)); } } finally { setState({ submitting: false }); } }; const onFormError = err => { if (err) { console.error(err); } setState({ submitting: false }); }; const onAction = () => { if (state.submitting || state.paying || !canPay) { return; } if (errorRef.current && !(0, _isEmpty.default)(errors) && isMobile) { errorRef.current.scrollIntoView({ behavior: "smooth" }); } if (session?.user) { handleSubmit(onFormSubmit, onFormError)(); } else { if (isDonationMode) { handleSubmit(onFormSubmit, onFormError)(); return; } session?.login(() => { setState({ submitting: true }); onUserLoggedIn().then(afterUserLoggedIn).catch(err => { console.error(err); setState({ submitting: false }); }); }); } }; const onStripeConfirm = async () => { setState({ stripePaying: false, paying: true }); await handleConnected(); }; const onStripeCancel = async () => { try { await _api.default.post(`/api/checkout-sessions/${checkoutSession.id}/abort-stripe`); } catch (err) { console.error(err); _Toast.default.error((0, _util2.formatError)(err)); } finally { setState({ stripePaying: false }); } }; (0, _react.useEffect)(() => { const handleKeyDown = e => { if (e.key === "Enter" && !state.submitting && !state.paying && !state.stripePaying && quantityInventoryStatus && canPay) { onAction(); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [state.submitting, state.paying, state.stripePaying, quantityInventoryStatus, canPay]); const balanceLink = (0, _util2.getTokenBalanceLink)(method, state.fastCheckoutInfo?.payer || ""); const FastCheckoutConfirmDialog = state.fastCheckoutInfo && /* @__PURE__ */(0, _jsxRuntime.jsx)(_confirm.default, { onConfirm: handleFastCheckoutConfirm, onCancel: handleFastCheckoutCancel, title: state.fastCheckoutInfo.sourceType === "credit" ? t("payment.checkout.fastPay.credit.title") : t("payment.checkout.fastPay.title"), message: state.fastCheckoutInfo.sourceType === "credit" ? /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { children: t("payment.checkout.fastPay.credit.meteringSubscriptionMessage", { available: `${(0, _util2.formatAmount)(state.fastCheckoutInfo?.balance || "0", paymentCurrency?.decimal || 18)} ${paymentCurrency?.symbol}` }) }) : /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { children: t("payment.checkout.fastPay.autoPaymentReason") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Divider, { sx: { mt: 1.5, mb: 1.5 } }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { spacing: 1, children: [/* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { color: "text.primary", whiteSpace: "nowrap" }, children: t("payment.checkout.fastPay.payer") }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Box, { sx: { display: "flex", alignItems: "center", gap: 0.5 }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_DID.default, { did: state.fastCheckoutInfo.payer || "", compact: true, responsive: false }), balanceLink && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Tooltip, { title: t("payment.checkout.fastPay.balanceLink"), placement: "top", children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_iconsMaterial.OpenInNew, { sx: { color: "text.lighter", fontSize: "0.85rem", cursor: "pointer", "&:hover": { color: "text.primary" } }, onClick: () => { window.open(balanceLink, "_blank"); } }) })] })] }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { sx: { flexDirection: "row", alignItems: "center", justifyContent: "space-between" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { color: "text.primary" }, children: t("payment.checkout.fastPay.amount") }), /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { children: [(0, _util2.formatAmount)(state.fastCheckoutInfo.amount, paymentCurrency?.decimal || 18), " ", paymentCurrency?.symbol] })] })] })] }), loading: state.fastCheckoutInfo.loading, color: "primary" }); const CreditInsufficientDialog = state.creditInsufficientInfo && /* @__PURE__ */(0, _jsxRuntime.jsx)(_confirm.default, { onConfirm: handleCreditInsufficientClose, onCancel: handleCreditInsufficientClose, title: t("payment.checkout.fastPay.credit.insufficientTitle"), message: /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { children: t("payment.checkout.fastPay.credit.insufficientMessage") }), confirm: t("common.confirm") }); const PriceUpdatedDialog = priceUpdateInfo.open && /* @__PURE__ */(0, _jsxRuntime.jsx)(_confirm.default, { onConfirm: handlePriceUpdateConfirm, onCancel: handlePriceUpdateCancel, title: t("payment.checkout.quote.priceUpdatedTitle"), message: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { spacing: 1, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { children: t(priceUpdateInfo.reason === "rateChanged" ? "payment.checkout.quote.priceUpdatedDescriptionRate" : "payment.checkout.quote.priceUpdatedDescriptionRecalc") }), priceUpdateInfo.hasQuotes && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { spacing: 0.25, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { color: "text.secondary", fontSize: "0.7875rem" }, children: t("payment.checkout.quote.priceUpdatedNewTotalLabel") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { fontWeight: 600 }, children: priceUpdateInfo.total }), priceUpdateInfo.usd && /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Typography, { sx: { color: "text.secondary" }, children: ["\u2248 ", priceUpdateInfo.usd, " ", priceUpdateInfo.baseCurrency] }), priceUpdateInfo.oldTotal && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { sx: { color: "text.secondary", fontSize: "0.75rem" }, children: t("payment.checkout.quote.priceUpdatedOldTotal", { total: priceUpdateInfo.oldTotal }) })] })] }), confirm: t("payment.checkout.quote.priceUpdatedConfirm"), cancel: t("common.cancel"), color: "primary" }); const getRedirectUrl = () => { if (searchParams.redirect) { return decodeURIComponent(searchParams.redirect); } if (checkoutSession.success_url) { return checkoutSession.success_url; } if (paymentLink?.after_completion?.redirect?.url) { return paymentLink.after_completion.redirect.url; } return void 0; }; if (onlyShowBtn) { return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Box, { className: "cko-payment-submit-btn", children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Button, { variant: "contained", color: "primary", size: "large", className: "cko-submit-button", onClick: () => { if (state.submitting || state.paying || !canPay) { return; } onAction(); }, fullWidth: true, disabled: state.stripePaying || !quantityInventoryStatus || !canPay, children: [(state.submitting || state.paying) && /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.CircularProgress, { size: 16, sx: { mr: 0.5, color: "primary.contrastText" } }), state.submitting || state.paying ? t("payment.checkout.processing") : buttonText] }) }), state.customerLimited && /* @__PURE__ */(0, _jsxRuntime.jsx)(_overDueInvoicePayment.default, { customerId: customer?.id || session?.user?.did, onPaid: () => { setState({ customerLimited: false }); onAction(); }, alertMessage: t("payment.customer.pastDue.alert.customMessage"), detailLinkOptions: { enabled: true, onClick: () => { setState({ customerLimited: false }); window.open((0, _ufo.joinURL)((0, _util2.getPrefix)(), `/customer/invoice/past-due?referer=${encodeURIComponent(window.location.href)}`), "_self"); } }, dialogProps: { open: state.customerLimited, onClose: () => setState({ customerLimited: false }), title: t("payment.customer.pastDue.alert.title") } }), state.serviceSuspended && /* @__PURE__ */(0, _jsxRuntime.jsx)(_serviceSuspendedDialog.default, { open: true, onClose: () => setState({ serviceSuspended: false }) }), FastCheckoutConfirmDialog, CreditInsufficientDialog, PriceUpdatedDialog, state.priceChangeConfirm?.open && /* @__PURE__ */(0, _jsxRuntime.jsx)(_priceChangeConfirm.default, { open: true, changePercent: state.priceChangeConfirm.changePercent, onConfirm: handlePriceChangeConfirm, onCancel: handlePriceChangeCancel, loading: state.submitting })] }); } return /* @__PURE__ */(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, { children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Fade, { in: true, children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { className: "cko-payment-contact", children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Typography, { title: t("payment.checkout.paymentDetails"), sx: { color: "text.primary", fontSize: { xs: "18px", md: "24px" }, fontWeight: "700", lineHeight: "32px", mb: 2.5 }, children: t("payment.checkout.paymentDetails") }), /* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Fade, { in: true, children: /* @__PURE__ */(0, _jsxRuntime.jsxs)(_material.Stack, { direction: "column", className: "cko-payment-methods", sx: { alignItems: "flex-start" }, children: [/* @__PURE__ */(0, _jsxRuntime.jsx)(_material.Stack, { direction: "row", sx: { width: "100%" }, children: /* @__PURE__ */(0, _jsxRuntime.jsx)(_reactHookForm.Controller, { name: "payment_currency", control, render: ({ field }) => /* @__PURE__ */(0, _jsxRuntime.jsx)(_currency.default, { value: field.value, currencies, onChange: async (id, methodId) => { const oldCurrencyId = field.value; field.onChange(id); setValue("payment_method", methodId); (0, _currency2.saveCurrencyPreference)(id, session?.user?.did); if (oldCurrencyId && oldCurrencyId !== id) { try { const { data } = await _api.default.put(`/api/checkout-sessions/${checkoutSession.id}/switch-currency`, { currency_id: id, payment_method_id: methodId }); if (data.currency_changed && onQuoteUpdated) { onQuoteUpdated({ checkoutSession: data, quotes: data.quotes }); } } catch (err) { console.error("Failed to switch currency:", err); } } } }) }) }), state.stripePaying && state.stripeContext && /* @__PURE__ */(0, _jsxRuntime.jsx)(_stripe.default, { clientSecret: state.stripeContext.client_secret, intentType: state.stripeContext.intent_type, publicKey: method.s