@funnelsgrove/payments 0.23.0 → 0.23.2

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 CHANGED
@@ -68,6 +68,23 @@ unchanged. Set `errorMessagePolicy: 'content'` to render load/cancel failures fr
68
68
  the content's `errorMessages`. The default `'server'` policy preserves existing
69
69
  error rendering independently of the selected labels.
70
70
 
71
+ ## Checkout email prompt
72
+
73
+ When the access email is missing or invalid, wallet surfaces render nothing.
74
+ The funnel's primary payment CTA (such as Get My Plan) opens `CheckoutEmailDialog`
75
+ and saves the email through the existing caller-owned identity update. Once the
76
+ caller adopts the saved email, wallet controls can mount. Provider email rejections
77
+ still open the surface's repair dialog; saving never confirms the old session.
78
+
79
+ `CheckoutEmailDialog` defaults to a light surface regardless of the browser or
80
+ ancestor theme. It inherits the funnel font. Set inherited `--checkout-email-*`
81
+ CSS variables on the funnel root or `.checkout-email-dialog` to customize it:
82
+ `background`, `foreground`, `muted`, `border`, `input-background`,
83
+ `button-background`, `button-foreground`, `focus`, and `error`.
84
+ For example, `--checkout-email-button-background: var(--color-primary)` uses the
85
+ funnel accent. Set foreground/background pairs together for custom dark surfaces.
86
+ Existing `dialog.checkout-email-dialog` CSS overrides continue to work.
87
+
71
88
  ## Advanced Wallet Primitives
72
89
 
73
90
  `ApplePaySubscriptionCheckoutSlot`, `GooglePaySubscriptionCheckoutSlot`, and `WalletSubscriptionCheckoutSlot` stay exported and runtime-compatible. They are backward-compatible advanced low-level primitives: reach for them only when a placement needs wiring the shared wallet surfaces do not cover. Never share one checkout hook result across placements; give each visible placement its own session.
@@ -47,20 +47,19 @@ export function CheckoutEmailDialog({ customerEmail, rejectedEmail, onSave, onCl
47
47
  if (!busyRef.current)
48
48
  onClose();
49
49
  }, children: [_jsxs("form", { onSubmit: submit, noValidate: true, "aria-busy": busy, children: [_jsx("h2", { id: titleId, children: title }), _jsx("p", { children: description }), _jsxs("label", { children: [emailLabel, _jsx("input", { type: 'email', autoComplete: 'email', inputMode: 'email', autoFocus: true, className: 'bg-background text-foreground border-border', value: draft, placeholder: placeholder, disabled: busy, "aria-invalid": Boolean(error), "aria-describedby": error ? errorId : undefined, onChange: (event) => { setDraft(event.target.value); setError(null); } })] }), error ? _jsx("p", { id: errorId, role: 'alert', children: error }) : null, _jsxs("div", { className: 'checkout-email-dialog-actions', children: [_jsx("button", { type: 'button', className: 'bg-background text-foreground border-border', disabled: busy, onClick: onClose, children: cancelLabel }), _jsx("button", { type: 'submit', className: 'bg-accent text-foreground border-border', disabled: busy, children: busy ? 'Saving…' : continueLabel })] })] }), _jsx("style", { children: `
50
- .checkout-email-dialog { color-scheme: light dark; box-sizing: border-box; width: min(420px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); overflow-y: auto; margin: auto; padding: 24px; border: 1px solid GrayText; border-radius: 20px; background: Canvas; color: CanvasText; font: inherit; }
51
- .dark .checkout-email-dialog, [data-theme=dark] .checkout-email-dialog { color-scheme: dark; }
52
- .light .checkout-email-dialog, [data-theme=light] .checkout-email-dialog { color-scheme: light; }
53
- .checkout-email-dialog::backdrop { background: rgb(0 0 0 / 45%); }
54
- .checkout-email-dialog h2 { color: inherit; margin: 0 0 12px; font-size: 24px; line-height: 1.2; }
55
- .checkout-email-dialog p { margin: 0 0 20px; line-height: 1.5; }
56
- .checkout-email-dialog label { display: grid; gap: 8px; }
57
- .checkout-email-dialog input { box-sizing: border-box; width: 100%; min-height: 48px; padding: 12px; border: 1px solid GrayText; border-radius: 10px; background: Canvas; color: CanvasText; font: inherit; font-size: 16px; }
58
- .checkout-email-dialog input::placeholder { color: GrayText; }
59
- .checkout-email-dialog [role=alert] { margin-top: 12px; font-weight: 600; }
50
+ .checkout-email-dialog { color-scheme: light; box-sizing: border-box; width: min(420px, calc(100vw - 32px)); max-height: calc(100dvh - 32px); overflow-y: auto; margin: auto; padding: 24px; border: 1px solid var(--checkout-email-border, #e3e6ed); border-radius: 24px; background: var(--checkout-email-background, #fff); color: var(--checkout-email-foreground, #252936); box-shadow: 0 24px 64px rgb(0 0 0 / 18%), 0 4px 16px rgb(0 0 0 / 6%); font: inherit; }
51
+ .checkout-email-dialog::backdrop { background: rgb(0 0 0 / 35%); }
52
+ .checkout-email-dialog h2 { color: inherit; margin: 0 0 8px; font-size: 24px; line-height: 1.2; }
53
+ .checkout-email-dialog p { color: var(--checkout-email-muted, #626978); margin: 0 0 20px; line-height: 1.5; }
54
+ .checkout-email-dialog label { display: grid; gap: 8px; font-weight: 600; }
55
+ .checkout-email-dialog input { box-sizing: border-box; width: 100%; min-height: 48px; padding: 12px; border: 1px solid var(--checkout-email-border, #e3e6ed); border-radius: 12px; background: var(--checkout-email-input-background, #f7f8fb); color: inherit; font: inherit; font-size: 16px; }
56
+ .checkout-email-dialog input::placeholder { color: var(--checkout-email-muted, #626978); }
57
+ .checkout-email-dialog [role=alert] { margin-top: 12px; color: var(--checkout-email-error, #b42318); font-weight: 600; }
60
58
  .checkout-email-dialog-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 24px; }
61
- .checkout-email-dialog button { flex: 1; padding: 12px; min-height: 48px; border: 1px solid GrayText; border-radius: 10px; background: Canvas; color: CanvasText; font: inherit; cursor: pointer; }
62
- .checkout-email-dialog button[type=submit] { font-weight: 700; border: 2px solid CanvasText; }
59
+ .checkout-email-dialog button { flex: 1; padding: 12px; min-height: 48px; border: 1px solid var(--checkout-email-border, #e3e6ed); border-radius: 12px; background: var(--checkout-email-background, #fff); color: inherit; font: inherit; cursor: pointer; }
60
+ .checkout-email-dialog button[type=submit] { font-weight: 700; border-color: transparent; background: var(--checkout-email-button-background, #3156d3); color: var(--checkout-email-button-foreground, #fff); }
61
+ .checkout-email-dialog button:hover:not(:disabled) { filter: brightness(.96); }
63
62
  .checkout-email-dialog button:disabled { opacity: .6; cursor: wait; }
64
- .checkout-email-dialog :focus-visible { outline: 2px solid Highlight; outline-offset: 3px; }
63
+ .checkout-email-dialog :focus-visible { outline: 2px solid var(--checkout-email-focus, #3156d3); outline-offset: 3px; }
65
64
  ` })] }));
66
65
  }
@@ -59,6 +59,7 @@ export type StripeWalletSurfaceProps = {
59
59
  containerClassName?: string;
60
60
  customerEmail?: string | null;
61
61
  onCustomerEmailCommit?: CheckoutEmailDialogProps['onSave'];
62
+ /** @deprecated Resolve the email from the funnel's primary payment CTA instead. */
62
63
  onResolveCustomerEmail?: () => Promise<string | null>;
63
64
  emailDialogContent?: Pick<CheckoutEmailDialogProps, 'title' | 'description' | 'emailLabel' | 'placeholder' | 'invalidMessage' | 'continueLabel' | 'cancelLabel'>;
64
65
  customerName?: string | null;
@@ -84,4 +85,4 @@ export type StripeWalletSurfaceProps = {
84
85
  surfaceId: string;
85
86
  suspended?: boolean;
86
87
  };
87
- export declare function StripeWalletSurface({ amountCents, appearance, applePayOptions, applePayPlaceholderLabel, beforeConfirm, checkoutAnalytics, className, containerClassName, customerEmail, onCustomerEmailCommit, onResolveCustomerEmail, emailDialogContent, customerName, disabled, googlePayOptions, googlePayPlaceholderLabel, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, paymentMethods, renderHeader, resolveMethodClassName, resolveMethodWrapperClassName, returnUrl, session, slotClassName, summaryLabel, surfaceId, suspended, }: StripeWalletSurfaceProps): import("react/jsx-runtime").JSX.Element | null;
88
+ export declare function StripeWalletSurface({ amountCents, appearance, applePayOptions, applePayPlaceholderLabel, beforeConfirm, checkoutAnalytics, className, containerClassName, customerEmail, onCustomerEmailCommit, emailDialogContent, customerName, disabled, googlePayOptions, googlePayPlaceholderLabel, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, paymentMethods, renderHeader, resolveMethodClassName, resolveMethodWrapperClassName, returnUrl, session, slotClassName, summaryLabel, surfaceId, suspended, }: StripeWalletSurfaceProps): import("react/jsx-runtime").JSX.Element | null;
@@ -76,8 +76,8 @@ function buildAvailabilitySignature(availability) {
76
76
  .join(','),
77
77
  ].join('|');
78
78
  }
79
- export function StripeWalletSurface({ amountCents, appearance, applePayOptions, applePayPlaceholderLabel, beforeConfirm, checkoutAnalytics, className, containerClassName, customerEmail, onCustomerEmailCommit, onResolveCustomerEmail, emailDialogContent, customerName, disabled, googlePayOptions, googlePayPlaceholderLabel, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, paymentMethods, renderHeader, resolveMethodClassName, resolveMethodWrapperClassName, returnUrl, session, slotClassName, summaryLabel, surfaceId, suspended = false, }) {
80
- var _a, _b, _c;
79
+ export function StripeWalletSurface({ amountCents, appearance, applePayOptions, applePayPlaceholderLabel, beforeConfirm, checkoutAnalytics, className, containerClassName, customerEmail, onCustomerEmailCommit, emailDialogContent, customerName, disabled, googlePayOptions, googlePayPlaceholderLabel, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, paymentMethods, renderHeader, resolveMethodClassName, resolveMethodWrapperClassName, returnUrl, session, slotClassName, summaryLabel, surfaceId, suspended = false, }) {
80
+ var _a, _b;
81
81
  const [emailDialogOpen, setEmailDialogOpen] = useState(false);
82
82
  const [rejectedEmail, setRejectedEmail] = useState(null);
83
83
  const [savedEmail, setSavedEmail] = useState(null);
@@ -85,32 +85,10 @@ export function StripeWalletSurface({ amountCents, appearance, applePayOptions,
85
85
  const needsEmail = !isValidCheckoutEmail(normalizedEmail)
86
86
  || rejectedEmail === normalizedEmail
87
87
  || (savedEmail !== null && savedEmail !== normalizedEmail);
88
- const [resolvingEmail, setResolvingEmail] = useState(false);
89
88
  useEffect(() => {
90
89
  if (savedEmail === normalizedEmail)
91
90
  setSavedEmail(null);
92
91
  }, [normalizedEmail, savedEmail]);
93
- const openEmailDialog = async () => {
94
- if (resolvingEmail)
95
- return;
96
- if (!onCustomerEmailCommit) {
97
- await (onUnavailableClick === null || onUnavailableClick === void 0 ? void 0 : onUnavailableClick());
98
- return;
99
- }
100
- setResolvingEmail(true);
101
- try {
102
- const resolved = await (onResolveCustomerEmail === null || onResolveCustomerEmail === void 0 ? void 0 : onResolveCustomerEmail());
103
- if (isValidCheckoutEmail(resolved) && (resolved === null || resolved === void 0 ? void 0 : resolved.trim()) !== rejectedEmail)
104
- return;
105
- setEmailDialogOpen(true);
106
- }
107
- catch (_a) {
108
- setEmailDialogOpen(true);
109
- }
110
- finally {
111
- setResolvingEmail(false);
112
- }
113
- };
114
92
  const platformWalletPaymentMethods = usePlatformWalletPaymentMethods();
115
93
  const walletPaymentMethods = normalizeStripeWalletSurfaceMethods(paymentMethods !== null && paymentMethods !== void 0 ? paymentMethods : platformWalletPaymentMethods);
116
94
  const normalizedSurfaceId = surfaceId.trim();
@@ -192,14 +170,16 @@ export function StripeWalletSurface({ amountCents, appearance, applePayOptions,
192
170
  suspended,
193
171
  });
194
172
  if (needsEmail && session.configured && !suspended && walletPaymentMethods.length > 0) {
195
- return (_jsxs("div", { className: containerClassName, "data-wallet-surface-id": normalizedSurfaceId, children: [_jsx("button", { type: 'button', className: ['bg-card text-foreground border-border', className].filter(Boolean).join(' '), style: { minHeight: 55, width: '100%', borderRadius: 12, font: 'inherit', cursor: 'pointer' }, disabled: disabled || resolvingEmail, onClick: () => void openEmailDialog(), children: (_b = emailDialogContent === null || emailDialogContent === void 0 ? void 0 : emailDialogContent.continueLabel) !== null && _b !== void 0 ? _b : 'Continue' }), emailDialogOpen && onCustomerEmailCommit ? (_jsx(CheckoutEmailDialog, Object.assign({}, emailDialogContent, { customerEmail: normalizedEmail, rejectedEmail: rejectedEmail, onClose: () => setEmailDialogOpen(false), onSave: async (email) => {
196
- const saved = await onCustomerEmailCommit(email);
197
- const nextEmail = normalizeCheckoutEmailResult(saved, email, emailDialogContent === null || emailDialogContent === void 0 ? void 0 : emailDialogContent.invalidMessage);
198
- setSavedEmail(nextEmail);
199
- setRejectedEmail(null);
200
- session.setError(null);
201
- return nextEmail;
202
- } }))) : null] }));
173
+ if (!emailDialogOpen || !onCustomerEmailCommit)
174
+ return null;
175
+ return (_jsx("div", { className: containerClassName, "data-wallet-surface-id": normalizedSurfaceId, children: _jsx(CheckoutEmailDialog, Object.assign({}, emailDialogContent, { customerEmail: normalizedEmail, rejectedEmail: rejectedEmail, onClose: () => setEmailDialogOpen(false), onSave: async (email) => {
176
+ const saved = await onCustomerEmailCommit(email);
177
+ const nextEmail = normalizeCheckoutEmailResult(saved, email, emailDialogContent === null || emailDialogContent === void 0 ? void 0 : emailDialogContent.invalidMessage);
178
+ setSavedEmail(nextEmail);
179
+ setRejectedEmail(null);
180
+ session.setError(null);
181
+ return nextEmail;
182
+ } })) }));
203
183
  }
204
184
  if (!shouldRenderSurface) {
205
185
  return null;
@@ -209,7 +189,7 @@ export function StripeWalletSurface({ amountCents, appearance, applePayOptions,
209
189
  containerClassName !== null && containerClassName !== void 0 ? containerClassName : '',
210
190
  ]
211
191
  .filter(Boolean)
212
- .join(' '), "data-wallet-surface-id": normalizedSurfaceId, children: [(_c = renderHeader === null || renderHeader === void 0 ? void 0 : renderHeader({ availability })) !== null && _c !== void 0 ? _c : null, visibleWalletPaymentMethods.map((method) => {
192
+ .join(' '), "data-wallet-surface-id": normalizedSurfaceId, children: [(_b = renderHeader === null || renderHeader === void 0 ? void 0 : renderHeader({ availability })) !== null && _b !== void 0 ? _b : null, visibleWalletPaymentMethods.map((method) => {
213
193
  var _a;
214
194
  return (_jsx("div", { className: [
215
195
  'stripe-subscription-wallet-surface__method',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.23.0",
3
+ "version": "0.23.2",
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.21.0",
50
- "@funnelsgrove/analytics": "0.4.0"
49
+ "@funnelsgrove/runtime": "0.21.1",
50
+ "@funnelsgrove/analytics": "0.4.1"
51
51
  },
52
52
  "peerDependencies": {
53
- "@funnelsgrove/runtime": "^0.21.0",
54
- "@funnelsgrove/analytics": "^0.4.0"
53
+ "@funnelsgrove/runtime": "^0.21.1",
54
+ "@funnelsgrove/analytics": "^0.4.1"
55
55
  }
56
56
  }