@funnelsgrove/payments 0.37.0 → 0.39.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 +25 -1
- package/dist/generated/package-version.d.ts +1 -1
- package/dist/generated/package-version.js +1 -1
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Dialog.js +7 -1
- package/dist/providers/stripe/components/StripeWalletFirstCheckout.d.ts +36 -0
- package/dist/providers/stripe/components/StripeWalletFirstCheckout.js +196 -0
- package/dist/providers/stripe/components/StripeWalletFirstCheckout.styles.d.ts +2 -0
- package/dist/providers/stripe/components/StripeWalletFirstCheckout.styles.js +38 -0
- package/dist/providers/stripe/components/StripeWalletSurface.internal.d.ts +1 -1
- package/dist/providers/stripe/components/StripeWalletSurface.internal.js +8 -0
- package/dist/providers/stripe/components/index.d.ts +1 -0
- package/dist/providers/stripe/components/index.js +1 -0
- package/dist/providers/stripe/hooks/useCheckoutShownOnce.d.ts +2 -0
- package/dist/providers/stripe/hooks/useCheckoutShownOnce.js +11 -0
- package/dist/providers/stripe/services/checkoutCompletionAnalytics.service.d.ts +7 -0
- package/dist/providers/stripe/services/checkoutCompletionAnalytics.service.js +18 -0
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -28,6 +28,29 @@ 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_shown` (once per mount, `checkout_surface: inline`),
|
|
48
|
+
`checkout_started` (when the card form opens, or on wallet intent),
|
|
49
|
+
`add_payment_info` and verified `checkout_completed`.
|
|
50
|
+
- After confirmation, the payment status stays in the form until the caller replaces it.
|
|
51
|
+
With the runtime's embedded completion, the success screen takes over. Otherwise
|
|
52
|
+
`onSuccess` runs, and then the page navigates to `returnUrl`.
|
|
53
|
+
|
|
31
54
|
Use these for new one-time paywall plans:
|
|
32
55
|
|
|
33
56
|
- `StripeOneTimeWalletSurface`
|
|
@@ -198,11 +221,12 @@ conversion event is introduced.
|
|
|
198
221
|
- Wallet unavailable state must fall back to card/manual checkout or disappear.
|
|
199
222
|
- Count `checkout_started` only at shared UI boundaries: when a shared card dialog opens or when Stripe reports Apple Pay / Google Pay intent. Checkout Sessions use confirmation or cancellation as a fallback because Stripe omits their click callback. Cancellation recovers the start with `checkout_start_source: wallet_cancel`; its timestamp is dismissal time, not wallet-open time. Closing the browser before either callback is still unobservable. Cancel has no payment-method field, so the method is included only when the element enables exactly one method.
|
|
200
223
|
- Pass `checkoutAnalytics` to every current shared dialog and wallet surface. The package then owns `checkout_started`, `add_payment_info`, and verified `checkout_completed`, all deduplicated by the actual Checkout Session.
|
|
224
|
+
- The package also owns `checkout_shown`: a checkout form or payment button became visible. It is sent once per show with a new event ID and `checkout_surface`: `inline` (wallet-first form, or the V2 dialog with `presentation='inline'`), `dialog` (each V2 dialog open) or `wallet` (a standalone wallet surface, once per mount after Stripe reports a wallet available). It is not a conversion and is not sent to Meta or Google.
|
|
201
225
|
- Payment submission triggers, validation, declines, and retry identity follow [AE-BR-001–004](../../docs/product-specs/analytics-events.md#payment-information-submission). Keep that behavior inside shared checkout surfaces.
|
|
202
226
|
- Call funnel lifecycle `completeStep` only from the shared checkout `onSuccess` callback. Payment-info submission is not step completion.
|
|
203
227
|
- Wallet slots may create render-only Checkout Sessions on mount so Apple Pay and Google Pay render before the visitor taps. Session creation is technical preparation and must never emit `checkout_started`.
|
|
204
228
|
- Shared checkout analytics deduplicates `checkout_started` by Checkout Session id. Wallet confirmation and cancellation may retry the start signal when the click callback is unavailable, without creating a duplicate. Cancellation never emits payment-info or purchase completion events.
|
|
205
|
-
- Do not call `publicAnalyticsSdk.trackCheckoutStarted`, `trackPaymentInfoSubmitted`, or `trackCheckoutCompleted` directly from funnel checkout components.
|
|
229
|
+
- Do not call `publicAnalyticsSdk.trackCheckoutShown`, `trackCheckoutStarted`, `trackPaymentInfoSubmitted`, or `trackCheckoutCompleted` directly from funnel checkout components.
|
|
206
230
|
- Funnel paywalls should style wallet buttons through the shared slot API: `className` for the Stripe button/placeholder, `slotClassName` for the outer slot, `appearance` for Stripe Elements appearance, and `options` for Stripe Express Checkout options.
|
|
207
231
|
- Keep discount math reusable here, but keep offer activation timing in funnel code.
|
|
208
232
|
- `SharedStripeCheckoutV2Dialog` forwards wallet-sheet cancellation to `onClose` by default; `onWalletCancel` may override that callback. Standalone wallet surfaces call their `onCancel` callback. Funnel code chooses whether either callback opens a discount offer.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const PAYMENTS_PACKAGE_VERSION = "0.
|
|
1
|
+
export declare const PAYMENTS_PACKAGE_VERSION = "0.39.0";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated by scripts/generate-package-version.mjs.
|
|
2
|
-
export const PAYMENTS_PACKAGE_VERSION = "0.
|
|
2
|
+
export const PAYMENTS_PACKAGE_VERSION = "0.39.0";
|
|
@@ -14,8 +14,9 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
14
14
|
import { useCallback, useEffect, useMemo } from 'react';
|
|
15
15
|
import { CheckoutProvider } from '@stripe/react-stripe-js/checkout';
|
|
16
16
|
import { normalizeSupportedCheckoutCountries } from './checkoutCountries.js';
|
|
17
|
-
import { queueStripeSubscriptionCheckoutStarted } from '../services/checkoutCompletionAnalytics.service.js';
|
|
17
|
+
import { queueStripeSubscriptionCheckoutStarted, trackStripeCheckoutShown } from '../services/checkoutCompletionAnalytics.service.js';
|
|
18
18
|
import { trackCheckoutCanceled } from '../../../services/checkoutObservability.service.js';
|
|
19
|
+
import { useCheckoutShownOnce } from '../hooks/useCheckoutShownOnce.js';
|
|
19
20
|
import { SharedStripeCheckoutV2CheckoutSessionForm, getFixedSupportedBillingCountry } from './SharedStripeCheckoutV2Form.internal.js';
|
|
20
21
|
import { sharedStripeCheckoutV2Styles } from './SharedStripeCheckoutV2.styles.js';
|
|
21
22
|
const stripeCheckoutV2Appearance = {
|
|
@@ -73,6 +74,11 @@ export function SharedStripeCheckoutV2Dialog(_a) {
|
|
|
73
74
|
}
|
|
74
75
|
onClose();
|
|
75
76
|
}, [checkoutAnalytics, checkoutSessionId, onClose]);
|
|
77
|
+
// The dialog is mounted while open, so one mount is one show.
|
|
78
|
+
useCheckoutShownOnce(Boolean(checkoutAnalytics), () => {
|
|
79
|
+
if (checkoutAnalytics)
|
|
80
|
+
trackStripeCheckoutShown(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, checkoutSurface: presentation }));
|
|
81
|
+
});
|
|
76
82
|
useEffect(() => {
|
|
77
83
|
if (!checkoutAnalytics) {
|
|
78
84
|
return;
|
|
@@ -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,196 @@
|
|
|
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, trackStripeCheckoutShown, trackStripeSubscriptionPaymentInfoSubmitted, } from '../services/checkoutCompletionAnalytics.service.js';
|
|
24
|
+
import { trackCheckoutFailureShown } from '../../../services/checkoutObservability.service.js';
|
|
25
|
+
import { useCheckoutShownOnce } from '../hooks/useCheckoutShownOnce.js';
|
|
26
|
+
const appearance = {
|
|
27
|
+
labels: 'above',
|
|
28
|
+
variables: {
|
|
29
|
+
borderRadius: '12px',
|
|
30
|
+
colorBackground: '#fff',
|
|
31
|
+
colorText: '#303136',
|
|
32
|
+
colorTextPlaceholder: '#8c909a',
|
|
33
|
+
spacingUnit: '4px',
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
export function StripeWalletFirstCheckout(props) {
|
|
37
|
+
const { buttonColor, checkoutAnalytics, checkoutSessionId, clientSecret, copy, stripePromise, themeColor } = props;
|
|
38
|
+
const wallet = usePlatformWallet();
|
|
39
|
+
const [selected, setSelected] = useState('wallet');
|
|
40
|
+
const style = { '--wfc-theme': themeColor, '--wfc-button': buttonColor };
|
|
41
|
+
const options = useMemo(() => clientSecret ? {
|
|
42
|
+
clientSecret,
|
|
43
|
+
elementsOptions: { appearance: Object.assign(Object.assign({}, appearance), { variables: Object.assign(Object.assign({}, appearance.variables), { colorPrimary: themeColor }) }) },
|
|
44
|
+
} : null, [clientSecret, themeColor]);
|
|
45
|
+
// The whole layout shows on mount, so one mount is one show.
|
|
46
|
+
useCheckoutShownOnce(Boolean(checkoutAnalytics), () => {
|
|
47
|
+
if (checkoutAnalytics)
|
|
48
|
+
trackStripeCheckoutShown(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, checkoutSurface: 'inline' }));
|
|
49
|
+
});
|
|
50
|
+
// Session creation is preparation; the card checkout starts when the visitor opens the card form.
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!checkoutAnalytics || !clientSecret || selected !== 'card')
|
|
53
|
+
return;
|
|
54
|
+
queueStripeSubscriptionCheckoutStarted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId, checkoutStartSource: 'card_form_open', paymentMethod: 'card' }));
|
|
55
|
+
}, [checkoutAnalytics, checkoutSessionId, clientSecret, selected]);
|
|
56
|
+
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 })] }));
|
|
57
|
+
}
|
|
58
|
+
function StripeWalletFirstCheckoutForm({ amountCents, checkoutAnalytics, checkoutSessionId, copy, customerEmail, customerName, onError, onSuccess, returnUrl, summaryLabel, wallet, selected, onSelect, style, }) {
|
|
59
|
+
const checkoutState = useCheckout();
|
|
60
|
+
// null until Stripe answers; the slot shows progress at the button's height meanwhile.
|
|
61
|
+
const [walletAvailable, setWalletAvailable] = useState(null);
|
|
62
|
+
const [walletConfirming, setWalletConfirming] = useState(false);
|
|
63
|
+
const [paymentElementReady, setPaymentElementReady] = useState(false);
|
|
64
|
+
const [paymentElementComplete, setPaymentElementComplete] = useState(false);
|
|
65
|
+
const [submitting, setSubmitting] = useState(false);
|
|
66
|
+
const [providerConfirmed, setProviderConfirmed] = useState(false);
|
|
67
|
+
const [completed, setCompleted] = useState(false);
|
|
68
|
+
const [error, setError] = useState(null);
|
|
69
|
+
const submitInFlight = useRef(false);
|
|
70
|
+
const expressOptions = useMemo(() => ({
|
|
71
|
+
buttonHeight: 55,
|
|
72
|
+
buttonTheme: { applePay: 'black', googlePay: 'black' },
|
|
73
|
+
buttonType: { applePay: 'plain', googlePay: 'plain' },
|
|
74
|
+
layout: { maxColumns: 1, maxRows: 1, overflow: 'auto' },
|
|
75
|
+
paymentMethods: {
|
|
76
|
+
amazonPay: 'never', klarna: 'never', link: 'never', paypal: 'never',
|
|
77
|
+
applePay: wallet === 'applePay' ? 'always' : 'never',
|
|
78
|
+
googlePay: wallet === 'googlePay' ? 'always' : 'never',
|
|
79
|
+
},
|
|
80
|
+
}), [wallet]);
|
|
81
|
+
const paymentElementOptions = useMemo(() => ({
|
|
82
|
+
fields: { billingDetails: { name: 'never', email: 'never', address: { country: 'auto', postalCode: 'never' } } },
|
|
83
|
+
layout: 'tabs',
|
|
84
|
+
terms: { card: 'never' },
|
|
85
|
+
wallets: { applePay: 'never', googlePay: 'never', link: 'never' },
|
|
86
|
+
}), []);
|
|
87
|
+
// A hidden Payment Element measures 0px wide; let Stripe re-layout when it opens.
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (selected !== 'card')
|
|
90
|
+
return;
|
|
91
|
+
const timer = window.setTimeout(() => window.dispatchEvent(new Event('resize')), 0);
|
|
92
|
+
return () => window.clearTimeout(timer);
|
|
93
|
+
}, [selected]);
|
|
94
|
+
const showError = (message, providerError, alreadyTracked = false) => {
|
|
95
|
+
if (message && checkoutAnalytics && !alreadyTracked) {
|
|
96
|
+
trackCheckoutFailureShown({
|
|
97
|
+
analytics: checkoutAnalytics, checkoutSessionId, errorMessage: message,
|
|
98
|
+
failureStage: 'card_form', paymentMethod: 'card', provider: 'stripe',
|
|
99
|
+
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,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
setError(message);
|
|
103
|
+
onError === null || onError === void 0 ? void 0 : onError(message);
|
|
104
|
+
};
|
|
105
|
+
const trackSuccess = async (embedded = false) => {
|
|
106
|
+
if (checkoutAnalytics)
|
|
107
|
+
await trackPaidStripeSubscriptionCheckoutCompleted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId }));
|
|
108
|
+
if (!embedded)
|
|
109
|
+
await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess());
|
|
110
|
+
};
|
|
111
|
+
const finishPaidCheckout = async () => {
|
|
112
|
+
if (await completePaywallCheckout('stripe', checkoutSessionId || '', {
|
|
113
|
+
beforeNotify: async () => { setCompleted(true); await trackSuccess(true); },
|
|
114
|
+
}))
|
|
115
|
+
return;
|
|
116
|
+
await trackSuccess();
|
|
117
|
+
window.location.assign(returnUrl);
|
|
118
|
+
};
|
|
119
|
+
const submitCard = async (event) => {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
if (submitInFlight.current)
|
|
122
|
+
return;
|
|
123
|
+
if (!providerConfirmed) {
|
|
124
|
+
if (checkoutState.type !== 'success') {
|
|
125
|
+
showError(checkoutState.type === 'error' ? checkoutState.error.message : copy.formNotReady);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (!paymentElementReady) {
|
|
129
|
+
showError(copy.formNotReady, undefined, true);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
submitInFlight.current = true;
|
|
134
|
+
setSubmitting(true);
|
|
135
|
+
showError(null);
|
|
136
|
+
let confirmed = providerConfirmed;
|
|
137
|
+
try {
|
|
138
|
+
if (!providerConfirmed && checkoutState.type === 'success') {
|
|
139
|
+
const { checkout } = checkoutState;
|
|
140
|
+
if (!checkout.email && customerEmail) {
|
|
141
|
+
const updated = await checkout.updateEmail(customerEmail);
|
|
142
|
+
if (updated.type === 'error')
|
|
143
|
+
throw new Error(updated.error.message);
|
|
144
|
+
}
|
|
145
|
+
if (paymentElementComplete && checkoutAnalytics) {
|
|
146
|
+
await trackStripeSubscriptionPaymentInfoSubmitted(Object.assign(Object.assign({}, checkoutAnalytics), { checkoutSessionId })).catch(() => 'analytics_rejected');
|
|
147
|
+
}
|
|
148
|
+
const result = await checkout.confirm({ redirect: 'if_required' });
|
|
149
|
+
if (result.type === 'error') {
|
|
150
|
+
showError(result.error.message || copy.paymentFailed, result.error);
|
|
151
|
+
submitInFlight.current = false;
|
|
152
|
+
setSubmitting(false);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
confirmed = true;
|
|
156
|
+
setProviderConfirmed(true);
|
|
157
|
+
}
|
|
158
|
+
await finishPaidCheckout();
|
|
159
|
+
}
|
|
160
|
+
catch (submitError) {
|
|
161
|
+
showError(submitError instanceof Error && submitError.message ? submitError.message : copy.paymentFailed, undefined, confirmed);
|
|
162
|
+
submitInFlight.current = false;
|
|
163
|
+
setSubmitting(false);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
// After a successful confirmation the form stays in place and reports progress
|
|
167
|
+
// where the customer paid, until the success screen takes over.
|
|
168
|
+
const paymentStatus = completed ? copy.paymentConfirmed : providerConfirmed ? copy.verifyingPayment : null;
|
|
169
|
+
const locked = Boolean(paymentStatus) && !error;
|
|
170
|
+
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 }));
|
|
171
|
+
}
|
|
172
|
+
/** Shared by the loading and live states so switching between them moves nothing. */
|
|
173
|
+
function CheckoutLayout({ cardBody, copy, disabled, footer, onSelect, selected, style, wallet, walletNote, walletSlot }) {
|
|
174
|
+
const WalletMark = wallet === 'applePay' ? ApplePayMark : GooglePayMark;
|
|
175
|
+
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] }));
|
|
176
|
+
}
|
|
177
|
+
function MethodCard({ checked, children, disabled, header, label, onSelect }) {
|
|
178
|
+
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 }) }) })] }));
|
|
179
|
+
}
|
|
180
|
+
function CheckIcon() {
|
|
181
|
+
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' }) });
|
|
182
|
+
}
|
|
183
|
+
function ApplePayMark() {
|
|
184
|
+
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" })] }));
|
|
185
|
+
}
|
|
186
|
+
function GooglePayMark() {
|
|
187
|
+
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" })] }));
|
|
188
|
+
}
|
|
189
|
+
function CardBrandMarks() {
|
|
190
|
+
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" }) })] }));
|
|
191
|
+
}
|
|
192
|
+
/** Google Pay on Android, Apple Pay everywhere else — read synchronously from the browser. */
|
|
193
|
+
function usePlatformWallet() {
|
|
194
|
+
const platform = useSyncExternalStore(() => () => undefined, () => resolveWalletPlatform({ maxTouchPoints: navigator.maxTouchPoints, userAgent: navigator.userAgent }), () => 'ios');
|
|
195
|
+
return platform === 'android' ? 'googlePay' : 'applePay';
|
|
196
|
+
}
|
|
@@ -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
|
+
`;
|
|
@@ -3,7 +3,7 @@ import type { StripeWalletCheckoutSuccess } from './StripeCheckoutExpressCheckou
|
|
|
3
3
|
import { type ReactNode } from 'react';
|
|
4
4
|
import type { Appearance, StripeCheckoutExpressCheckoutElementOptions } from '@stripe/stripe-js';
|
|
5
5
|
import { type PlatformWalletPaymentMethod } from './walletPlatform.js';
|
|
6
|
-
import type
|
|
6
|
+
import { type StripeSubscriptionCheckoutAnalyticsContext } from '../services/checkoutCompletionAnalytics.service.js';
|
|
7
7
|
import type { StripeWalletCheckoutSession } from './WalletSubscriptionCheckoutSlot.js';
|
|
8
8
|
export type StripeWalletSurfaceAvailability = {
|
|
9
9
|
anyAvailable: boolean;
|
|
@@ -4,8 +4,10 @@ import { CheckoutEmailDialog } from '../../../components/CheckoutEmailDialog.js'
|
|
|
4
4
|
import { isCheckoutEmailError, isValidCheckoutEmail, normalizeCheckoutEmailResult } from '../../../services/checkoutEmail.js';
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
6
|
import { usePlatformWalletPaymentMethods, } from './walletPlatform.js';
|
|
7
|
+
import { trackStripeCheckoutShown } from '../services/checkoutCompletionAnalytics.service.js';
|
|
7
8
|
import { ApplePaySubscriptionCheckoutSlot } from './ApplePaySubscriptionCheckoutSlot.js';
|
|
8
9
|
import { GooglePaySubscriptionCheckoutSlot } from './GooglePaySubscriptionCheckoutSlot.js';
|
|
10
|
+
import { useCheckoutShownOnce } from '../hooks/useCheckoutShownOnce.js';
|
|
9
11
|
export function buildStripeWalletSurfaceAnalyticsMetadata(analyticsMetadata, surfaceId) {
|
|
10
12
|
return Object.assign(Object.assign({}, (analyticsMetadata !== null && analyticsMetadata !== void 0 ? analyticsMetadata : {})), { checkoutSurfaceId: surfaceId });
|
|
11
13
|
}
|
|
@@ -169,6 +171,12 @@ export function StripeWalletSurface({ amountCents, appearance, applePayOptions,
|
|
|
169
171
|
resolved: availability.resolved,
|
|
170
172
|
suspended,
|
|
171
173
|
});
|
|
174
|
+
const walletButtonVisible = shouldRenderSurface && availability.anyAvailable && !needsEmail;
|
|
175
|
+
useCheckoutShownOnce(walletButtonVisible && Boolean(surfaceCheckoutAnalytics), () => {
|
|
176
|
+
if (surfaceCheckoutAnalytics) {
|
|
177
|
+
trackStripeCheckoutShown(Object.assign(Object.assign({}, surfaceCheckoutAnalytics), { checkoutSessionId: session.checkoutSessionId, checkoutSurface: 'wallet' }));
|
|
178
|
+
}
|
|
179
|
+
});
|
|
172
180
|
if (needsEmail && session.configured && !suspended && walletPaymentMethods.length > 0) {
|
|
173
181
|
if (!emailDialogOpen || !onCustomerEmailCommit)
|
|
174
182
|
return null;
|
|
@@ -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';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
/** Calls `track` once per mount, the first time `enabled` is true; re-renders never repeat it. */
|
|
3
|
+
export function useCheckoutShownOnce(enabled, track) {
|
|
4
|
+
const tracked = useRef(false);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
if (!enabled || tracked.current)
|
|
7
|
+
return;
|
|
8
|
+
tracked.current = true;
|
|
9
|
+
track();
|
|
10
|
+
}, [enabled, track]);
|
|
11
|
+
}
|
|
@@ -23,9 +23,16 @@ export type TrackPaidStripeSubscriptionCheckoutCompletedInput = StripeSubscripti
|
|
|
23
23
|
export type TrackStripeSubscriptionPaymentInfoSubmittedInput = StripeSubscriptionCheckoutAnalyticsContext & {
|
|
24
24
|
checkoutSessionId?: string | null;
|
|
25
25
|
};
|
|
26
|
+
export type StripeCheckoutSurface = 'inline' | 'dialog' | 'wallet';
|
|
27
|
+
export type TrackStripeCheckoutShownInput = StripeSubscriptionCheckoutAnalyticsContext & {
|
|
28
|
+
checkoutSessionId?: string | null;
|
|
29
|
+
checkoutSurface: StripeCheckoutSurface;
|
|
30
|
+
};
|
|
26
31
|
export type TrackStripeSubscriptionCheckoutStartedResult = 'already_tracked' | 'analytics_rejected' | 'missing_checkout_session' | 'tracked';
|
|
27
32
|
export type TrackPaidStripeSubscriptionCheckoutCompletedResult = 'already_tracked' | 'analytics_rejected' | 'missing_checkout_session' | 'tracked' | 'unpaid';
|
|
28
33
|
export type TrackStripeSubscriptionPaymentInfoSubmittedResult = 'already_tracked' | 'analytics_rejected' | 'missing_checkout_session' | 'tracked';
|
|
34
|
+
/** Each call is one show of a checkout form or wallet button; callers own once-per-show. */
|
|
35
|
+
export declare function trackStripeCheckoutShown(input: TrackStripeCheckoutShownInput): void;
|
|
29
36
|
export declare function trackStripeSubscriptionCheckoutStarted(input: TrackStripeSubscriptionCheckoutStartedInput): Promise<TrackStripeSubscriptionCheckoutStartedResult>;
|
|
30
37
|
export declare function queueStripeSubscriptionCheckoutStarted(input: TrackStripeSubscriptionCheckoutStartedInput): void;
|
|
31
38
|
export declare function trackStripeSubscriptionPaymentInfoSubmitted(input: TrackStripeSubscriptionPaymentInfoSubmittedInput): Promise<TrackStripeSubscriptionPaymentInfoSubmittedResult>;
|
|
@@ -49,6 +49,24 @@ const markCheckoutSessionTracked = (eventName, checkoutSessionId, runtimeConfig)
|
|
|
49
49
|
// A stable checkout-session event id still protects provider-side dedupe.
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
|
+
/** Each call is one show of a checkout form or wallet button; callers own once-per-show. */
|
|
53
|
+
export function trackStripeCheckoutShown(input) {
|
|
54
|
+
var _a;
|
|
55
|
+
const checkoutSessionId = ((_a = input.checkoutSessionId) === null || _a === void 0 ? void 0 : _a.trim()) || '';
|
|
56
|
+
try {
|
|
57
|
+
publicAnalyticsSdk.trackCheckoutShown({
|
|
58
|
+
featureFlags: input.featureFlags,
|
|
59
|
+
stepId: input.stepId,
|
|
60
|
+
stepName: input.stepName,
|
|
61
|
+
stepType: input.stepType,
|
|
62
|
+
stepContractVersion: input.stepContractVersion,
|
|
63
|
+
metadata: Object.assign(Object.assign(Object.assign(Object.assign({}, asRecord(input.metadata)), { checkout_surface: input.checkoutSurface }), (checkoutSessionId ? { checkoutSessionId, checkout_session_id: checkoutSessionId } : {})), { environment: input.environment || asRecord(input.metadata).environment, payment_provider: 'stripe' }),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (_b) {
|
|
67
|
+
// Analytics must not affect checkout rendering.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
52
70
|
export async function trackStripeSubscriptionCheckoutStarted(input) {
|
|
53
71
|
var _a;
|
|
54
72
|
const checkoutSessionId = ((_a = input.checkoutSessionId) === null || _a === void 0 ? void 0 : _a.trim()) || '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@funnelsgrove/payments",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.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.
|
|
50
|
-
"@funnelsgrove/analytics": "0.
|
|
49
|
+
"@funnelsgrove/runtime": "0.31.0",
|
|
50
|
+
"@funnelsgrove/analytics": "0.13.0"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@funnelsgrove/runtime": "^0.
|
|
54
|
-
"@funnelsgrove/analytics": "^0.
|
|
53
|
+
"@funnelsgrove/runtime": "^0.31.0",
|
|
54
|
+
"@funnelsgrove/analytics": "^0.13.0"
|
|
55
55
|
}
|
|
56
56
|
}
|