@funnelsgrove/payments 0.37.0 → 0.38.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 CHANGED
@@ -28,6 +28,28 @@ Use these for new subscription paywalls:
28
28
  - `trackPaidStripeSubscriptionCheckoutCompleted`
29
29
  - `trackStripeSubscriptionPaymentInfoSubmitted`
30
30
 
31
+ For an inline subscription form with the device wallet first and card second, use
32
+ `StripeWalletFirstCheckout`. It shows one wallet, Google Pay on Android and Apple Pay
33
+ elsewhere, preselected, with Stripe's own button, and the card form as the second method.
34
+
35
+ - Pass the card hook's `clientSecret`, `checkoutSessionId` and `stripePromise`. While the
36
+ Checkout Session is being created, pass `clientSecret={null}`: the full layout renders
37
+ and the wallet slot holds a progress bar at the button's height, so nothing moves when
38
+ Stripe's button arrives.
39
+ - Also pass `amountCents` and `summaryLabel` for the wallet sheet, `customerEmail` (set on
40
+ the session before card confirmation, since the form has no email field), `returnUrl`,
41
+ `checkoutAnalytics`, a complete `StripeWalletFirstCheckoutCopy` from the funnel's
42
+ content, and `themeColor`/`buttonColor`.
43
+ - The card form has no ZIP or terms text.
44
+ - Where the browser cannot pay with that wallet, the wallet stays first and its slot shows
45
+ `walletUnavailableNote` (`{wallet}` is replaced with the wallet name). The card remains
46
+ one tap away. This is deliberate: the layout must not move.
47
+ - The component owns `checkout_started` (when the card form opens, or on wallet intent),
48
+ `add_payment_info` and verified `checkout_completed`.
49
+ - After confirmation, the payment status stays in the form until the caller replaces it.
50
+ With the runtime's embedded completion, the success screen takes over. Otherwise
51
+ `onSuccess` runs, and then the page navigates to `returnUrl`.
52
+
31
53
  Use these for new one-time paywall plans:
32
54
 
33
55
  - `StripeOneTimeWalletSurface`
@@ -1 +1 @@
1
- export declare const PAYMENTS_PACKAGE_VERSION = "0.37.0";
1
+ export declare const PAYMENTS_PACKAGE_VERSION = "0.38.0";
@@ -1,2 +1,2 @@
1
1
  // Generated by scripts/generate-package-version.mjs.
2
- export const PAYMENTS_PACKAGE_VERSION = "0.37.0";
2
+ export const PAYMENTS_PACKAGE_VERSION = "0.38.0";
@@ -0,0 +1,36 @@
1
+ import type { Stripe } from '@stripe/stripe-js';
2
+ import { type PlatformWalletPaymentMethod } from './walletPlatform.js';
3
+ import { type StripeSubscriptionCheckoutAnalyticsContext } from '../services/checkoutCompletionAnalytics.service.js';
4
+ export type StripeWalletFirstCheckoutCopy = {
5
+ /** Benefit bullets under the wallet header, per wallet. */
6
+ walletBenefits: Record<PlatformWalletPaymentMethod, readonly string[]>;
7
+ walletLabels: Record<PlatformWalletPaymentMethod, string>;
8
+ cardLabel: string;
9
+ cardSubmitLabel: string;
10
+ processingLabel: string;
11
+ secureNote: string;
12
+ /** Shown in the wallet slot when this browser cannot pay with the wallet; `{wallet}` is the wallet name. */
13
+ walletUnavailableNote: string;
14
+ verifyingPayment: string;
15
+ paymentConfirmed: string;
16
+ paymentFailed: string;
17
+ formNotReady: string;
18
+ };
19
+ export type StripeWalletFirstCheckoutProps = {
20
+ amountCents: number;
21
+ buttonColor: string;
22
+ checkoutAnalytics: StripeSubscriptionCheckoutAnalyticsContext | null;
23
+ checkoutSessionId: string | null;
24
+ /** Null while the checkout session is being created; the layout shows progress meanwhile. */
25
+ clientSecret: string | null;
26
+ copy: StripeWalletFirstCheckoutCopy;
27
+ customerEmail: string;
28
+ customerName?: string | null;
29
+ onError?: (message: string | null) => void;
30
+ onSuccess?: () => void | Promise<void>;
31
+ returnUrl: string;
32
+ stripePromise: Promise<Stripe | null> | null;
33
+ summaryLabel: string;
34
+ themeColor: string;
35
+ };
36
+ export declare function StripeWalletFirstCheckout(props: StripeWalletFirstCheckoutProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,190 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * Wallet-first inline Stripe checkout (Checkout Sessions).
5
+ *
6
+ * One platform wallet is the first, preselected method: Google Pay on Android,
7
+ * Apple Pay everywhere else, shown with Stripe's own wallet button. Card
8
+ * payment is always the second method. The whole layout renders immediately,
9
+ * even before the checkout session exists; the wallet slot holds a progress
10
+ * bar at the exact button height until Stripe's button replaces it, so nothing
11
+ * moves. Where the wallet cannot pay, the same slot explains that. The card
12
+ * form has no email, ZIP or terms text: the session carries the email.
13
+ *
14
+ * The caller owns the subscription Checkout Session (`clientSecret`), the copy
15
+ * and what happens after `onSuccess`.
16
+ */
17
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
18
+ import { CheckoutProvider, PaymentElement, useCheckout } from '@stripe/react-stripe-js/checkout';
19
+ import { completePaywallCheckout } from '@funnelsgrove/runtime';
20
+ import { StripeCheckoutExpressCheckoutButton } from './StripeCheckoutExpressCheckoutButton.js';
21
+ import { resolveWalletPlatform } from './walletPlatform.js';
22
+ import { stripeWalletFirstCheckoutStyles } from './StripeWalletFirstCheckout.styles.js';
23
+ import { queueStripeSubscriptionCheckoutStarted, trackPaidStripeSubscriptionCheckoutCompleted, trackStripeSubscriptionPaymentInfoSubmitted, } from '../services/checkoutCompletionAnalytics.service.js';
24
+ import { trackCheckoutFailureShown } from '../../../services/checkoutObservability.service.js';
25
+ const appearance = {
26
+ labels: 'above',
27
+ variables: {
28
+ borderRadius: '12px',
29
+ colorBackground: '#fff',
30
+ colorText: '#303136',
31
+ colorTextPlaceholder: '#8c909a',
32
+ spacingUnit: '4px',
33
+ },
34
+ };
35
+ export function StripeWalletFirstCheckout(props) {
36
+ const { buttonColor, checkoutAnalytics, checkoutSessionId, clientSecret, copy, stripePromise, themeColor } = props;
37
+ const wallet = usePlatformWallet();
38
+ const [selected, setSelected] = useState('wallet');
39
+ const style = { '--wfc-theme': themeColor, '--wfc-button': buttonColor };
40
+ const options = useMemo(() => clientSecret ? {
41
+ clientSecret,
42
+ elementsOptions: { appearance: Object.assign(Object.assign({}, appearance), { variables: Object.assign(Object.assign({}, appearance.variables), { colorPrimary: themeColor }) }) },
43
+ } : null, [clientSecret, themeColor]);
44
+ // Session creation is preparation; the card checkout starts when the visitor opens the card form.
45
+ useEffect(() => {
46
+ if (!checkoutAnalytics || !clientSecret || selected !== 'card')
47
+ return;
48
+ queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, checkoutStartSource: 'card_form_open', paymentMethod: 'card' }));
49
+ }, [checkoutAnalytics, checkoutSessionId, clientSecret, selected]);
50
+ return (_jsxs(_Fragment, { children: [options && stripePromise ? (_jsx(CheckoutProvider, { stripe: stripePromise, options: options, children: _jsx(StripeWalletFirstCheckoutForm, Object.assign({}, props, { wallet: wallet, selected: selected, onSelect: setSelected, style: style })) }, options.clientSecret)) : (_jsx(CheckoutLayout, { copy: copy, wallet: wallet, selected: selected, onSelect: setSelected, disabled: false, style: style, walletSlot: _jsx("span", { className: 'wfc-progress', role: 'status', "aria-label": copy.processingLabel }), cardBody: _jsx("span", { className: 'wfc-progress wfc-progress--form', "aria-hidden": 'true' }) })), _jsx("style", { children: stripeWalletFirstCheckoutStyles })] }));
51
+ }
52
+ function StripeWalletFirstCheckoutForm({ amountCents, checkoutAnalytics, checkoutSessionId, copy, customerEmail, customerName, onError, onSuccess, returnUrl, summaryLabel, wallet, selected, onSelect, style, }) {
53
+ const checkoutState = useCheckout();
54
+ // null until Stripe answers; the slot shows progress at the button's height meanwhile.
55
+ const [walletAvailable, setWalletAvailable] = useState(null);
56
+ const [walletConfirming, setWalletConfirming] = useState(false);
57
+ const [paymentElementReady, setPaymentElementReady] = useState(false);
58
+ const [paymentElementComplete, setPaymentElementComplete] = useState(false);
59
+ const [submitting, setSubmitting] = useState(false);
60
+ const [providerConfirmed, setProviderConfirmed] = useState(false);
61
+ const [completed, setCompleted] = useState(false);
62
+ const [error, setError] = useState(null);
63
+ const submitInFlight = useRef(false);
64
+ const expressOptions = useMemo(() => ({
65
+ buttonHeight: 55,
66
+ buttonTheme: { applePay: 'black', googlePay: 'black' },
67
+ buttonType: { applePay: 'plain', googlePay: 'plain' },
68
+ layout: { maxColumns: 1, maxRows: 1, overflow: 'auto' },
69
+ paymentMethods: {
70
+ amazonPay: 'never', klarna: 'never', link: 'never', paypal: 'never',
71
+ applePay: wallet === 'applePay' ? 'always' : 'never',
72
+ googlePay: wallet === 'googlePay' ? 'always' : 'never',
73
+ },
74
+ }), [wallet]);
75
+ const paymentElementOptions = useMemo(() => ({
76
+ fields: { billingDetails: { name: 'never', email: 'never', address: { country: 'auto', postalCode: 'never' } } },
77
+ layout: 'tabs',
78
+ terms: { card: 'never' },
79
+ wallets: { applePay: 'never', googlePay: 'never', link: 'never' },
80
+ }), []);
81
+ // A hidden Payment Element measures 0px wide; let Stripe re-layout when it opens.
82
+ useEffect(() => {
83
+ if (selected !== 'card')
84
+ return;
85
+ const timer = window.setTimeout(() => window.dispatchEvent(new Event('resize')), 0);
86
+ return () => window.clearTimeout(timer);
87
+ }, [selected]);
88
+ const showError = (message, providerError, alreadyTracked = false) => {
89
+ if (message && checkoutAnalytics && !alreadyTracked) {
90
+ trackCheckoutFailureShown({
91
+ analytics: checkoutAnalytics, checkoutSessionId, errorMessage: message,
92
+ failureStage: 'card_form', paymentMethod: 'card', provider: 'stripe',
93
+ providerDeclineCode: providerError === null || providerError === void 0 ? void 0 : providerError.decline_code, providerErrorCode: providerError === null || providerError === void 0 ? void 0 : providerError.code, providerErrorType: providerError === null || providerError === void 0 ? void 0 : providerError.type,
94
+ });
95
+ }
96
+ setError(message);
97
+ onError === null || onError === void 0 ? void 0 : onError(message);
98
+ };
99
+ const trackSuccess = async (embedded = false) => {
100
+ if (checkoutAnalytics)
101
+ await trackPaidStripeSubscriptionCheckoutCompleted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId }));
102
+ if (!embedded)
103
+ await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess());
104
+ };
105
+ const finishPaidCheckout = async () => {
106
+ if (await completePaywallCheckout('stripe', checkoutSessionId || '', {
107
+ beforeNotify: async () => { setCompleted(true); await trackSuccess(true); },
108
+ }))
109
+ return;
110
+ await trackSuccess();
111
+ window.location.assign(returnUrl);
112
+ };
113
+ const submitCard = async (event) => {
114
+ event.preventDefault();
115
+ if (submitInFlight.current)
116
+ return;
117
+ if (!providerConfirmed) {
118
+ if (checkoutState.type !== 'success') {
119
+ showError(checkoutState.type === 'error' ? checkoutState.error.message : copy.formNotReady);
120
+ return;
121
+ }
122
+ if (!paymentElementReady) {
123
+ showError(copy.formNotReady, undefined, true);
124
+ return;
125
+ }
126
+ }
127
+ submitInFlight.current = true;
128
+ setSubmitting(true);
129
+ showError(null);
130
+ let confirmed = providerConfirmed;
131
+ try {
132
+ if (!providerConfirmed && checkoutState.type === 'success') {
133
+ const { checkout } = checkoutState;
134
+ if (!checkout.email && customerEmail) {
135
+ const updated = await checkout.updateEmail(customerEmail);
136
+ if (updated.type === 'error')
137
+ throw new Error(updated.error.message);
138
+ }
139
+ if (paymentElementComplete && checkoutAnalytics) {
140
+ await trackStripeSubscriptionPaymentInfoSubmitted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId })).catch(() => 'analytics_rejected');
141
+ }
142
+ const result = await checkout.confirm({ redirect: 'if_required' });
143
+ if (result.type === 'error') {
144
+ showError(result.error.message || copy.paymentFailed, result.error);
145
+ submitInFlight.current = false;
146
+ setSubmitting(false);
147
+ return;
148
+ }
149
+ confirmed = true;
150
+ setProviderConfirmed(true);
151
+ }
152
+ await finishPaidCheckout();
153
+ }
154
+ catch (submitError) {
155
+ showError(submitError instanceof Error && submitError.message ? submitError.message : copy.paymentFailed, undefined, confirmed);
156
+ submitInFlight.current = false;
157
+ setSubmitting(false);
158
+ }
159
+ };
160
+ // After a successful confirmation the form stays in place and reports progress
161
+ // where the customer paid, until the success screen takes over.
162
+ const paymentStatus = completed ? copy.paymentConfirmed : providerConfirmed ? copy.verifyingPayment : null;
163
+ const locked = Boolean(paymentStatus) && !error;
164
+ return (_jsx(CheckoutLayout, { copy: copy, wallet: wallet, selected: selected, onSelect: onSelect, disabled: submitting || walletConfirming || locked, style: style, walletNote: selected === 'wallet' ? paymentStatus : null, walletSlot: _jsxs(_Fragment, { children: [_jsx("div", { className: 'wfc-wallet-button', "data-ready": walletAvailable === true, children: _jsx(StripeCheckoutExpressCheckoutButton, { amountCents: amountCents, availabilityPaymentMethods: [wallet], beforeConfirm: async () => { setWalletConfirming(true); return customerEmail || null; }, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, confirmEmail: false, customerEmail: customerEmail, customerName: customerName, onAvailabilityChange: setWalletAvailable, onCancel: () => setWalletConfirming(false), onEmbeddedComplete: () => setCompleted(true), onError: message => { setWalletConfirming(false); showError(message, undefined, true); }, onPaymentRejected: () => { setWalletConfirming(false); onSelect('card'); }, onSuccess: () => trackSuccess(), onVerificationPending: () => setProviderConfirmed(true), options: expressOptions, returnUrl: returnUrl, summaryLabel: summaryLabel }) }), walletAvailable === null ? _jsx("span", { className: 'wfc-progress wfc-progress--overlay', role: 'status', "aria-label": copy.processingLabel }) : null, walletAvailable === false ? _jsx("p", { className: 'wfc-wallet-unavailable', role: 'status', children: copy.walletUnavailableNote.replace('{wallet}', copy.walletLabels[wallet]) }) : null] }), cardBody: _jsxs("form", { className: 'wfc-card-form', onSubmit: event => void submitCard(event), children: [_jsx(PaymentElement, { options: paymentElementOptions, onChange: event => setPaymentElementComplete(event.complete), onReady: () => setPaymentElementReady(true) }), _jsx("button", { type: 'submit', className: 'wfc-submit', disabled: locked || submitting || !paymentElementReady, "aria-live": 'polite', children: locked && paymentStatus ? paymentStatus : submitting ? copy.processingLabel : copy.cardSubmitLabel }), _jsx("p", { className: 'wfc-secure', children: copy.secureNote })] }), footer: error ? _jsx("p", { className: 'wfc-error', role: 'alert', children: error }) : null }));
165
+ }
166
+ /** Shared by the loading and live states so switching between them moves nothing. */
167
+ function CheckoutLayout({ cardBody, copy, disabled, footer, onSelect, selected, style, wallet, walletNote, walletSlot }) {
168
+ const WalletMark = wallet === 'applePay' ? ApplePayMark : GooglePayMark;
169
+ return (_jsxs("div", { className: 'wfc', style: style, children: [_jsxs(MethodCard, { checked: selected === 'wallet', disabled: disabled, label: copy.walletLabels[wallet], onSelect: () => onSelect('wallet'), header: _jsx(WalletMark, {}), children: [_jsx("ul", { className: 'wfc-benefits', children: copy.walletBenefits[wallet].map(item => _jsxs("li", { children: [_jsx(CheckIcon, {}), item] }, item)) }), _jsx("div", { className: 'wfc-wallet-slot', children: walletSlot }), _jsx("p", { className: walletNote ? 'wfc-secure is-status' : 'wfc-secure', role: walletNote ? 'status' : undefined, children: walletNote || copy.secureNote })] }), _jsx(MethodCard, { checked: selected === 'card', disabled: disabled, label: copy.cardLabel, onSelect: () => onSelect('card'), header: _jsx(CardBrandMarks, {}), children: cardBody }), footer] }));
170
+ }
171
+ function MethodCard({ checked, children, disabled, header, label, onSelect }) {
172
+ return (_jsxs("section", { className: 'wfc-method', "data-checked": checked, children: [_jsxs("button", { type: 'button', role: 'radio', "aria-checked": checked, "aria-label": label, className: 'wfc-method-head', disabled: disabled, onClick: onSelect, children: [_jsx("span", { className: 'wfc-radio', "aria-hidden": 'true' }), header] }), _jsx("div", { className: 'wfc-method-body', "data-open": checked, inert: !checked, "aria-hidden": !checked, children: _jsx("div", { className: 'wfc-method-clip', children: _jsx("div", { className: 'wfc-method-content', children: children }) }) })] }));
173
+ }
174
+ function CheckIcon() {
175
+ return _jsx("svg", { className: 'wfc-check', viewBox: '0 0 16 16', "aria-hidden": 'true', children: _jsx("path", { d: 'M3 8.5 6.5 12 13 4.5', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round' }) });
176
+ }
177
+ function ApplePayMark() {
178
+ return (_jsxs("svg", { className: 'wfc-mark wfc-mark--apple', viewBox: '0 0 64 26', role: 'img', "aria-label": 'Apple Pay', children: [_jsx("path", { d: 'M14.6 13.6c0-2.6 2.1-3.8 2.2-3.9-1.2-1.8-3.1-2-3.7-2-1.6-.2-3.1.9-3.9.9-.8 0-2-.9-3.4-.9-1.7 0-3.3 1-4.2 2.6-1.8 3.1-.5 7.7 1.3 10.2.9 1.2 1.9 2.6 3.2 2.6 1.3-.1 1.8-.8 3.3-.8 1.6 0 2 .8 3.4.8s2.3-1.3 3.1-2.5c1-1.4 1.4-2.8 1.4-2.9 0 0-2.7-1-2.7-4.1ZM12.1 6c.7-.9 1.2-2 1-3.2-1 0-2.3.7-3 1.6-.7.8-1.3 2-1.1 3.1 1.1.1 2.3-.6 3.1-1.5Z', fill: 'currentColor' }), _jsx("text", { x: '22', y: '20', fontSize: '17', fontWeight: '600', fill: 'currentColor', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif', children: "Pay" })] }));
179
+ }
180
+ function GooglePayMark() {
181
+ return (_jsxs("svg", { className: 'wfc-mark wfc-mark--google', viewBox: '0 0 64 26', role: 'img', "aria-label": 'Google Pay', children: [_jsx("path", { d: 'M19.6 13.2c0-.6-.1-1.2-.2-1.8h-8.6v3.4h4.9a4.2 4.2 0 0 1-1.8 2.8v2.3h2.9c1.7-1.6 2.8-3.9 2.8-6.7Z', fill: '#4285F4' }), _jsx("path", { d: 'M10.8 22c2.4 0 4.5-.8 6-2.2l-2.9-2.3c-.8.6-1.9.9-3.1.9-2.4 0-4.4-1.6-5.1-3.7h-3v2.3A9 9 0 0 0 10.8 22Z', fill: '#34A853' }), _jsx("path", { d: 'M5.7 14.7a5.4 5.4 0 0 1 0-3.4V9H2.7a9 9 0 0 0 0 8l3-2.3Z', fill: '#FBBC04' }), _jsx("path", { d: 'M10.8 7.6c1.3 0 2.5.5 3.5 1.4l2.6-2.6A9 9 0 0 0 2.7 9l3 2.3c.7-2.1 2.7-3.7 5.1-3.7Z', fill: '#EA4335' }), _jsx("text", { x: '23', y: '20', fontSize: '17', fontWeight: '500', fill: '#3c4043', fontFamily: '"Google Sans", Roboto, -apple-system, sans-serif', children: "Pay" })] }));
182
+ }
183
+ function CardBrandMarks() {
184
+ return (_jsxs("span", { className: 'wfc-card-marks', "aria-hidden": 'true', children: [_jsx("svg", { viewBox: '0 0 48 30', className: 'wfc-brand', children: _jsx("text", { x: '24', y: '21', textAnchor: 'middle', fontSize: '17', fontWeight: '800', fontStyle: 'italic', fill: '#1a1f71', fontFamily: 'Arial, sans-serif', children: "VISA" }) }), _jsxs("svg", { viewBox: '0 0 48 30', className: 'wfc-brand', children: [_jsx("circle", { cx: '19', cy: '15', r: '10', fill: '#eb001b' }), _jsx("circle", { cx: '29', cy: '15', r: '10', fill: '#f79e1b', fillOpacity: '.9' })] }), _jsxs("svg", { viewBox: '0 0 48 30', className: 'wfc-brand wfc-brand--box', style: { background: '#1b4fa0' }, children: [_jsx("circle", { cx: '19', cy: '15', r: '9', fill: '#0099df' }), _jsx("circle", { cx: '29', cy: '15', r: '9', fill: '#e60014', fillOpacity: '.85' })] }), _jsx("svg", { viewBox: '0 0 48 30', className: 'wfc-brand wfc-brand--box', style: { background: '#2e77bc' }, children: _jsx("text", { x: '24', y: '19', textAnchor: 'middle', fontSize: '10', fontWeight: '800', fill: '#fff', fontFamily: 'Arial, sans-serif', children: "AMEX" }) })] }));
185
+ }
186
+ /** Google Pay on Android, Apple Pay everywhere else — read synchronously from the browser. */
187
+ function usePlatformWallet() {
188
+ const platform = useSyncExternalStore(() => () => undefined, () => resolveWalletPlatform({ maxTouchPoints: navigator.maxTouchPoints, userAgent: navigator.userAgent }), () => 'ios');
189
+ return platform === 'android' ? 'googlePay' : 'applePay';
190
+ }
@@ -0,0 +1,2 @@
1
+ /** Styles for StripeWalletFirstCheckout; colors come from its `themeColor` and `buttonColor` props. */
2
+ export declare const stripeWalletFirstCheckoutStyles = "\n.wfc { display:grid; gap:12px; }\n/* The radio marks the choice; every card keeps the same quiet border. */\n.wfc-method { border-radius:16px; background:#fff; box-shadow:0 0 0 1.5px #e6e6ec; }\n.wfc-method-head { display:flex; align-items:center; gap:14px; width:100%; min-height:60px; padding:14px 16px; border:0; background:transparent; color:inherit; text-align:left; cursor:pointer; border-radius:16px; }\n.wfc-method-head:disabled { cursor:default; }\n.wfc-method-head:focus-visible { outline:3px solid var(--wfc-theme); outline-offset:2px; }\n.wfc-radio { flex:none; width:24px; height:24px; border-radius:50%; border:2px solid #b8bac4; background:#fff; transition:border-color .2s ease, box-shadow .2s ease; }\n.wfc-method[data-checked='true'] .wfc-radio { border-color:var(--wfc-theme); box-shadow:inset 0 0 0 4px #fff, inset 0 0 0 12px var(--wfc-theme); }\n.wfc-method-body { display:grid; grid-template-rows:0fr; opacity:0; transition:grid-template-rows .34s cubic-bezier(.25,.8,.25,1), opacity .22s ease; }\n.wfc-method-body[data-open='true'] { grid-template-rows:1fr; opacity:1; transition:grid-template-rows .34s cubic-bezier(.25,.8,.25,1), opacity .28s ease .08s; }\n.wfc-method-clip { min-height:0; overflow:hidden; }\n.wfc-method-content { padding:0 16px 16px; }\n@media (prefers-reduced-motion: reduce) { .wfc-method-body, .wfc-method-body[data-open='true'] { transition:none; } }\n.wfc-mark { height:30px; width:auto; color:#000; }\n.wfc-card-marks { display:flex; gap:6px; align-items:center; }\n.wfc-brand { width:40px; height:26px; border-radius:4px; }\n.wfc-benefits { margin:0 0 16px; padding:0; list-style:none; display:grid; gap:10px; font-size:15px; line-height:1.4; color:#303136; }\n.wfc-benefits li { display:flex; gap:10px; align-items:flex-start; }\n.wfc-check { flex:none; width:16px; height:16px; margin-top:2px; color:var(--wfc-theme); }\n.wfc-wallet-slot { position:relative; height:55px; }\n.wfc-wallet-button { height:55px; overflow:hidden; border-radius:12px; }\n.wfc-progress { position:relative; display:block; height:100%; overflow:hidden; border-radius:12px; background:#eef0f6; }\n.wfc-progress::after { content:''; position:absolute; inset:0; background:linear-gradient(90deg, transparent, rgba(255,255,255,.75), transparent); transform:translateX(-100%); animation:wfc-sheen 1.3s ease-in-out infinite; }\n.wfc-progress--overlay { position:absolute; inset:0; z-index:1; }\n.wfc-progress--form { height:320px; }\n@keyframes wfc-sheen { to { transform:translateX(100%); } }\n@media (prefers-reduced-motion: reduce) { .wfc-progress::after { animation:none; } }\n.wfc-wallet-unavailable { position:absolute; inset:0; z-index:1; display:flex; align-items:center; justify-content:center; margin:0; padding:0 16px; border-radius:12px; background:#f4f5f8; color:#6b6773; font-size:14px; line-height:1.35; text-align:center; }\n.wfc-secure { margin:16px 0 0; text-align:center; font-size:14px; line-height:1.45; color:#8c909a; }\n.wfc-card-form { display:grid; gap:14px; }\n.wfc-card-form .wfc-secure { margin-top:0; }\n.wfc-submit { min-height:52px; border:0; border-radius:12px; background:var(--wfc-button); color:#fff; font:600 17px/1.2 inherit; cursor:pointer; }\n.wfc-submit:disabled { opacity:.6; cursor:default; }\n.wfc-error { margin:0; color:#c62828; font-size:14px; text-align:center; }\n.wfc-secure.is-status { color:var(--wfc-theme); font-weight:600; }\n";
@@ -0,0 +1,38 @@
1
+ /** Styles for StripeWalletFirstCheckout; colors come from its `themeColor` and `buttonColor` props. */
2
+ export const stripeWalletFirstCheckoutStyles = `
3
+ .wfc { display:grid; gap:12px; }
4
+ /* The radio marks the choice; every card keeps the same quiet border. */
5
+ .wfc-method { border-radius:16px; background:#fff; box-shadow:0 0 0 1.5px #e6e6ec; }
6
+ .wfc-method-head { display:flex; align-items:center; gap:14px; width:100%; min-height:60px; padding:14px 16px; border:0; background:transparent; color:inherit; text-align:left; cursor:pointer; border-radius:16px; }
7
+ .wfc-method-head:disabled { cursor:default; }
8
+ .wfc-method-head:focus-visible { outline:3px solid var(--wfc-theme); outline-offset:2px; }
9
+ .wfc-radio { flex:none; width:24px; height:24px; border-radius:50%; border:2px solid #b8bac4; background:#fff; transition:border-color .2s ease, box-shadow .2s ease; }
10
+ .wfc-method[data-checked='true'] .wfc-radio { border-color:var(--wfc-theme); box-shadow:inset 0 0 0 4px #fff, inset 0 0 0 12px var(--wfc-theme); }
11
+ .wfc-method-body { display:grid; grid-template-rows:0fr; opacity:0; transition:grid-template-rows .34s cubic-bezier(.25,.8,.25,1), opacity .22s ease; }
12
+ .wfc-method-body[data-open='true'] { grid-template-rows:1fr; opacity:1; transition:grid-template-rows .34s cubic-bezier(.25,.8,.25,1), opacity .28s ease .08s; }
13
+ .wfc-method-clip { min-height:0; overflow:hidden; }
14
+ .wfc-method-content { padding:0 16px 16px; }
15
+ @media (prefers-reduced-motion: reduce) { .wfc-method-body, .wfc-method-body[data-open='true'] { transition:none; } }
16
+ .wfc-mark { height:30px; width:auto; color:#000; }
17
+ .wfc-card-marks { display:flex; gap:6px; align-items:center; }
18
+ .wfc-brand { width:40px; height:26px; border-radius:4px; }
19
+ .wfc-benefits { margin:0 0 16px; padding:0; list-style:none; display:grid; gap:10px; font-size:15px; line-height:1.4; color:#303136; }
20
+ .wfc-benefits li { display:flex; gap:10px; align-items:flex-start; }
21
+ .wfc-check { flex:none; width:16px; height:16px; margin-top:2px; color:var(--wfc-theme); }
22
+ .wfc-wallet-slot { position:relative; height:55px; }
23
+ .wfc-wallet-button { height:55px; overflow:hidden; border-radius:12px; }
24
+ .wfc-progress { position:relative; display:block; height:100%; overflow:hidden; border-radius:12px; background:#eef0f6; }
25
+ .wfc-progress::after { content:''; position:absolute; inset:0; background:linear-gradient(90deg, transparent, rgba(255,255,255,.75), transparent); transform:translateX(-100%); animation:wfc-sheen 1.3s ease-in-out infinite; }
26
+ .wfc-progress--overlay { position:absolute; inset:0; z-index:1; }
27
+ .wfc-progress--form { height:320px; }
28
+ @keyframes wfc-sheen { to { transform:translateX(100%); } }
29
+ @media (prefers-reduced-motion: reduce) { .wfc-progress::after { animation:none; } }
30
+ .wfc-wallet-unavailable { position:absolute; inset:0; z-index:1; display:flex; align-items:center; justify-content:center; margin:0; padding:0 16px; border-radius:12px; background:#f4f5f8; color:#6b6773; font-size:14px; line-height:1.35; text-align:center; }
31
+ .wfc-secure { margin:16px 0 0; text-align:center; font-size:14px; line-height:1.45; color:#8c909a; }
32
+ .wfc-card-form { display:grid; gap:14px; }
33
+ .wfc-card-form .wfc-secure { margin-top:0; }
34
+ .wfc-submit { min-height:52px; border:0; border-radius:12px; background:var(--wfc-button); color:#fff; font:600 17px/1.2 inherit; cursor:pointer; }
35
+ .wfc-submit:disabled { opacity:.6; cursor:default; }
36
+ .wfc-error { margin:0; color:#c62828; font-size:14px; text-align:center; }
37
+ .wfc-secure.is-status { color:var(--wfc-theme); font-weight:600; }
38
+ `;
@@ -10,5 +10,6 @@ export * from './StripeExpressCheckoutElement.js';
10
10
  export * from './StripeOneTimeWalletSurface.js';
11
11
  export * from './StripePlanSelector.js';
12
12
  export * from './StripeSubscriptionWalletSurface.js';
13
+ export * from './StripeWalletFirstCheckout.js';
13
14
  export * from './WalletSubscriptionCheckoutSlot.js';
14
15
  export * from './walletPlatform.js';
@@ -10,5 +10,6 @@ export * from './StripeExpressCheckoutElement.js';
10
10
  export * from './StripeOneTimeWalletSurface.js';
11
11
  export * from './StripePlanSelector.js';
12
12
  export * from './StripeSubscriptionWalletSurface.js';
13
+ export * from './StripeWalletFirstCheckout.js';
13
14
  export * from './WalletSubscriptionCheckoutSlot.js';
14
15
  export * from './walletPlatform.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.37.0",
3
+ "version": "0.38.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",