@funnelsgrove/payments 0.30.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/generated/package-version.d.ts +1 -1
- package/dist/generated/package-version.js +1 -1
- package/dist/providers/solidgate/components/SolidgateCheckoutDialog.js +15 -9
- package/dist/providers/solidgate/components/SolidgateCheckoutDialog.styles.d.ts +1 -1
- package/dist/providers/solidgate/components/SolidgateCheckoutDialog.styles.js +3 -0
- package/dist/providers/solidgate/components/SolidgateCheckoutDialog.types.d.ts +3 -1
- package/dist/providers/stripe/components/StripeSubscriptionWalletFirstSurface.d.ts +15 -0
- package/dist/providers/stripe/components/StripeSubscriptionWalletFirstSurface.js +50 -0
- package/dist/providers/stripe/components/StripeSubscriptionWalletSurface.d.ts +3 -1
- package/dist/providers/stripe/components/StripeSubscriptionWalletSurface.js +6 -1
- package/dist/providers/stripe/hooks/checkoutPreparationBackoff.d.ts +14 -0
- package/dist/providers/stripe/hooks/checkoutPreparationBackoff.js +17 -0
- package/dist/providers/stripe/hooks/useStripeOneTimeCheckoutSession.js +25 -3
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.d.ts +1 -0
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.js +38 -7
- package/dist/providers/stripe/hooks/useStripeSubscriptionWalletFirstRequest.d.ts +26 -0
- package/dist/providers/stripe/hooks/useStripeSubscriptionWalletFirstRequest.js +229 -0
- package/dist/providers/stripe/services/stripeCheckoutFailure.d.ts +1 -0
- package/dist/providers/stripe/services/stripeCheckoutFailure.js +16 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -76,6 +76,19 @@ and saves the email through the existing caller-owned identity update. Once the
|
|
|
76
76
|
caller adopts the saved email, wallet controls can mount. Provider email rejections
|
|
77
77
|
still open the surface's repair dialog; saving never confirms the old session.
|
|
78
78
|
|
|
79
|
+
For a wallet-first subscription flow, set `walletFirst` on each
|
|
80
|
+
`StripeSubscriptionWalletSurface` with the device payment method and an
|
|
81
|
+
`onPayerEmail` callback that saves the wallet email on the current Funnel User.
|
|
82
|
+
The wallet sheet opens before a Checkout Session exists; the shared surface
|
|
83
|
+
prepares and confirms the session after the email is saved. Render one surface
|
|
84
|
+
per visible placement and a separate card session for fallback. A Stripe
|
|
85
|
+
publishable key must be available before the wallet opens; otherwise the surface
|
|
86
|
+
reports the wallet unavailable so the caller can show card checkout.
|
|
87
|
+
Set `walletFirst.buttonLabel` for a single funnel-styled trial CTA; without it,
|
|
88
|
+
wallet-first mode renders the existing branded Apple Pay or Google Pay button.
|
|
89
|
+
The regular surface without `walletFirst` keeps its existing separate wallet
|
|
90
|
+
choices and card entry.
|
|
91
|
+
|
|
79
92
|
`CheckoutEmailDialog` defaults to a light surface regardless of the browser or
|
|
80
93
|
ancestor theme. It inherits the funnel font. Set inherited `--checkout-email-*`
|
|
81
94
|
CSS variables on the funnel root or `.checkout-email-dialog` to customize it:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PAYMENTS_PACKAGE_VERSION = "0.
|
|
1
|
+
export declare const PAYMENTS_PACKAGE_VERSION = "0.31.0";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/generate-package-version.mjs.
|
|
2
|
-
export const PAYMENTS_PACKAGE_VERSION = "0.
|
|
2
|
+
export const PAYMENTS_PACKAGE_VERSION = "0.31.0";
|
|
@@ -19,6 +19,8 @@ const defaultContent = {
|
|
|
19
19
|
error: 'Payment could not be completed. Please try again.',
|
|
20
20
|
expired: 'This checkout has expired. Start a new payment to continue.',
|
|
21
21
|
retry: 'Try again',
|
|
22
|
+
existingSubscriptionTitle: 'Subscription already active',
|
|
23
|
+
existingSubscriptionContinue: 'Continue',
|
|
22
24
|
resumeVerification: 'Check payment status',
|
|
23
25
|
customerEmailLabel: 'Email address',
|
|
24
26
|
customerEmailPlaceholder: 'you@example.com',
|
|
@@ -227,7 +229,10 @@ export function SolidgateCheckoutDialog({ open, layout = 'dialog', request, cust
|
|
|
227
229
|
if (!controller.signal.aborted && !isAbortError(error)) {
|
|
228
230
|
const failureMessage = error instanceof SolidgateExistingSubscriptionError
|
|
229
231
|
? error.message : copyRef.current.error;
|
|
230
|
-
setState({
|
|
232
|
+
setState({
|
|
233
|
+
state: error instanceof SolidgateExistingSubscriptionError ? 'existing_subscription' : 'recoverable_error',
|
|
234
|
+
checkout: null, canResumeVerification: false, failureMessage,
|
|
235
|
+
});
|
|
231
236
|
trackFailure(failureMessage, 'checkout_session_creation');
|
|
232
237
|
(_a = onErrorRef.current) === null || _a === void 0 ? void 0 : _a.call(onErrorRef, failureMessage, error instanceof SolidgateExistingSubscriptionError ? error.code : undefined);
|
|
233
238
|
}
|
|
@@ -237,9 +242,10 @@ export function SolidgateCheckoutDialog({ open, layout = 'dialog', request, cust
|
|
|
237
242
|
const paymentFormMounted = Boolean(view.checkout && mountedPaymentAttemptId === view.checkout.attemptId);
|
|
238
243
|
const showPreparingLoader = view.state === 'loading'
|
|
239
244
|
|| (view.state === 'ready' && !paymentFormMounted);
|
|
245
|
+
const hasExistingSubscription = view.state === 'existing_subscription';
|
|
240
246
|
useEffect(() => {
|
|
241
247
|
var _a;
|
|
242
|
-
if (!open || layout === 'wallet') {
|
|
248
|
+
if (!open || (layout === 'wallet' && !hasExistingSubscription)) {
|
|
243
249
|
return;
|
|
244
250
|
}
|
|
245
251
|
const previouslyFocused = document.activeElement instanceof HTMLElement
|
|
@@ -269,7 +275,7 @@ export function SolidgateCheckoutDialog({ open, layout = 'dialog', request, cust
|
|
|
269
275
|
window.removeEventListener('keydown', handleKeyDown);
|
|
270
276
|
previouslyFocused === null || previouslyFocused === void 0 ? void 0 : previouslyFocused.focus();
|
|
271
277
|
};
|
|
272
|
-
}, [layout, open, showPreparingLoader, view.checkout]);
|
|
278
|
+
}, [hasExistingSubscription, layout, open, showPreparingLoader, view.checkout]);
|
|
273
279
|
useEffect(() => () => {
|
|
274
280
|
var _a, _b;
|
|
275
281
|
(_a = prepareAbortRef.current) === null || _a === void 0 ? void 0 : _a.abort();
|
|
@@ -389,7 +395,7 @@ export function SolidgateCheckoutDialog({ open, layout = 'dialog', request, cust
|
|
|
389
395
|
? copy.success
|
|
390
396
|
: view.state === 'expired'
|
|
391
397
|
? copy.expired
|
|
392
|
-
: view.state === 'recoverable_error'
|
|
398
|
+
: view.state === 'recoverable_error' || hasExistingSubscription
|
|
393
399
|
? view.failureMessage || copy.error
|
|
394
400
|
: null;
|
|
395
401
|
const presentationStyle = presentation
|
|
@@ -397,7 +403,7 @@ export function SolidgateCheckoutDialog({ open, layout = 'dialog', request, cust
|
|
|
397
403
|
'--solidgate-checkout-theme-color': presentation.themeColor,
|
|
398
404
|
}
|
|
399
405
|
: undefined;
|
|
400
|
-
const recoveryActions = (_jsxs(_Fragment, { children: [view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => void verifyCanonicalStatus(), className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.resumeVerification })) : null, (view.state === 'recoverable_error' || view.state === 'expired') && !view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => {
|
|
406
|
+
const recoveryActions = (_jsxs(_Fragment, { children: [hasExistingSubscription ? (_jsx("a", { href: successUrl, className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.existingSubscriptionContinue })) : null, view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => void verifyCanonicalStatus(), className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.resumeVerification })) : null, (view.state === 'recoverable_error' || view.state === 'expired') && !view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => {
|
|
401
407
|
var _a;
|
|
402
408
|
const source = checkoutType === 'subscription'
|
|
403
409
|
? declinedAttemptRef.current || (retryAttemptId ? ((_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId) || retryAttemptId : null)
|
|
@@ -429,7 +435,7 @@ export function SolidgateCheckoutDialog({ open, layout = 'dialog', request, cust
|
|
|
429
435
|
trackFailure(copyRef.current.error, 'payment_form', (_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId);
|
|
430
436
|
(_b = onErrorRef.current) === null || _b === void 0 ? void 0 : _b.call(onErrorRef, copyRef.current.error);
|
|
431
437
|
} }, view.checkout.attemptId)) : null;
|
|
432
|
-
if (layout === 'wallet') {
|
|
438
|
+
if (layout === 'wallet' && !hasExistingSubscription) {
|
|
433
439
|
return (_jsxs("div", { className: 'solidgate-inline-wallet', onClick: (event) => {
|
|
434
440
|
var _a;
|
|
435
441
|
const attemptId = (_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId;
|
|
@@ -440,11 +446,11 @@ export function SolidgateCheckoutDialog({ open, layout = 'dialog', request, cust
|
|
|
440
446
|
}
|
|
441
447
|
}, children: [paymentForm, statusMessage && !showPreparingLoader ? _jsx("p", { role: 'status', children: statusMessage }) : null, recoveryActions, _jsx("style", { children: solidgateCheckoutDialogStyles })] }));
|
|
442
448
|
}
|
|
443
|
-
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: `solidgate-checkout-overlay${presentation ? ' is-branded' : ''}`, style: presentationStyle, children: [showPreparingLoader ? (_jsxs("div", { className: 'solidgate-checkout-loader', role: 'status', "aria-live": 'polite', children: [_jsx("span", { className: 'solidgate-checkout-spinner', "aria-hidden": 'true' }), _jsx("span", { className: 'solidgate-checkout-visually-hidden', children: copy.loading })] })) : null, view.state !== 'loading' || view.checkout ? (_jsxs("div", { role: 'dialog', "aria-modal": 'true', "aria-hidden": showPreparingLoader || undefined, "aria-labelledby": 'solidgate-checkout-title', "data-checkout-state": view.state, className: [
|
|
449
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: `solidgate-checkout-overlay${presentation && !hasExistingSubscription ? ' is-branded' : ''}`, style: presentationStyle, children: [showPreparingLoader ? (_jsxs("div", { className: 'solidgate-checkout-loader', role: 'status', "aria-live": 'polite', children: [_jsx("span", { className: 'solidgate-checkout-spinner', "aria-hidden": 'true' }), _jsx("span", { className: 'solidgate-checkout-visually-hidden', children: copy.loading })] })) : null, view.state !== 'loading' || view.checkout ? (_jsxs("div", { role: 'dialog', "aria-modal": 'true', "aria-hidden": showPreparingLoader || undefined, "aria-labelledby": 'solidgate-checkout-title', "data-checkout-state": view.state, className: [
|
|
444
450
|
'solidgate-checkout-dialog',
|
|
445
451
|
showPreparingLoader ? 'is-preparing' : '',
|
|
446
|
-
presentation ? 'is-branded' : '',
|
|
447
|
-
].filter(Boolean).join(' '), children: [_jsxs("div", { className: 'solidgate-checkout-header', children: [_jsx("button", { ref: closeButtonRef, type: 'button', onClick: handleClose, "aria-label": copy.closeAriaLabel, className: 'solidgate-checkout-close', children: _jsx("span", { "aria-hidden": 'true' }) }), _jsx("h2", { id: 'solidgate-checkout-title', className: 'solidgate-checkout-title', children: (presentation === null || presentation === void 0 ? void 0 : presentation.title) || copy.title })] }), presentation ? _jsx(SolidgateCheckoutSummary, { presentation: presentation }) : null, view.state === 'collecting_email' ? (_jsxs("form", { className: 'solidgate-checkout-email-form', onSubmit: (event) => void commitCustomerEmail(event), children: [_jsxs("label", { className: 'solidgate-checkout-email-label', children: [copy.customerEmailLabel, _jsx("input", { type: 'email', autoComplete: 'email', inputMode: 'email', maxLength: 320, required: true, value: emailInput, onChange: (event) => {
|
|
452
|
+
presentation && !hasExistingSubscription ? 'is-branded' : '',
|
|
453
|
+
].filter(Boolean).join(' '), children: [_jsxs("div", { className: 'solidgate-checkout-header', children: [_jsx("button", { ref: closeButtonRef, type: 'button', onClick: handleClose, "aria-label": copy.closeAriaLabel, className: 'solidgate-checkout-close', children: _jsx("span", { "aria-hidden": 'true' }) }), _jsx("h2", { id: 'solidgate-checkout-title', className: 'solidgate-checkout-title', children: hasExistingSubscription ? copy.existingSubscriptionTitle : (presentation === null || presentation === void 0 ? void 0 : presentation.title) || copy.title })] }), presentation && !hasExistingSubscription ? _jsx(SolidgateCheckoutSummary, { presentation: presentation }) : null, view.state === 'collecting_email' ? (_jsxs("form", { className: 'solidgate-checkout-email-form', onSubmit: (event) => void commitCustomerEmail(event), children: [_jsxs("label", { className: 'solidgate-checkout-email-label', children: [copy.customerEmailLabel, _jsx("input", { type: 'email', autoComplete: 'email', inputMode: 'email', maxLength: 320, required: true, value: emailInput, onChange: (event) => {
|
|
448
454
|
setEmailInput(event.target.value);
|
|
449
455
|
setEmailError(null);
|
|
450
456
|
}, placeholder: copy.customerEmailPlaceholder, "aria-invalid": Boolean(emailError), className: 'solidgate-checkout-email-input' })] }), emailError ? (_jsx("p", { role: 'alert', className: 'solidgate-checkout-alert', children: emailError })) : null, _jsx("button", { type: 'submit', disabled: emailSubmitting, className: 'solidgate-checkout-action', children: copy.customerEmailContinue })] })) : null, paymentForm, statusMessage && view.state !== 'ready' ? (_jsx("div", { className: 'solidgate-checkout-status', children: _jsx("p", { role: 'status', "aria-live": 'polite', children: statusMessage }) })) : null, recoveryActions] })) : null] }), _jsx("style", { children: solidgateCheckoutDialogStyles })] }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const solidgateCheckoutDialogStyles = "\n.solidgate-checkout-overlay {\n position: fixed;\n inset: 0;\n z-index: 100;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n padding: 16px;\n background: rgb(17 24 39 / 60%);\n backdrop-filter: blur(4px);\n}\n\n.solidgate-checkout-overlay.is-branded {\n z-index: 110;\n align-items: stretch;\n padding: 0;\n background: #fff;\n backdrop-filter: none;\n}\n\n.solidgate-checkout-dialog {\n box-sizing: border-box;\n width: min(100%, 512px);\n max-height: calc(100svh - 32px);\n overflow-y: auto;\n border: 1px solid var(--color-border, #d1d5db);\n border-radius: 16px;\n background: var(--color-surface, #fff);\n padding: 20px;\n color: var(--color-text, #111827);\n box-shadow: 0 24px 80px rgb(17 24 39 / 22%);\n}\n\n.solidgate-checkout-dialog.is-branded {\n width: min(100%, 430px);\n height: 100svh;\n max-height: 100svh;\n border: 0;\n border-radius: 0;\n background: #fff;\n padding: 0 17px 28px;\n color: #303136;\n box-shadow: none;\n font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n}\n\n.solidgate-checkout-dialog.is-preparing {\n position: absolute;\n opacity: 0;\n pointer-events: none;\n}\n\n.solidgate-checkout-loader {\n display: grid;\n place-items: center;\n}\n\n.solidgate-checkout-spinner {\n box-sizing: border-box;\n width: 44px;\n height: 44px;\n border: 4px solid var(--color-border, #d1d5db);\n border-top-color: var(--color-primary, #22c55e);\n border-radius: 50%;\n animation: solidgate-checkout-spin 720ms linear infinite;\n}\n\n.solidgate-checkout-visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n@keyframes solidgate-checkout-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .solidgate-checkout-spinner {\n animation-duration: 1.6s;\n }\n}\n\n.solidgate-checkout-header {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n margin-bottom: 16px;\n}\n\n.solidgate-checkout-dialog.is-branded .solidgate-checkout-header {\n min-height: 70px;\n margin: 0 -17px;\n padding: 0 56px;\n}\n\n.solidgate-checkout-title {\n margin: 0;\n color: var(--color-text, #111827);\n font-size: 18px;\n font-weight: 600;\n line-height: 1.3;\n}\n\n.solidgate-checkout-dialog.is-branded .solidgate-checkout-title {\n color: #020203;\n font-size: 24px;\n font-weight: 900;\n line-height: 1.15;\n text-align: center;\n}\n\n.solidgate-checkout-close {\n position: absolute;\n left: 0;\n top: 50%;\n width: 36px;\n height: 36px;\n flex: none;\n border: 1px solid var(--color-border, #d1d5db);\n border-radius: 8px;\n background: var(--color-bg, #f9fafb);\n color: var(--color-text, #111827);\n font-size: 22px;\n transform: translateY(-50%);\n cursor: pointer;\n}\n\n.solidgate-checkout-dialog.is-branded .solidgate-checkout-close {\n left: 16px;\n width: 40px;\n height: 40px;\n border: 0;\n border-radius: 999px;\n background: #f2f2f3;\n color: #aaa;\n}\n\n.solidgate-checkout-close span,\n.solidgate-checkout-close span::after {\n position: absolute;\n left: 50%;\n top: 50%;\n width: 22px;\n height: 3px;\n border-radius: 999px;\n background: currentColor;\n content: '';\n}\n\n.solidgate-checkout-close span {\n transform: translate(-50%, -50%) rotate(45deg);\n}\n\n.solidgate-checkout-close span::after {\n transform: translate(-50%, -50%) rotate(90deg);\n}\n\n.solidgate-checkout-close:hover {\n background: var(--color-accent-soft, #eef2ff);\n}\n\n.solidgate-checkout-summary {\n color: #303136;\n}\n\n.solidgate-checkout-countdown {\n min-height: 37px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0;\n border-radius: 12px;\n background: color-mix(in srgb, var(--solidgate-checkout-theme-color) 10%, #fff);\n color: var(--solidgate-checkout-theme-color);\n font-size: 16px;\n font-weight: 900;\n text-align: center;\n}\n\n.solidgate-checkout-satisfaction {\n margin: 17px 0 21px;\n color: #2f3034;\n font-size: 19px;\n font-weight: 800;\n line-height: 1.15;\n text-align: center;\n}\n\n.solidgate-checkout-satisfaction strong {\n color: var(--solidgate-checkout-theme-color);\n}\n\n.solidgate-checkout-price-stack {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.solidgate-checkout-price-row,\n.solidgate-checkout-promo,\n.solidgate-checkout-total {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.solidgate-checkout-price-row {\n font-size: 17px;\n}\n\n.solidgate-checkout-price-row.is-muted {\n color: #9d9d9f;\n font-weight: 500;\n}\n\n.solidgate-checkout-strike {\n text-decoration: line-through 2px;\n}\n\n.solidgate-checkout-price-row.is-discount {\n color: #040508;\n font-weight: 900;\n}\n\n.solidgate-checkout-price-row.is-discount strong:last-child {\n color: #ff2544;\n}\n\n.solidgate-checkout-promo {\n min-height: 42px;\n border-radius: 13px;\n background: #f0f0f0;\n padding: 0 14px;\n color: #8d8d90;\n font-size: 16px;\n}\n\n.solidgate-checkout-promo strong {\n color: #050609;\n font-weight: 900;\n}\n\n.solidgate-checkout-total {\n min-height: 76px;\n margin-top: 20px;\n border-top: 1px solid #dedede;\n font-size: 23px;\n font-weight: 800;\n}\n\n.solidgate-checkout-total.is-plain {\n margin-top: 0;\n}\n\n.solidgate-checkout-total strong {\n font-size: 25px;\n font-weight: 900;\n}\n\n.solidgate-checkout-total strong {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n}\n.solidgate-checkout-total svg {\n width: 25px;\n height: 25px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linejoin: round;\n}\n.solidgate-checkout-saved {\n width: fit-content;\n min-height: 36px;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n margin: -8px 0 9px auto;\n border-radius: 12px;\n background: #ffdce3;\n padding: 0 14px;\n color: #ff2544;\n font-size: 14px;\n font-weight: 900;\n line-height: 1.1;\n}\n\n.solidgate-checkout-email-form,\n.solidgate-checkout-email-label,\n.solidgate-checkout-wallets {\n display: grid;\n}\n\n.solidgate-checkout-email-form {\n gap: 16px;\n}\n\n.solidgate-checkout-email-label {\n gap: 8px;\n color: var(--color-text, #111827);\n font-size: 14px;\n font-weight: 500;\n}\n\n.solidgate-checkout-email-input {\n box-sizing: border-box;\n width: 100%;\n min-height: 44px;\n border: 1px solid var(--color-border, #d1d5db);\n border-radius: 8px;\n background: var(--color-bg, #fff);\n padding: 8px 12px;\n color: var(--color-text, #111827);\n font: inherit;\n}\n\n.solidgate-checkout-email-input::placeholder,\n.solidgate-checkout-status,\n.solidgate-checkout-status-text {\n color: var(--color-text-muted, #6b7280);\n}\n\n.solidgate-checkout-email-input:focus-visible,\n.solidgate-checkout-close:focus-visible,\n.solidgate-checkout-action:focus-visible {\n outline: 2px solid var(--color-accent, #3f51b5);\n outline-offset: 2px;\n}\n\n.solidgate-checkout-alert {\n margin: 0;\n color: var(--color-danger, #b91c1c);\n font-size: 14px;\n}\n\n.solidgate-checkout-action {\n box-sizing: border-box;\n width: 100%;\n min-height: 44px;\n border: 1px solid var(--color-accent, #3f51b5);\n border-radius: 8px;\n background: var(--color-accent, #3f51b5);\n padding: 8px 16px;\n color: var(--color-primary-text, #fff);\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n\n.solidgate-checkout-action:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.solidgate-checkout-action--spaced {\n margin-top: 16px;\n}\n\n.solidgate-checkout-wallets {\n gap: 12px;\n}\n.solidgate-checkout-wallets > div:empty,\n.solidgate-checkout-wallets:not(:has(> div:not(:empty))) { display: none; }\n.solidgate-checkout-wallets > div:not(:empty) {\n width: 100%;\n height: 54px;\n}\n.solidgate-checkout-wallets apple-pay-button {\n --apple-pay-button-width: 100%;\n --apple-pay-button-height: 54px;\n --apple-pay-button-border-radius: 0;\n width: 100%;\n height: 54px;\n}\n.solidgate-checkout-payment.is-branded {\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin: 0 -17px;\n padding: 9px 17px 32px;\n background: #f2f2f3;\n}\n.solidgate-checkout-methods {\n display: grid;\n grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);\n gap: 42px;\n}\n.solidgate-checkout-method {\n min-height: 74px;\n border: 2px solid #e6e7eb;\n border-radius: 13px;\n background: #fff;\n color: #303136;\n padding: 9px 8px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n font: inherit;\n cursor: pointer;\n}\n.solidgate-checkout-method[aria-selected='true'] { border: 3px solid #050609; }\n.solidgate-checkout-method:focus-visible { outline: 2px solid var(--solidgate-checkout-theme-color, #3f51b5); outline-offset: 2px; }\n.solidgate-checkout-method strong { font-size: 20px; font-weight: 800; line-height: 1.1; }\n.solidgate-checkout-tab-wallet { width: 58px; height: 36px; }\n.solidgate-checkout-tab-brands { display: flex; align-items: center; gap: 4px; }\n.solidgate-checkout-tab-brand { width: 24px; height: 15px; }\n.solidgate-checkout-tab-wallet svg, .solidgate-checkout-tab-brand svg { width: 100%; height: 100%; }\n.solidgate-checkout-secure {\n min-height: 47px;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 9px;\n margin: 0;\n border-radius: 12px;\n background: #dcffd0;\n color: #207733;\n font-size: 18px;\n font-weight: 800;\n}\n.solidgate-checkout-secure svg { width: 23px; height: 23px; }\n.solidgate-checkout-payment.is-branded .solidgate-checkout-card { padding: 10px 14px 0; }\n.solidgate-checkout-secure.is-card { width: fit-content; margin: 14px auto 0; padding: 0 14px; }\n.solidgate-sdk-card-container iframe { border: 0; }\n.solidgate-checkout-payment[hidden],\n.solidgate-checkout-wallets[hidden],\n.solidgate-checkout-card[hidden] { display: none; }\n.solidgate-inline-wallet { width: 100%; }\n.solidgate-inline-wallet .solidgate-checkout-wallets:has(> div:not(:empty)) { margin-bottom: 12px; }\n\n.solidgate-checkout-status {\n margin-top: 12px;\n border: 1px solid var(--color-border, #d1d5db);\n border-radius: 12px;\n background: var(--color-bg, #f9fafb);\n padding: 16px;\n font-size: 14px;\n}\n\n.solidgate-checkout-status p,\n.solidgate-checkout-status-text {\n margin: 0;\n}\n";
|
|
1
|
+
export declare const solidgateCheckoutDialogStyles = "\n.solidgate-checkout-overlay {\n position: fixed;\n inset: 0;\n z-index: 100;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n overflow: hidden;\n padding: 16px;\n background: rgb(17 24 39 / 60%);\n backdrop-filter: blur(4px);\n}\n\n.solidgate-checkout-overlay.is-branded {\n z-index: 110;\n align-items: stretch;\n padding: 0;\n background: #fff;\n backdrop-filter: none;\n}\n\n.solidgate-checkout-dialog {\n box-sizing: border-box;\n width: min(100%, 512px);\n max-height: calc(100svh - 32px);\n overflow-y: auto;\n border: 1px solid var(--color-border, #d1d5db);\n border-radius: 16px;\n background: var(--color-surface, #fff);\n padding: 20px;\n color: var(--color-text, #111827);\n box-shadow: 0 24px 80px rgb(17 24 39 / 22%);\n}\n\n.solidgate-checkout-dialog.is-branded {\n width: min(100%, 430px);\n height: 100svh;\n max-height: 100svh;\n border: 0;\n border-radius: 0;\n background: #fff;\n padding: 0 17px 28px;\n color: #303136;\n box-shadow: none;\n font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;\n}\n\n.solidgate-checkout-dialog.is-preparing {\n position: absolute;\n opacity: 0;\n pointer-events: none;\n}\n\n.solidgate-checkout-loader {\n display: grid;\n place-items: center;\n}\n\n.solidgate-checkout-spinner {\n box-sizing: border-box;\n width: 44px;\n height: 44px;\n border: 4px solid var(--color-border, #d1d5db);\n border-top-color: var(--color-primary, #22c55e);\n border-radius: 50%;\n animation: solidgate-checkout-spin 720ms linear infinite;\n}\n\n.solidgate-checkout-visually-hidden {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n white-space: nowrap;\n}\n\n@keyframes solidgate-checkout-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .solidgate-checkout-spinner {\n animation-duration: 1.6s;\n }\n}\n\n.solidgate-checkout-header {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 16px;\n margin-bottom: 16px;\n}\n\n.solidgate-checkout-dialog.is-branded .solidgate-checkout-header {\n min-height: 70px;\n margin: 0 -17px;\n padding: 0 56px;\n}\n\n.solidgate-checkout-title {\n margin: 0;\n color: var(--color-text, #111827);\n font-size: 18px;\n font-weight: 600;\n line-height: 1.3;\n}\n\n.solidgate-checkout-dialog.is-branded .solidgate-checkout-title {\n color: #020203;\n font-size: 24px;\n font-weight: 900;\n line-height: 1.15;\n text-align: center;\n}\n\n.solidgate-checkout-close {\n position: absolute;\n left: 0;\n top: 50%;\n width: 36px;\n height: 36px;\n flex: none;\n border: 1px solid var(--color-border, #d1d5db);\n border-radius: 8px;\n background: var(--color-bg, #f9fafb);\n color: var(--color-text, #111827);\n font-size: 22px;\n transform: translateY(-50%);\n cursor: pointer;\n}\n\n.solidgate-checkout-dialog.is-branded .solidgate-checkout-close {\n left: 16px;\n width: 40px;\n height: 40px;\n border: 0;\n border-radius: 999px;\n background: #f2f2f3;\n color: #aaa;\n}\n\n.solidgate-checkout-close span,\n.solidgate-checkout-close span::after {\n position: absolute;\n left: 50%;\n top: 50%;\n width: 22px;\n height: 3px;\n border-radius: 999px;\n background: currentColor;\n content: '';\n}\n\n.solidgate-checkout-close span {\n transform: translate(-50%, -50%) rotate(45deg);\n}\n\n.solidgate-checkout-close span::after {\n transform: translate(-50%, -50%) rotate(90deg);\n}\n\n.solidgate-checkout-close:hover {\n background: var(--color-accent-soft, #eef2ff);\n}\n\n.solidgate-checkout-summary {\n color: #303136;\n}\n\n.solidgate-checkout-countdown {\n min-height: 37px;\n display: flex;\n align-items: center;\n justify-content: center;\n margin: 0;\n border-radius: 12px;\n background: color-mix(in srgb, var(--solidgate-checkout-theme-color) 10%, #fff);\n color: var(--solidgate-checkout-theme-color);\n font-size: 16px;\n font-weight: 900;\n text-align: center;\n}\n\n.solidgate-checkout-satisfaction {\n margin: 17px 0 21px;\n color: #2f3034;\n font-size: 19px;\n font-weight: 800;\n line-height: 1.15;\n text-align: center;\n}\n\n.solidgate-checkout-satisfaction strong {\n color: var(--solidgate-checkout-theme-color);\n}\n\n.solidgate-checkout-price-stack {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.solidgate-checkout-price-row,\n.solidgate-checkout-promo,\n.solidgate-checkout-total {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n}\n\n.solidgate-checkout-price-row {\n font-size: 17px;\n}\n\n.solidgate-checkout-price-row.is-muted {\n color: #9d9d9f;\n font-weight: 500;\n}\n\n.solidgate-checkout-strike {\n text-decoration: line-through 2px;\n}\n\n.solidgate-checkout-price-row.is-discount {\n color: #040508;\n font-weight: 900;\n}\n\n.solidgate-checkout-price-row.is-discount strong:last-child {\n color: #ff2544;\n}\n\n.solidgate-checkout-promo {\n min-height: 42px;\n border-radius: 13px;\n background: #f0f0f0;\n padding: 0 14px;\n color: #8d8d90;\n font-size: 16px;\n}\n\n.solidgate-checkout-promo strong {\n color: #050609;\n font-weight: 900;\n}\n\n.solidgate-checkout-total {\n min-height: 76px;\n margin-top: 20px;\n border-top: 1px solid #dedede;\n font-size: 23px;\n font-weight: 800;\n}\n\n.solidgate-checkout-total.is-plain {\n margin-top: 0;\n}\n\n.solidgate-checkout-total strong {\n font-size: 25px;\n font-weight: 900;\n}\n\n.solidgate-checkout-total strong {\n display: inline-flex;\n align-items: center;\n gap: 7px;\n}\n.solidgate-checkout-total svg {\n width: 25px;\n height: 25px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linejoin: round;\n}\n.solidgate-checkout-saved {\n width: fit-content;\n min-height: 36px;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 8px;\n margin: -8px 0 9px auto;\n border-radius: 12px;\n background: #ffdce3;\n padding: 0 14px;\n color: #ff2544;\n font-size: 14px;\n font-weight: 900;\n line-height: 1.1;\n}\n\n.solidgate-checkout-email-form,\n.solidgate-checkout-email-label,\n.solidgate-checkout-wallets {\n display: grid;\n}\n\n.solidgate-checkout-email-form {\n gap: 16px;\n}\n\n.solidgate-checkout-email-label {\n gap: 8px;\n color: var(--color-text, #111827);\n font-size: 14px;\n font-weight: 500;\n}\n\n.solidgate-checkout-email-input {\n box-sizing: border-box;\n width: 100%;\n min-height: 44px;\n border: 1px solid var(--color-border, #d1d5db);\n border-radius: 8px;\n background: var(--color-bg, #fff);\n padding: 8px 12px;\n color: var(--color-text, #111827);\n font: inherit;\n}\n\n.solidgate-checkout-email-input::placeholder,\n.solidgate-checkout-status,\n.solidgate-checkout-status-text {\n color: var(--color-text-muted, #6b7280);\n}\n\n.solidgate-checkout-email-input:focus-visible,\n.solidgate-checkout-close:focus-visible,\n.solidgate-checkout-action:focus-visible {\n outline: 2px solid var(--color-accent, #3f51b5);\n outline-offset: 2px;\n}\n\n.solidgate-checkout-alert {\n margin: 0;\n color: var(--color-danger, #b91c1c);\n font-size: 14px;\n}\n\n.solidgate-checkout-action {\n display: block;\n box-sizing: border-box;\n text-align: center;\n text-decoration: none;\n width: 100%;\n min-height: 44px;\n border: 1px solid var(--color-accent, #3f51b5);\n border-radius: 8px;\n background: var(--color-accent, #3f51b5);\n padding: 8px 16px;\n color: var(--color-primary-text, #fff);\n font: inherit;\n font-weight: 600;\n cursor: pointer;\n}\n\n.solidgate-checkout-action:disabled {\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.solidgate-checkout-action--spaced {\n margin-top: 16px;\n}\n\n.solidgate-checkout-wallets {\n gap: 12px;\n}\n.solidgate-checkout-wallets > div:empty,\n.solidgate-checkout-wallets:not(:has(> div:not(:empty))) { display: none; }\n.solidgate-checkout-wallets > div:not(:empty) {\n width: 100%;\n height: 54px;\n}\n.solidgate-checkout-wallets apple-pay-button {\n --apple-pay-button-width: 100%;\n --apple-pay-button-height: 54px;\n --apple-pay-button-border-radius: 0;\n width: 100%;\n height: 54px;\n}\n.solidgate-checkout-payment.is-branded {\n display: flex;\n flex-direction: column;\n gap: 12px;\n margin: 0 -17px;\n padding: 9px 17px 32px;\n background: #f2f2f3;\n}\n.solidgate-checkout-methods {\n display: grid;\n grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);\n gap: 42px;\n}\n.solidgate-checkout-method {\n min-height: 74px;\n border: 2px solid #e6e7eb;\n border-radius: 13px;\n background: #fff;\n color: #303136;\n padding: 9px 8px;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 6px;\n font: inherit;\n cursor: pointer;\n}\n.solidgate-checkout-method[aria-selected='true'] { border: 3px solid #050609; }\n.solidgate-checkout-method:focus-visible { outline: 2px solid var(--solidgate-checkout-theme-color, #3f51b5); outline-offset: 2px; }\n.solidgate-checkout-method strong { font-size: 20px; font-weight: 800; line-height: 1.1; }\n.solidgate-checkout-tab-wallet { width: 58px; height: 36px; }\n.solidgate-checkout-tab-brands { display: flex; align-items: center; gap: 4px; }\n.solidgate-checkout-tab-brand { width: 24px; height: 15px; }\n.solidgate-checkout-tab-wallet svg, .solidgate-checkout-tab-brand svg { width: 100%; height: 100%; }\n.solidgate-checkout-secure {\n min-height: 47px;\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 9px;\n margin: 0;\n border-radius: 12px;\n background: #dcffd0;\n color: #207733;\n font-size: 18px;\n font-weight: 800;\n}\n.solidgate-checkout-secure svg { width: 23px; height: 23px; }\n.solidgate-checkout-payment.is-branded .solidgate-checkout-card { padding: 10px 14px 0; }\n.solidgate-checkout-secure.is-card { width: fit-content; margin: 14px auto 0; padding: 0 14px; }\n.solidgate-sdk-card-container iframe { border: 0; }\n.solidgate-checkout-payment[hidden],\n.solidgate-checkout-wallets[hidden],\n.solidgate-checkout-card[hidden] { display: none; }\n.solidgate-inline-wallet { width: 100%; }\n.solidgate-inline-wallet .solidgate-checkout-wallets:has(> div:not(:empty)) { margin-bottom: 12px; }\n\n.solidgate-checkout-status {\n margin-top: 12px;\n border: 1px solid var(--color-border, #d1d5db);\n border-radius: 12px;\n background: var(--color-bg, #f9fafb);\n padding: 16px;\n font-size: 14px;\n}\n\n.solidgate-checkout-status p,\n.solidgate-checkout-status-text {\n margin: 0;\n}\n";
|
|
@@ -350,7 +350,10 @@ export const solidgateCheckoutDialogStyles = `
|
|
|
350
350
|
}
|
|
351
351
|
|
|
352
352
|
.solidgate-checkout-action {
|
|
353
|
+
display: block;
|
|
353
354
|
box-sizing: border-box;
|
|
355
|
+
text-align: center;
|
|
356
|
+
text-decoration: none;
|
|
354
357
|
width: 100%;
|
|
355
358
|
min-height: 44px;
|
|
356
359
|
border: 1px solid var(--color-accent, #3f51b5);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FunnelSdkRuntimeConfigOverrides, FunnelStepType, RuntimeMode } from '@funnelsgrove/runtime';
|
|
2
2
|
import type { PublicAnalyticsFeatureFlags } from '@funnelsgrove/analytics';
|
|
3
3
|
import type { PrepareSolidgateCheckoutInput, SolidgateCheckoutPreparation } from '../services/solidgate.service.js';
|
|
4
|
-
export type SolidgateCheckoutDialogState = 'collecting_email' | 'loading' | 'ready' | 'submitting' | 'verifying' | 'success' | 'recoverable_error' | 'expired';
|
|
4
|
+
export type SolidgateCheckoutDialogState = 'collecting_email' | 'loading' | 'ready' | 'submitting' | 'verifying' | 'success' | 'existing_subscription' | 'recoverable_error' | 'expired';
|
|
5
5
|
export type SolidgateCheckoutDialogContent = {
|
|
6
6
|
title: string;
|
|
7
7
|
closeAriaLabel: string;
|
|
@@ -12,6 +12,8 @@ export type SolidgateCheckoutDialogContent = {
|
|
|
12
12
|
error: string;
|
|
13
13
|
expired: string;
|
|
14
14
|
retry: string;
|
|
15
|
+
existingSubscriptionTitle: string;
|
|
16
|
+
existingSubscriptionContinue: string;
|
|
15
17
|
resumeVerification: string;
|
|
16
18
|
customerEmailLabel: string;
|
|
17
19
|
customerEmailPlaceholder: string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { StripeSubscriptionCheckoutSession } from '../hooks/useStripeSubscriptionCheckoutSession.js';
|
|
2
|
+
import { type StripeWalletSurfaceProps } from './StripeWalletSurface.internal.js';
|
|
3
|
+
export type StripeSubscriptionWalletFirstOptions = {
|
|
4
|
+
paymentMethod: 'applePay' | 'googlePay';
|
|
5
|
+
buttonLabel?: string;
|
|
6
|
+
onPayerEmail: (email: string) => Promise<string>;
|
|
7
|
+
merchantCountry?: string;
|
|
8
|
+
};
|
|
9
|
+
type Props = Omit<StripeWalletSurfaceProps, 'session'> & {
|
|
10
|
+
currency: string;
|
|
11
|
+
session: StripeSubscriptionCheckoutSession;
|
|
12
|
+
walletFirst: StripeSubscriptionWalletFirstOptions;
|
|
13
|
+
};
|
|
14
|
+
export declare function StripeSubscriptionWalletFirstSurface({ amountCents, checkoutAnalytics, className, containerClassName, currency, disabled, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, returnUrl, session, summaryLabel, surfaceId, suspended, walletFirst, }: Props): import("react/jsx-runtime").JSX.Element | null;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useRef } from 'react';
|
|
4
|
+
import { useStripeSubscriptionWalletFirstRequest } from '../hooks/useStripeSubscriptionWalletFirstRequest.js';
|
|
5
|
+
import { buildStripeWalletSurfaceCheckoutAnalytics } from './StripeWalletSurface.internal.js';
|
|
6
|
+
import { ApplePaySubscribeButton } from './ApplePaySubscribeButton.js';
|
|
7
|
+
import { GooglePaySubscribeButton } from './GooglePaySubscribeButton.js';
|
|
8
|
+
export function StripeSubscriptionWalletFirstSurface({ amountCents, checkoutAnalytics, className, containerClassName, currency, disabled, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, returnUrl, session, summaryLabel, surfaceId, suspended, walletFirst, }) {
|
|
9
|
+
var _a;
|
|
10
|
+
const { paymentMethod, onPayerEmail, merchantCountry } = walletFirst;
|
|
11
|
+
const reportedAvailability = useRef(null);
|
|
12
|
+
const request = useStripeSubscriptionWalletFirstRequest({
|
|
13
|
+
amountCents,
|
|
14
|
+
checkoutAnalytics: buildStripeWalletSurfaceCheckoutAnalytics(checkoutAnalytics, surfaceId),
|
|
15
|
+
currency,
|
|
16
|
+
merchantCountry,
|
|
17
|
+
onError: (message) => onError === null || onError === void 0 ? void 0 : onError(message, { paymentMethod, surfaceId }),
|
|
18
|
+
onCancel,
|
|
19
|
+
onPayerEmail,
|
|
20
|
+
onPaymentInfoSubmitted,
|
|
21
|
+
onSuccess: (result) => onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess(result),
|
|
22
|
+
paymentMethod,
|
|
23
|
+
returnUrl,
|
|
24
|
+
session,
|
|
25
|
+
summaryLabel,
|
|
26
|
+
});
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
if (request.available === null)
|
|
29
|
+
return;
|
|
30
|
+
const signature = `${session.intentKey}:${paymentMethod}:${request.available}`;
|
|
31
|
+
if (reportedAvailability.current === signature)
|
|
32
|
+
return;
|
|
33
|
+
reportedAvailability.current = signature;
|
|
34
|
+
onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange({
|
|
35
|
+
anyAvailable: request.available,
|
|
36
|
+
methods: { [paymentMethod]: request.available },
|
|
37
|
+
resolved: true,
|
|
38
|
+
});
|
|
39
|
+
}, [onAvailabilityChange, paymentMethod, request.available, session.intentKey]);
|
|
40
|
+
if (!request.available || suspended)
|
|
41
|
+
return null;
|
|
42
|
+
const Button = paymentMethod === 'applePay'
|
|
43
|
+
? ApplePaySubscribeButton : GooglePaySubscribeButton;
|
|
44
|
+
const buttonLabel = (_a = walletFirst.buttonLabel) === null || _a === void 0 ? void 0 : _a.trim();
|
|
45
|
+
const launch = () => {
|
|
46
|
+
if (!request.launch())
|
|
47
|
+
void (onUnavailableClick === null || onUnavailableClick === void 0 ? void 0 : onUnavailableClick());
|
|
48
|
+
};
|
|
49
|
+
return (_jsx("div", { className: containerClassName, "data-wallet-surface-id": surfaceId, children: buttonLabel ? (_jsx("button", { type: 'button', className: className, disabled: disabled || request.busy, "aria-label": buttonLabel, "aria-busy": request.busy, onClick: launch, children: buttonLabel })) : (_jsx(Button, { className: className, disabled: disabled || request.busy, onClick: launch })) }));
|
|
50
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type StripeSubscriptionCheckoutSessionInput } from '../hooks/useStripeSubscriptionCheckoutSession.js';
|
|
2
2
|
import { applyStripeWalletSurfaceMethodAvailability as applyStripeSubscriptionWalletSurfaceMethodAvailability, buildStripeWalletSurfaceAnalyticsMetadata as buildStripeSubscriptionWalletSurfaceAnalyticsMetadata, buildStripeWalletSurfaceAvailability as buildStripeSubscriptionWalletSurfaceAvailability, buildStripeWalletSurfaceCheckoutAnalytics as buildStripeSubscriptionWalletSurfaceCheckoutAnalytics, getStripeWalletSurfaceMethodAvailability as getStripeSubscriptionWalletSurfaceMethodAvailability, getStripeWalletSurfaceVisibleMethods as getStripeSubscriptionWalletSurfaceVisibleMethods, normalizeStripeWalletSurfaceMethods as normalizeStripeSubscriptionWalletSurfaceMethods, resolveStripeWalletSurfaceError as resolveStripeSubscriptionWalletSurfaceError, shouldRenderStripeWalletSurface as shouldRenderStripeSubscriptionWalletSurface, type StripeWalletSurfaceAvailability as StripeSubscriptionWalletSurfaceAvailability, type StripeWalletSurfaceAvailabilityState as StripeSubscriptionWalletSurfaceAvailabilityState, type StripeWalletSurfaceErrorContext as StripeSubscriptionWalletSurfaceErrorContext, type StripeWalletSurfaceErrorEmission as StripeSubscriptionWalletSurfaceErrorEmission, type StripeWalletSurfaceErrorState as StripeSubscriptionWalletSurfaceErrorState, type StripeWalletSurfaceProps, type StripeWalletSurfaceRenderStateInput as StripeSubscriptionWalletSurfaceRenderStateInput } from './StripeWalletSurface.internal.js';
|
|
3
|
+
import { type StripeSubscriptionWalletFirstOptions } from './StripeSubscriptionWalletFirstSurface.js';
|
|
3
4
|
export { applyStripeSubscriptionWalletSurfaceMethodAvailability, buildStripeSubscriptionWalletSurfaceAnalyticsMetadata, buildStripeSubscriptionWalletSurfaceAvailability, buildStripeSubscriptionWalletSurfaceCheckoutAnalytics, getStripeSubscriptionWalletSurfaceMethodAvailability, getStripeSubscriptionWalletSurfaceVisibleMethods, normalizeStripeSubscriptionWalletSurfaceMethods, resolveStripeSubscriptionWalletSurfaceError, shouldRenderStripeSubscriptionWalletSurface, };
|
|
4
5
|
export type { StripeSubscriptionWalletSurfaceAvailability, StripeSubscriptionWalletSurfaceAvailabilityState, StripeSubscriptionWalletSurfaceErrorContext, StripeSubscriptionWalletSurfaceErrorEmission, StripeSubscriptionWalletSurfaceErrorState, StripeSubscriptionWalletSurfaceRenderStateInput, };
|
|
5
6
|
export type StripeSubscriptionWalletSurfaceCheckoutInput = Omit<StripeSubscriptionCheckoutSessionInput, 'onError'> & {
|
|
@@ -7,5 +8,6 @@ export type StripeSubscriptionWalletSurfaceCheckoutInput = Omit<StripeSubscripti
|
|
|
7
8
|
};
|
|
8
9
|
export type StripeSubscriptionWalletSurfaceProps = Omit<StripeWalletSurfaceProps, 'customerEmail' | 'customerName' | 'returnUrl' | 'session'> & {
|
|
9
10
|
checkout: StripeSubscriptionWalletSurfaceCheckoutInput;
|
|
11
|
+
walletFirst?: StripeSubscriptionWalletFirstOptions;
|
|
10
12
|
};
|
|
11
|
-
export declare function StripeSubscriptionWalletSurface({ checkout, surfaceId, ...surfaceProps }: StripeSubscriptionWalletSurfaceProps): import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export declare function StripeSubscriptionWalletSurface({ checkout, surfaceId, walletFirst, ...surfaceProps }: StripeSubscriptionWalletSurfaceProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -14,12 +14,17 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
14
14
|
import { useMemo } from 'react';
|
|
15
15
|
import { useStripeSubscriptionCheckoutSession, } from '../hooks/useStripeSubscriptionCheckoutSession.js';
|
|
16
16
|
import { StripeWalletSurface, applyStripeWalletSurfaceMethodAvailability as applyStripeSubscriptionWalletSurfaceMethodAvailability, buildStripeWalletSurfaceAnalyticsMetadata as buildStripeSubscriptionWalletSurfaceAnalyticsMetadata, buildStripeWalletSurfaceAvailability as buildStripeSubscriptionWalletSurfaceAvailability, buildStripeWalletSurfaceCheckoutAnalytics as buildStripeSubscriptionWalletSurfaceCheckoutAnalytics, getStripeWalletSurfaceMethodAvailability as getStripeSubscriptionWalletSurfaceMethodAvailability, getStripeWalletSurfaceVisibleMethods as getStripeSubscriptionWalletSurfaceVisibleMethods, normalizeStripeWalletSurfaceMethods as normalizeStripeSubscriptionWalletSurfaceMethods, resolveStripeWalletSurfaceError as resolveStripeSubscriptionWalletSurfaceError, shouldRenderStripeWalletSurface as shouldRenderStripeSubscriptionWalletSurface, } from './StripeWalletSurface.internal.js';
|
|
17
|
+
import { StripeSubscriptionWalletFirstSurface, } from './StripeSubscriptionWalletFirstSurface.js';
|
|
17
18
|
export { applyStripeSubscriptionWalletSurfaceMethodAvailability, buildStripeSubscriptionWalletSurfaceAnalyticsMetadata, buildStripeSubscriptionWalletSurfaceAvailability, buildStripeSubscriptionWalletSurfaceCheckoutAnalytics, getStripeSubscriptionWalletSurfaceMethodAvailability, getStripeSubscriptionWalletSurfaceVisibleMethods, normalizeStripeSubscriptionWalletSurfaceMethods, resolveStripeSubscriptionWalletSurfaceError, shouldRenderStripeSubscriptionWalletSurface, };
|
|
18
19
|
export function StripeSubscriptionWalletSurface(_a) {
|
|
19
|
-
var
|
|
20
|
+
var _b, _c;
|
|
21
|
+
var { checkout, surfaceId, walletFirst } = _a, surfaceProps = __rest(_a, ["checkout", "surfaceId", "walletFirst"]);
|
|
20
22
|
const normalizedSurfaceId = surfaceId.trim();
|
|
21
23
|
const { customerName } = checkout, checkoutSessionInput = __rest(checkout, ["customerName"]);
|
|
22
24
|
const surfaceAnalyticsMetadata = useMemo(() => buildStripeSubscriptionWalletSurfaceAnalyticsMetadata(checkout.analyticsMetadata, normalizedSurfaceId), [checkout.analyticsMetadata, normalizedSurfaceId]);
|
|
23
25
|
const session = useStripeSubscriptionCheckoutSession(Object.assign(Object.assign({}, checkoutSessionInput), { analyticsMetadata: surfaceAnalyticsMetadata, checkoutAnalytics: surfaceProps.checkoutAnalytics }));
|
|
26
|
+
if (walletFirst) {
|
|
27
|
+
return (_jsx(StripeSubscriptionWalletFirstSurface, Object.assign({}, surfaceProps, { currency: (_c = (_b = checkout.plan) === null || _b === void 0 ? void 0 : _b.currency) !== null && _c !== void 0 ? _c : 'USD', customerEmail: checkout.customerEmail, customerName: customerName, returnUrl: checkout.returnUrl, session: session, surfaceId: normalizedSurfaceId, walletFirst: walletFirst })));
|
|
28
|
+
}
|
|
24
29
|
return (_jsx(StripeWalletSurface, Object.assign({}, surfaceProps, { customerEmail: checkout.customerEmail, customerName: customerName, returnUrl: checkout.returnUrl, session: session, surfaceId: normalizedSurfaceId })));
|
|
25
30
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type CheckoutPreparationFailure = {
|
|
2
|
+
intentKey: string;
|
|
3
|
+
count: number;
|
|
4
|
+
until: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const recordCheckoutPreparationFailure: (previous: CheckoutPreparationFailure | null, failure: {
|
|
7
|
+
intentKey: string;
|
|
8
|
+
retryAfterMs?: number | null;
|
|
9
|
+
}, now: number) => CheckoutPreparationFailure;
|
|
10
|
+
export declare const blocksCheckoutPreparation: (failure: CheckoutPreparationFailure | null, request: {
|
|
11
|
+
intentKey: string;
|
|
12
|
+
source?: string;
|
|
13
|
+
forceNew?: boolean;
|
|
14
|
+
}, now: number) => boolean;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const BASE_WINDOW_MS = 2000;
|
|
2
|
+
const MAX_WINDOW_MS = 30000;
|
|
3
|
+
const MAX_RETRY_AFTER_MS = 60000;
|
|
4
|
+
const AUTOMATIC_SOURCES = new Set(['checkout_render', 'wallet_render']);
|
|
5
|
+
export const recordCheckoutPreparationFailure = (previous, failure, now) => {
|
|
6
|
+
var _a;
|
|
7
|
+
const count = (previous === null || previous === void 0 ? void 0 : previous.intentKey) === failure.intentKey ? previous.count + 1 : 1;
|
|
8
|
+
const windowMs = Math.max(Math.min((_a = failure.retryAfterMs) !== null && _a !== void 0 ? _a : 0, MAX_RETRY_AFTER_MS), Math.min(BASE_WINDOW_MS * 2 ** (count - 1), MAX_WINDOW_MS));
|
|
9
|
+
return { intentKey: failure.intentKey, count, until: now + windowMs };
|
|
10
|
+
};
|
|
11
|
+
export const blocksCheckoutPreparation = (failure, request, now) => {
|
|
12
|
+
var _a;
|
|
13
|
+
return !request.forceNew
|
|
14
|
+
&& AUTOMATIC_SOURCES.has((_a = request.source) !== null && _a !== void 0 ? _a : '')
|
|
15
|
+
&& (failure === null || failure === void 0 ? void 0 : failure.intentKey) === request.intentKey
|
|
16
|
+
&& now < failure.until;
|
|
17
|
+
};
|
|
@@ -19,6 +19,7 @@ import { toAnalyticsPaymentMethod } from '../../../services/paymentMethodAnalyti
|
|
|
19
19
|
import { resolveCheckoutObservabilityAnalyticsContext, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
20
20
|
import { resolveStripeCheckoutAttempt, } from './checkoutAttempt.js';
|
|
21
21
|
import { isStripeCheckoutIdentityReady } from './checkoutIdentity.js';
|
|
22
|
+
import { blocksCheckoutPreparation, recordCheckoutPreparationFailure, } from './checkoutPreparationBackoff.js';
|
|
22
23
|
export { isStripeCheckoutIdentityReady as isStripeOneTimeCheckoutIdentityReady, } from './checkoutIdentity.js';
|
|
23
24
|
export function resolveStripeOneTimeCheckoutPreparationOptions(source, options = {}) {
|
|
24
25
|
var _a, _b;
|
|
@@ -94,6 +95,8 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId: nextR
|
|
|
94
95
|
const creatingIntentRef = useRef(null);
|
|
95
96
|
const requestIdRef = useRef(0);
|
|
96
97
|
const attemptRef = useRef(null);
|
|
98
|
+
const preparationFailureRef = useRef(null);
|
|
99
|
+
const recoveredIntentKeyRef = useRef(null);
|
|
97
100
|
const anonymousUserIdRef = useRef(null);
|
|
98
101
|
const checkoutIdentityReady = isStripeCheckoutIdentityReady({
|
|
99
102
|
runtimeConfig,
|
|
@@ -142,7 +145,8 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId: nextR
|
|
|
142
145
|
setCheckoutSessionId(null);
|
|
143
146
|
}, []);
|
|
144
147
|
const setError = useCallback((message) => {
|
|
145
|
-
if (isInactiveCheckoutSessionError(message)) {
|
|
148
|
+
if (isInactiveCheckoutSessionError(message) && recoveredIntentKeyRef.current !== intentKey) {
|
|
149
|
+
recoveredIntentKeyRef.current = intentKey;
|
|
146
150
|
clearActiveCheckoutSession();
|
|
147
151
|
setErrorState(null);
|
|
148
152
|
onError === null || onError === void 0 ? void 0 : onError(null);
|
|
@@ -150,10 +154,12 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId: nextR
|
|
|
150
154
|
}
|
|
151
155
|
setErrorState(message);
|
|
152
156
|
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
153
|
-
}, [clearActiveCheckoutSession, onError]);
|
|
157
|
+
}, [clearActiveCheckoutSession, intentKey, onError]);
|
|
154
158
|
const reset = useCallback(() => {
|
|
155
159
|
preparedSelectionRef.current = null;
|
|
156
160
|
attemptRef.current = null;
|
|
161
|
+
preparationFailureRef.current = null;
|
|
162
|
+
recoveredIntentKeyRef.current = null;
|
|
157
163
|
clearActiveCheckoutSession();
|
|
158
164
|
setLoading(createEmptyCheckoutPreparationLoading());
|
|
159
165
|
setRuntimeStripePublishableKey(null);
|
|
@@ -232,6 +238,7 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId: nextR
|
|
|
232
238
|
if (publishableKey) {
|
|
233
239
|
setRuntimeStripePublishableKey(publishableKey);
|
|
234
240
|
}
|
|
241
|
+
preparationFailureRef.current = null;
|
|
235
242
|
setClientSecret(response.clientSecret);
|
|
236
243
|
setClientSecretIntentKey(requestKey);
|
|
237
244
|
setCheckoutSessionId(((_d = response.checkoutSessionId) === null || _d === void 0 ? void 0 : _d.trim()) || null);
|
|
@@ -260,6 +267,11 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId: nextR
|
|
|
260
267
|
checkoutRequestId: failure.checkoutRequestId,
|
|
261
268
|
});
|
|
262
269
|
}
|
|
270
|
+
// The first inactive-session error per intent clears silently so the
|
|
271
|
+
// caller can prepare a fresh session at once; a repeat is shown.
|
|
272
|
+
if (!isInactiveCheckoutSessionError(message) || recoveredIntentKeyRef.current === requestKey) {
|
|
273
|
+
preparationFailureRef.current = recordCheckoutPreparationFailure(preparationFailureRef.current, { intentKey: requestKey, retryAfterMs: failure.retryAfterMs }, Date.now());
|
|
274
|
+
}
|
|
263
275
|
setError(message);
|
|
264
276
|
}
|
|
265
277
|
return null;
|
|
@@ -300,6 +312,15 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId: nextR
|
|
|
300
312
|
if (!configured || !plan) {
|
|
301
313
|
return null;
|
|
302
314
|
}
|
|
315
|
+
const preparationOptions = resolveStripeOneTimeCheckoutPreparationOptions(source, options);
|
|
316
|
+
// No state change here: a blocked call keeps the visible error as it is.
|
|
317
|
+
if (blocksCheckoutPreparation(preparationFailureRef.current, {
|
|
318
|
+
intentKey,
|
|
319
|
+
source: preparationOptions.checkoutStartSource,
|
|
320
|
+
forceNew: options.forceNew,
|
|
321
|
+
}, Date.now())) {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
303
324
|
onPreparationStart === null || onPreparationStart === void 0 ? void 0 : onPreparationStart();
|
|
304
325
|
(_a = preparedSelectionRef.current) !== null && _a !== void 0 ? _a : (preparedSelectionRef.current = latestSelectionRef.current);
|
|
305
326
|
if (!options.forceNew && activeClientSecret) {
|
|
@@ -308,7 +329,7 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId: nextR
|
|
|
308
329
|
setSourceLoading(source, true);
|
|
309
330
|
setError(null);
|
|
310
331
|
try {
|
|
311
|
-
return await createCheckoutSession(
|
|
332
|
+
return await createCheckoutSession(preparationOptions);
|
|
312
333
|
}
|
|
313
334
|
finally {
|
|
314
335
|
setSourceLoading(source, false);
|
|
@@ -317,6 +338,7 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId: nextR
|
|
|
317
338
|
activeClientSecret,
|
|
318
339
|
configured,
|
|
319
340
|
createCheckoutSession,
|
|
341
|
+
intentKey,
|
|
320
342
|
plan,
|
|
321
343
|
onPreparationStart,
|
|
322
344
|
setError,
|
|
@@ -28,6 +28,7 @@ export type StripeSubscriptionCheckoutSessionInput = {
|
|
|
28
28
|
};
|
|
29
29
|
export type StripeSubscriptionCheckoutPreparationOptions = {
|
|
30
30
|
checkoutStartSource?: 'card_click' | 'checkout_render' | 'wallet_click' | 'wallet_render' | (string & {});
|
|
31
|
+
customerEmail?: string;
|
|
31
32
|
forceNew?: boolean;
|
|
32
33
|
paymentMethod?: unknown;
|
|
33
34
|
};
|
|
@@ -18,6 +18,7 @@ import { toAnalyticsPaymentMethod } from '../../../services/paymentMethodAnalyti
|
|
|
18
18
|
import { resolveCheckoutObservabilityAnalyticsContext, trackCheckoutFailureShown, } from '../../../services/checkoutObservability.service.js';
|
|
19
19
|
import { resolveStripeCheckoutAttempt, } from './checkoutAttempt.js';
|
|
20
20
|
import { isStripeCheckoutIdentityReady } from './checkoutIdentity.js';
|
|
21
|
+
import { blocksCheckoutPreparation, recordCheckoutPreparationFailure, } from './checkoutPreparationBackoff.js';
|
|
21
22
|
export { isStripeCheckoutIdentityReady as isStripeSubscriptionCheckoutIdentityReady, } from './checkoutIdentity.js';
|
|
22
23
|
export function resolveStripeSubscriptionCheckoutPreparationOptions(source, options = {}) {
|
|
23
24
|
var _a, _b;
|
|
@@ -97,6 +98,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
97
98
|
const requestIdRef = useRef(0);
|
|
98
99
|
const attemptRef = useRef(null);
|
|
99
100
|
const recoveredIntentKeyRef = useRef(null);
|
|
101
|
+
const preparationFailureRef = useRef(null);
|
|
100
102
|
const handledRecoveryRequestRef = useRef(0);
|
|
101
103
|
const anonymousUserIdRef = useRef(null);
|
|
102
104
|
const checkoutIdentityReady = isStripeCheckoutIdentityReady({
|
|
@@ -169,6 +171,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
169
171
|
preparedSelectionRef.current = null;
|
|
170
172
|
attemptRef.current = null;
|
|
171
173
|
recoveredIntentKeyRef.current = null;
|
|
174
|
+
preparationFailureRef.current = null;
|
|
172
175
|
handledRecoveryRequestRef.current = recoveryRequest;
|
|
173
176
|
clearActiveCheckoutSession();
|
|
174
177
|
setLoading(createEmptyCheckoutPreparationLoading());
|
|
@@ -192,33 +195,48 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
192
195
|
: Object.assign(Object.assign({}, current), { [source]: value }));
|
|
193
196
|
}, []);
|
|
194
197
|
const createCheckoutSubscription = useCallback(async (options = {}) => {
|
|
195
|
-
var _a, _b;
|
|
198
|
+
var _a, _b, _c;
|
|
196
199
|
const forceNew = options.forceNew === true;
|
|
200
|
+
const preparedCustomerEmail = (_a = options.customerEmail) !== null && _a !== void 0 ? _a : customerEmail;
|
|
197
201
|
if (!configured || !plan || !displayPlan) {
|
|
198
202
|
return null;
|
|
199
203
|
}
|
|
200
204
|
if (!forceNew && activeClientSecret) {
|
|
201
205
|
return activeClientSecret;
|
|
202
206
|
}
|
|
203
|
-
if (!forceNew && ((
|
|
207
|
+
if (!forceNew && ((_b = creatingIntentRef.current) === null || _b === void 0 ? void 0 : _b.key) === intentKey) {
|
|
204
208
|
return creatingIntentRef.current.promise;
|
|
205
209
|
}
|
|
206
210
|
if (forceNew) {
|
|
207
211
|
clearActiveCheckoutSession();
|
|
208
212
|
attemptRef.current = null;
|
|
209
213
|
}
|
|
214
|
+
const requestKey = options.customerEmail === undefined
|
|
215
|
+
? intentKey
|
|
216
|
+
: buildStripeSubscriptionCheckoutIntentKey({
|
|
217
|
+
checkoutMode,
|
|
218
|
+
couponId,
|
|
219
|
+
customerEmail: preparedCustomerEmail,
|
|
220
|
+
paymentProfileId,
|
|
221
|
+
planKey,
|
|
222
|
+
provider,
|
|
223
|
+
returnUrl,
|
|
224
|
+
runtimeConfig,
|
|
225
|
+
runtimeConfigRevisionId,
|
|
226
|
+
stripePublishableKey,
|
|
227
|
+
userId,
|
|
228
|
+
});
|
|
210
229
|
attemptRef.current = resolveStripeCheckoutAttempt({
|
|
211
230
|
current: attemptRef.current,
|
|
212
231
|
forceNew,
|
|
213
|
-
intentKey,
|
|
232
|
+
intentKey: requestKey,
|
|
214
233
|
paymentsApiVersion: runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.paymentsApiVersion,
|
|
215
234
|
createId: () => crypto.randomUUID(),
|
|
216
235
|
});
|
|
217
|
-
const idempotencyKey = (
|
|
236
|
+
const idempotencyKey = (_c = attemptRef.current) === null || _c === void 0 ? void 0 : _c.id;
|
|
218
237
|
if (idempotencyKey && !userId && !anonymousUserIdRef.current) {
|
|
219
238
|
anonymousUserIdRef.current = `anonymous_${crypto.randomUUID()}`;
|
|
220
239
|
}
|
|
221
|
-
const requestKey = intentKey;
|
|
222
240
|
const requestPlanKey = planKey;
|
|
223
241
|
const requestId = requestIdRef.current + 1;
|
|
224
242
|
requestIdRef.current = requestId;
|
|
@@ -229,7 +247,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
229
247
|
plan,
|
|
230
248
|
analyticsMetadata: buildStripeSubscriptionCheckoutPreparationAnalyticsMetadata(analyticsMetadataRef.current, options),
|
|
231
249
|
couponId,
|
|
232
|
-
customerEmail,
|
|
250
|
+
customerEmail: preparedCustomerEmail,
|
|
233
251
|
returnUrl,
|
|
234
252
|
user_id: userId,
|
|
235
253
|
environment: checkoutMode,
|
|
@@ -250,6 +268,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
250
268
|
}
|
|
251
269
|
anonymousUserIdRef.current = ((_d = response.anonymousUserId) === null || _d === void 0 ? void 0 : _d.trim())
|
|
252
270
|
|| anonymousUserIdRef.current;
|
|
271
|
+
preparationFailureRef.current = null;
|
|
253
272
|
setClientSecret(response.clientSecret);
|
|
254
273
|
setClientSecretIntentKey(requestKey);
|
|
255
274
|
setCheckoutSessionId(((_e = response.checkoutSessionId) === null || _e === void 0 ? void 0 : _e.trim()) || null);
|
|
@@ -280,6 +299,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
280
299
|
checkoutRequestId: failure.checkoutRequestId,
|
|
281
300
|
});
|
|
282
301
|
}
|
|
302
|
+
preparationFailureRef.current = recordCheckoutPreparationFailure(preparationFailureRef.current, { intentKey: requestKey, retryAfterMs: failure.retryAfterMs }, Date.now());
|
|
283
303
|
setError(message);
|
|
284
304
|
}
|
|
285
305
|
return null;
|
|
@@ -315,6 +335,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
315
335
|
paymentProfileId,
|
|
316
336
|
provider,
|
|
317
337
|
setError,
|
|
338
|
+
stripePublishableKey,
|
|
318
339
|
userId,
|
|
319
340
|
]);
|
|
320
341
|
const prepareCheckout = useCallback(async (source, options = {}) => {
|
|
@@ -322,6 +343,15 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
322
343
|
if (!configured || !plan || !displayPlan) {
|
|
323
344
|
return null;
|
|
324
345
|
}
|
|
346
|
+
const preparationOptions = resolveStripeSubscriptionCheckoutPreparationOptions(source, options);
|
|
347
|
+
// No state change here: a blocked call keeps the visible error as it is.
|
|
348
|
+
if (blocksCheckoutPreparation(preparationFailureRef.current, {
|
|
349
|
+
intentKey,
|
|
350
|
+
source: preparationOptions.checkoutStartSource,
|
|
351
|
+
forceNew: options.forceNew,
|
|
352
|
+
}, Date.now())) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
325
355
|
onPreparationStart === null || onPreparationStart === void 0 ? void 0 : onPreparationStart();
|
|
326
356
|
(_a = preparedSelectionRef.current) !== null && _a !== void 0 ? _a : (preparedSelectionRef.current = latestSelectionRef.current);
|
|
327
357
|
if (!options.forceNew && activeClientSecret) {
|
|
@@ -330,7 +360,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
330
360
|
setSourceLoading(source, true);
|
|
331
361
|
setError(null);
|
|
332
362
|
try {
|
|
333
|
-
return await createCheckoutSubscription(
|
|
363
|
+
return await createCheckoutSubscription(preparationOptions);
|
|
334
364
|
}
|
|
335
365
|
finally {
|
|
336
366
|
setSourceLoading(source, false);
|
|
@@ -340,6 +370,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode: nextCheckou
|
|
|
340
370
|
configured,
|
|
341
371
|
createCheckoutSubscription,
|
|
342
372
|
displayPlan,
|
|
373
|
+
intentKey,
|
|
343
374
|
plan,
|
|
344
375
|
onPreparationStart,
|
|
345
376
|
setError,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { type StripeSubscriptionCheckoutAnalyticsContext } from '../services/checkoutCompletionAnalytics.service.js';
|
|
2
|
+
import type { StripeSubscriptionCheckoutSession } from './useStripeSubscriptionCheckoutSession.js';
|
|
3
|
+
type StripeSubscriptionWalletFirstRequestInput = {
|
|
4
|
+
amountCents: number;
|
|
5
|
+
checkoutAnalytics: StripeSubscriptionCheckoutAnalyticsContext | null;
|
|
6
|
+
currency?: string;
|
|
7
|
+
merchantCountry?: string;
|
|
8
|
+
onError: (message: string | null) => void;
|
|
9
|
+
onCancel?: () => void;
|
|
10
|
+
onPayerEmail: (email: string) => Promise<string>;
|
|
11
|
+
onPaymentInfoSubmitted?: () => void | Promise<void>;
|
|
12
|
+
onSuccess: (result: {
|
|
13
|
+
checkoutSessionId: string | null;
|
|
14
|
+
}) => void | Promise<void>;
|
|
15
|
+
paymentMethod: 'applePay' | 'googlePay';
|
|
16
|
+
returnUrl: string;
|
|
17
|
+
session: StripeSubscriptionCheckoutSession;
|
|
18
|
+
summaryLabel: string;
|
|
19
|
+
};
|
|
20
|
+
type StripeSubscriptionWalletFirstRequest = {
|
|
21
|
+
available: boolean | null;
|
|
22
|
+
launch: () => boolean;
|
|
23
|
+
busy: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function useStripeSubscriptionWalletFirstRequest({ amountCents, checkoutAnalytics, currency, merchantCountry, onError, onCancel, onSuccess, onPayerEmail, onPaymentInfoSubmitted, paymentMethod, returnUrl, session, summaryLabel, }: StripeSubscriptionWalletFirstRequestInput): StripeSubscriptionWalletFirstRequest;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { queueStripeSubscriptionCheckoutStarted, trackStripeSubscriptionPaymentInfoSubmitted, trackPaidStripeSubscriptionCheckoutCompleted, } from '../services/checkoutCompletionAnalytics.service.js';
|
|
4
|
+
import { isValidCheckoutEmail, isCheckoutEmailError } from '../../../services/checkoutEmail.js';
|
|
5
|
+
const normalizeCurrency = (currency) => currency.trim().toLowerCase() || 'usd';
|
|
6
|
+
export function useStripeSubscriptionWalletFirstRequest({ amountCents, checkoutAnalytics, currency = 'usd', merchantCountry = 'US', onError, onCancel, onSuccess, onPayerEmail, onPaymentInfoSubmitted, paymentMethod, returnUrl, session, summaryLabel, }) {
|
|
7
|
+
const requestKey = [session.planKey, paymentMethod, currency, merchantCountry, amountCents].join('|');
|
|
8
|
+
const [availability, setAvailability] = useState({ requestKey: '', stripePromise: null, value: null });
|
|
9
|
+
const [busy, setBusy] = useState(false);
|
|
10
|
+
const busyRef = useRef(false);
|
|
11
|
+
const confirmingRef = useRef(false);
|
|
12
|
+
const requestRef = useRef(null);
|
|
13
|
+
const callbacksRef = useRef({ onError, onCancel, onSuccess, onPayerEmail, onPaymentInfoSubmitted, session, checkoutAnalytics, returnUrl });
|
|
14
|
+
useLayoutEffect(() => {
|
|
15
|
+
callbacksRef.current = { onError, onCancel, onSuccess, onPayerEmail, onPaymentInfoSubmitted, session, checkoutAnalytics, returnUrl };
|
|
16
|
+
}, [onError, onCancel, onSuccess, onPayerEmail, onPaymentInfoSubmitted, session, checkoutAnalytics, returnUrl]);
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
let disposed = false;
|
|
19
|
+
let request = null;
|
|
20
|
+
let stripe = null;
|
|
21
|
+
requestRef.current = null;
|
|
22
|
+
if (!session.configured || !session.stripePromise) {
|
|
23
|
+
return () => {
|
|
24
|
+
disposed = true;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
const handlePaymentMethod = async (event) => {
|
|
28
|
+
var _a, _b, _c, _d;
|
|
29
|
+
if (!stripe || disposed || confirmingRef.current) {
|
|
30
|
+
event.complete('fail');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
confirmingRef.current = true;
|
|
34
|
+
busyRef.current = true;
|
|
35
|
+
setBusy(true);
|
|
36
|
+
let callbacks = callbacksRef.current;
|
|
37
|
+
let completed = false;
|
|
38
|
+
const completeWallet = (result) => {
|
|
39
|
+
if (completed)
|
|
40
|
+
return;
|
|
41
|
+
completed = true;
|
|
42
|
+
event.complete(result);
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
callbacks.onError(null);
|
|
46
|
+
const payerEmail = ((_a = event.payerEmail) === null || _a === void 0 ? void 0 : _a.trim())
|
|
47
|
+
|| ((_c = (_b = event.paymentMethod.billing_details) === null || _b === void 0 ? void 0 : _b.email) === null || _c === void 0 ? void 0 : _c.trim())
|
|
48
|
+
|| '';
|
|
49
|
+
if (!isValidCheckoutEmail(payerEmail)) {
|
|
50
|
+
completeWallet('invalid_payer_email');
|
|
51
|
+
throw new Error('Enter a valid email address to continue.');
|
|
52
|
+
}
|
|
53
|
+
const savedEmail = await callbacks.onPayerEmail(payerEmail);
|
|
54
|
+
if (!isValidCheckoutEmail(savedEmail)) {
|
|
55
|
+
completeWallet('invalid_payer_email');
|
|
56
|
+
throw new Error('Enter a valid email address to continue.');
|
|
57
|
+
}
|
|
58
|
+
callbacks = callbacksRef.current;
|
|
59
|
+
if (disposed) {
|
|
60
|
+
completeWallet('fail');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const clientSecret = await callbacks.session.prepareWalletCheckout({
|
|
64
|
+
checkoutStartSource: 'wallet_click', customerEmail: savedEmail,
|
|
65
|
+
forceNew: true, paymentMethod,
|
|
66
|
+
});
|
|
67
|
+
if (!clientSecret) {
|
|
68
|
+
completeWallet('fail');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (disposed) {
|
|
72
|
+
completeWallet('fail');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const checkout = stripe.initCheckout({ clientSecret });
|
|
76
|
+
const loaded = await checkout.loadActions();
|
|
77
|
+
if (loaded.type === 'error')
|
|
78
|
+
throw new Error(loaded.error.message);
|
|
79
|
+
if (disposed) {
|
|
80
|
+
completeWallet('fail');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const checkoutSession = loaded.actions.getSession();
|
|
84
|
+
const checkoutAmount = checkoutSession.total.total.minorUnitsAmount;
|
|
85
|
+
const checkoutCurrency = checkoutSession.currency;
|
|
86
|
+
const checkoutDivisor = checkoutSession.minorUnitsAmountDivisor;
|
|
87
|
+
if (checkoutAmount !== amountCents
|
|
88
|
+
|| checkoutCurrency.toLowerCase() !== normalizeCurrency(currency)
|
|
89
|
+
|| checkoutDivisor !== 100) {
|
|
90
|
+
throw new Error('The checkout price changed. Please refresh and try again.');
|
|
91
|
+
}
|
|
92
|
+
const checkoutSessionId = checkoutSession.id;
|
|
93
|
+
if (callbacks.checkoutAnalytics) {
|
|
94
|
+
queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, callbacks.checkoutAnalytics), { checkoutSessionId, checkoutStartSource: 'wallet_click', paymentMethod }));
|
|
95
|
+
await trackStripeSubscriptionPaymentInfoSubmitted(Object.assign(Object.assign({}, callbacks.checkoutAnalytics), { checkoutSessionId })).catch(() => 'analytics_rejected');
|
|
96
|
+
}
|
|
97
|
+
await ((_d = callbacks.onPaymentInfoSubmitted) === null || _d === void 0 ? void 0 : _d.call(callbacks));
|
|
98
|
+
const confirmation = await loaded.actions.confirm({
|
|
99
|
+
paymentMethod: event.paymentMethod.id,
|
|
100
|
+
redirect: 'if_required',
|
|
101
|
+
});
|
|
102
|
+
if (confirmation.type === 'error')
|
|
103
|
+
throw new Error(isCheckoutEmailError(confirmation.error)
|
|
104
|
+
? 'Enter a valid email address to continue.' : confirmation.error.message);
|
|
105
|
+
if (confirmation.session.status.type !== 'complete'
|
|
106
|
+
|| confirmation.session.status.paymentStatus !== 'paid') {
|
|
107
|
+
throw new Error('Payment is not confirmed yet. Please check its status before trying again.');
|
|
108
|
+
}
|
|
109
|
+
completeWallet('success');
|
|
110
|
+
if (callbacks.checkoutAnalytics) {
|
|
111
|
+
await trackPaidStripeSubscriptionCheckoutCompleted(Object.assign(Object.assign({}, callbacks.checkoutAnalytics), { checkoutSessionId })).catch(() => 'analytics_rejected');
|
|
112
|
+
}
|
|
113
|
+
await callbacks.onSuccess({ checkoutSessionId });
|
|
114
|
+
window.location.assign(callbacks.returnUrl);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
completeWallet('fail');
|
|
118
|
+
callbacks.onError(error instanceof Error ? error.message : 'Wallet checkout failed.');
|
|
119
|
+
}
|
|
120
|
+
finally {
|
|
121
|
+
confirmingRef.current = false;
|
|
122
|
+
busyRef.current = false;
|
|
123
|
+
setBusy(false);
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
const handleCancel = () => {
|
|
127
|
+
var _a, _b;
|
|
128
|
+
if (confirmingRef.current)
|
|
129
|
+
return;
|
|
130
|
+
busyRef.current = false;
|
|
131
|
+
setBusy(false);
|
|
132
|
+
callbacksRef.current.onError(null);
|
|
133
|
+
(_b = (_a = callbacksRef.current).onCancel) === null || _b === void 0 ? void 0 : _b.call(_a);
|
|
134
|
+
};
|
|
135
|
+
void session.stripePromise.then(async (resolvedStripe) => {
|
|
136
|
+
if (disposed)
|
|
137
|
+
return;
|
|
138
|
+
if (!resolvedStripe) {
|
|
139
|
+
setAvailability({ requestKey, stripePromise: session.stripePromise, value: false });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
stripe = resolvedStripe;
|
|
143
|
+
request = resolvedStripe.paymentRequest({
|
|
144
|
+
country: merchantCountry.trim().toUpperCase() || 'US',
|
|
145
|
+
currency: normalizeCurrency(currency),
|
|
146
|
+
total: {
|
|
147
|
+
label: summaryLabel.trim() || 'Selected plan',
|
|
148
|
+
amount: amountCents,
|
|
149
|
+
},
|
|
150
|
+
displayItems: [{
|
|
151
|
+
label: summaryLabel.trim() || 'Selected plan',
|
|
152
|
+
amount: amountCents,
|
|
153
|
+
}],
|
|
154
|
+
requestPayerEmail: true,
|
|
155
|
+
requestPayerName: true,
|
|
156
|
+
disableWallets: paymentMethod === 'applePay'
|
|
157
|
+
? ['googlePay', 'link', 'browserCard']
|
|
158
|
+
: ['applePay', 'link', 'browserCard'],
|
|
159
|
+
});
|
|
160
|
+
request.on('paymentmethod', handlePaymentMethod);
|
|
161
|
+
request.on('cancel', handleCancel);
|
|
162
|
+
const availability = await request.canMakePayment().catch(() => null);
|
|
163
|
+
if (disposed)
|
|
164
|
+
return;
|
|
165
|
+
const methodAvailable = Boolean(availability === null || availability === void 0 ? void 0 : availability[paymentMethod]);
|
|
166
|
+
requestRef.current = methodAvailable ? request : null;
|
|
167
|
+
setAvailability({ requestKey, stripePromise: session.stripePromise, value: methodAvailable });
|
|
168
|
+
}).catch(() => {
|
|
169
|
+
if (!disposed)
|
|
170
|
+
setAvailability({ requestKey, stripePromise: session.stripePromise, value: false });
|
|
171
|
+
});
|
|
172
|
+
return () => {
|
|
173
|
+
disposed = true;
|
|
174
|
+
if (request) {
|
|
175
|
+
if (!confirmingRef.current && busyRef.current) {
|
|
176
|
+
if (request.isShowing())
|
|
177
|
+
request.abort();
|
|
178
|
+
busyRef.current = false;
|
|
179
|
+
setBusy(false);
|
|
180
|
+
}
|
|
181
|
+
request.off('paymentmethod', handlePaymentMethod);
|
|
182
|
+
request.off('cancel', handleCancel);
|
|
183
|
+
}
|
|
184
|
+
requestRef.current = null;
|
|
185
|
+
};
|
|
186
|
+
}, [
|
|
187
|
+
amountCents,
|
|
188
|
+
currency,
|
|
189
|
+
merchantCountry,
|
|
190
|
+
paymentMethod,
|
|
191
|
+
session.configured,
|
|
192
|
+
requestKey,
|
|
193
|
+
session.stripePromise,
|
|
194
|
+
summaryLabel,
|
|
195
|
+
]);
|
|
196
|
+
const launch = useCallback(() => {
|
|
197
|
+
const request = requestRef.current;
|
|
198
|
+
if (busyRef.current || (request === null || request === void 0 ? void 0 : request.isShowing()))
|
|
199
|
+
return true;
|
|
200
|
+
if (!request)
|
|
201
|
+
return false;
|
|
202
|
+
callbacksRef.current.onError(null);
|
|
203
|
+
try {
|
|
204
|
+
busyRef.current = true;
|
|
205
|
+
setBusy(true);
|
|
206
|
+
request.show();
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
busyRef.current = false;
|
|
211
|
+
setBusy(false);
|
|
212
|
+
callbacksRef.current.onError(error instanceof Error && error.message
|
|
213
|
+
? error.message
|
|
214
|
+
: 'Unable to open wallet checkout.');
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
}, []);
|
|
218
|
+
const available = !session.configured ? null
|
|
219
|
+
: !session.stripePromise ? false
|
|
220
|
+
: availability.requestKey === requestKey
|
|
221
|
+
&& availability.stripePromise === session.stripePromise
|
|
222
|
+
? availability.value
|
|
223
|
+
: null;
|
|
224
|
+
return {
|
|
225
|
+
available,
|
|
226
|
+
busy,
|
|
227
|
+
launch,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { FunnelSdkHttpError } from '@funnelsgrove/runtime';
|
|
2
|
+
const readRetryAfterMs = (retryAfter) => {
|
|
3
|
+
const value = retryAfter === null || retryAfter === void 0 ? void 0 : retryAfter.trim();
|
|
4
|
+
if (!value) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (/^\d+$/.test(value)) {
|
|
8
|
+
return Number(value) * 1000;
|
|
9
|
+
}
|
|
10
|
+
const date = Date.parse(value);
|
|
11
|
+
return Number.isFinite(date) ? Math.max(0, date - Date.now()) : null;
|
|
12
|
+
};
|
|
2
13
|
export const describeStripeCheckoutFailure = (error) => {
|
|
3
14
|
const errorCode = error instanceof FunnelSdkHttpError
|
|
4
15
|
? error.code || undefined
|
|
@@ -14,8 +25,11 @@ export const describeStripeCheckoutFailure = (error) => {
|
|
|
14
25
|
: rawMessage === 'Internal server error'
|
|
15
26
|
? 'Unable to initialize checkout. Stripe settings may be missing for this funnel.'
|
|
16
27
|
: rawMessage;
|
|
17
|
-
|
|
28
|
+
const retryAfterMs = error instanceof FunnelSdkHttpError
|
|
29
|
+
? readRetryAfterMs(error.retryAfter)
|
|
30
|
+
: null;
|
|
31
|
+
return Object.assign(Object.assign(Object.assign({}, (error instanceof FunnelSdkHttpError && error.checkoutRequestId
|
|
18
32
|
? { checkoutRequestId: error.checkoutRequestId } : {})), { errorCode,
|
|
19
33
|
message,
|
|
20
|
-
providerErrorCode });
|
|
34
|
+
providerErrorCode }), (retryAfterMs !== null ? { retryAfterMs } : {}));
|
|
21
35
|
};
|