@funnelsgrove/payments 0.30.0 → 0.32.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.
Files changed (21) hide show
  1. package/README.md +13 -0
  2. package/dist/generated/package-version.d.ts +1 -1
  3. package/dist/generated/package-version.js +1 -1
  4. package/dist/providers/solidgate/components/SolidgateCheckoutDialog.js +15 -9
  5. package/dist/providers/solidgate/components/SolidgateCheckoutDialog.styles.d.ts +1 -1
  6. package/dist/providers/solidgate/components/SolidgateCheckoutDialog.styles.js +3 -0
  7. package/dist/providers/solidgate/components/SolidgateCheckoutDialog.types.d.ts +3 -1
  8. package/dist/providers/stripe/components/StripeSubscriptionWalletFirstSurface.d.ts +15 -0
  9. package/dist/providers/stripe/components/StripeSubscriptionWalletFirstSurface.js +50 -0
  10. package/dist/providers/stripe/components/StripeSubscriptionWalletSurface.d.ts +3 -1
  11. package/dist/providers/stripe/components/StripeSubscriptionWalletSurface.js +6 -1
  12. package/dist/providers/stripe/hooks/checkoutPreparationBackoff.d.ts +14 -0
  13. package/dist/providers/stripe/hooks/checkoutPreparationBackoff.js +17 -0
  14. package/dist/providers/stripe/hooks/useStripeOneTimeCheckoutSession.js +25 -3
  15. package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.d.ts +1 -0
  16. package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.js +38 -7
  17. package/dist/providers/stripe/hooks/useStripeSubscriptionWalletFirstRequest.d.ts +26 -0
  18. package/dist/providers/stripe/hooks/useStripeSubscriptionWalletFirstRequest.js +229 -0
  19. package/dist/providers/stripe/services/stripeCheckoutFailure.d.ts +1 -0
  20. package/dist/providers/stripe/services/stripeCheckoutFailure.js +16 -2
  21. package/package.json +5 -5
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.30.0";
1
+ export declare const PAYMENTS_PACKAGE_VERSION = "0.32.0";
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/generate-package-version.mjs.
2
- export const PAYMENTS_PACKAGE_VERSION = "0.30.0";
2
+ export const PAYMENTS_PACKAGE_VERSION = "0.32.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({ state: 'recoverable_error', checkout: null, canResumeVerification: false, failureMessage });
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 { checkout, surfaceId } = _a, surfaceProps = __rest(_a, ["checkout", "surfaceId"]);
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(Object.assign({}, resolveStripeOneTimeCheckoutPreparationOptions(source, options)));
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 && ((_a = creatingIntentRef.current) === null || _a === void 0 ? void 0 : _a.key) === intentKey) {
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 = (_b = attemptRef.current) === null || _b === void 0 ? void 0 : _b.id;
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(Object.assign({}, resolveStripeSubscriptionCheckoutPreparationOptions(source, options)));
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
+ }
@@ -3,5 +3,6 @@ export type StripeCheckoutFailure = {
3
3
  errorCode?: string;
4
4
  message: string;
5
5
  providerErrorCode?: string;
6
+ retryAfterMs?: number;
6
7
  };
7
8
  export declare const describeStripeCheckoutFailure: (error: unknown) => StripeCheckoutFailure;
@@ -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
- return Object.assign(Object.assign({}, (error instanceof FunnelSdkHttpError && error.checkoutRequestId
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.30.0",
3
+ "version": "0.32.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
@@ -46,11 +46,11 @@
46
46
  "jsdom": "20.0.3",
47
47
  "typescript": "^5",
48
48
  "vitest": "^3.2.4",
49
- "@funnelsgrove/runtime": "0.26.0",
50
- "@funnelsgrove/analytics": "0.8.0"
49
+ "@funnelsgrove/runtime": "0.27.0",
50
+ "@funnelsgrove/analytics": "0.9.0"
51
51
  },
52
52
  "peerDependencies": {
53
- "@funnelsgrove/runtime": "^0.26.0",
54
- "@funnelsgrove/analytics": "^0.8.0"
53
+ "@funnelsgrove/runtime": "^0.27.0",
54
+ "@funnelsgrove/analytics": "^0.9.0"
55
55
  }
56
56
  }