@funnelsgrove/payments 0.1.38 → 0.1.40

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
@@ -47,6 +47,7 @@ Detailed implementation docs:
47
47
  - `src/providers/stripe/useStripeSubscriptionCheckoutSession.ts` for Stripe SDK initialization, publishable-key resolution, subscription client-secret preparation, card/wallet loading state, and friendly checkout errors
48
48
  - `src/providers/stripe/ApplePaySubscriptionCheckoutSlot.tsx` and `src/providers/stripe/GooglePaySubscriptionCheckoutSlot.tsx` for wallet shells that prepare Stripe Express Checkout on load, keep the loading placeholder clickable, and let Stripe's real wallet controls sit above the placeholder as soon as they mount
49
49
  - Stripe subscription checkout sessions reuse the active client secret across card checkout and paywall wallet checkout for the same intent key, so opening the embedded checkout after a paywall wallet button is ready does not create a second subscription session.
50
+ - `trackPaidStripeSubscriptionCheckoutCompleted` verifies a Stripe custom Checkout Session is paid before delegating to `@funnelsgrove/analytics` for the canonical `checkout_completed` purchase event.
50
51
  - Stripe Express Checkout elements must stay mounted and measurable until Stripe reports availability; only hide the container after `onReady` or `onLoadError` reports that the requested wallet method is unavailable.
51
52
  - Shared checkout dialogs must switch to card checkout when Stripe reports no requested wallet availability, so an unavailable Apple Pay or Google Pay shell never leaves a fake wallet tab blocking the card form.
52
53
  - Wallet slots should not leave disabled grey placeholder shells blocking the paywall. While availability is unknown, the placeholder can sit behind Stripe's real wallet element; after Stripe reports unavailable or preparation cannot create a client secret, the placeholder must disappear instead of opening a card checkout under wallet branding.
@@ -62,7 +63,7 @@ Detailed implementation docs:
62
63
  ## What Does Not Belong Here
63
64
 
64
65
  - funnel routing
65
- - funnel analytics
66
+ - funnel-owned analytics event definitions outside the shared checkout completion handoff
66
67
  - funnel-owned step layout outside the shared checkout shell
67
68
  - funnel-owned billing catalog entries
68
69
  - funnel-specific decisions about when to activate or upgrade an offer
@@ -22,6 +22,7 @@ export type SharedStripeCheckoutV2DialogProps = {
22
22
  onCustomerEmailCommit?: (email: string) => Promise<string | null | void>;
23
23
  onError?: (message: string | null) => void;
24
24
  onInactiveCheckoutSession?: () => void;
25
+ onPaymentInfoSubmitted?: () => void | Promise<void>;
25
26
  onSuccess?: () => void | Promise<void>;
26
27
  paymentMethodLabels?: readonly string[];
27
28
  paypalButtonLabel: string;
@@ -52,7 +52,7 @@ function getFriendlyCardErrorMessage(error) {
52
52
  ? error.message
53
53
  : 'Payment failed. Please review your card details and try again.';
54
54
  }
55
- function SharedStripeCheckoutV2Form({ amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, customerEmail, customerEmailEditable = false, customerEmailLabel = 'Email', customerEmailPlaceholder = 'Enter your email', customerEmailInvalidMessage = 'Enter a valid email address.', customerName, discountAmountLabel, discountLabel, discountPercent, initialWalletAvailable, onClose, onCustomerEmailChange, onCustomerEmailCommit, onError, onInactiveCheckoutSession, onSuccess, paymentMethodLabels = defaultPaymentMethodLabels, promoActive, processingLabel, promoCode, promoCodeLabel, remainingSeconds, returnUrl, savedLabel, secureLabel, satisfactionLabel, satisfactionPercent, summaryLabel, themeColor, title, totalLabel, totalValue, walletButtonLabel, originalPriceLabel, originalPriceValue, }) {
55
+ function SharedStripeCheckoutV2Form({ amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, customerEmail, customerEmailEditable = false, customerEmailLabel = 'Email', customerEmailPlaceholder = 'Enter your email', customerEmailInvalidMessage = 'Enter a valid email address.', customerName, discountAmountLabel, discountLabel, discountPercent, initialWalletAvailable, onClose, onCustomerEmailChange, onCustomerEmailCommit, onError, onInactiveCheckoutSession, onPaymentInfoSubmitted, onSuccess, paymentMethodLabels = defaultPaymentMethodLabels, promoActive, processingLabel, promoCode, promoCodeLabel, remainingSeconds, returnUrl, savedLabel, secureLabel, satisfactionLabel, satisfactionPercent, summaryLabel, themeColor, title, totalLabel, totalValue, walletButtonLabel, originalPriceLabel, originalPriceValue, }) {
56
56
  const checkoutState = useCheckout();
57
57
  const walletPaymentMethods = usePlatformWalletPaymentMethods();
58
58
  const [selectedMethod, setSelectedMethod] = useState('wallet');
@@ -217,6 +217,7 @@ function SharedStripeCheckoutV2Form({ amountCents, buttonColor, cardSubmitLabel,
217
217
  setSubmitting(false);
218
218
  return;
219
219
  }
220
+ await (onPaymentInfoSubmitted === null || onPaymentInfoSubmitted === void 0 ? void 0 : onPaymentInfoSubmitted());
220
221
  const result = await checkoutState.checkout.confirm({
221
222
  redirect: 'if_required',
222
223
  });
@@ -259,7 +260,7 @@ function SharedStripeCheckoutV2Form({ amountCents, buttonColor, cardSubmitLabel,
259
260
  effectiveSelectedMethod === 'wallet' ? 'is-visible' : '',
260
261
  ]
261
262
  .filter(Boolean)
262
- .join(' '), children: [_jsxs("p", { className: 'shared-checkout-v2-secure-pill', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] }), _jsxs("div", { className: 'shared-checkout-v2-wallet-shell', "aria-label": walletAriaLabel, children: [_jsx(StripeCheckoutExpressCheckoutButton, { amountCents: amountCents, availabilityPaymentMethods: walletPaymentMethods, beforeConfirm: ensureCustomerEmail, className: 'shared-checkout-v2-express-buttons', confirmEmail: false, customerEmail: resolvedCustomerEmail, customerName: customerName, initialAvailable: initialWalletAvailable, keepInitialAvailableOnReady: initialWalletAvailable === true, onAvailabilityChange: handleWalletAvailabilityChange, onError: setSharedError, onSuccess: onSuccess, options: expressCheckoutOptions, returnUrl: returnUrl, summaryLabel: expressCheckoutSummaryLabel }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] })] }), _jsxs("div", { hidden: effectiveSelectedMethod !== 'card', className: [
263
+ .join(' '), children: [_jsxs("p", { className: 'shared-checkout-v2-secure-pill', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] }), _jsxs("div", { className: 'shared-checkout-v2-wallet-shell', "aria-label": walletAriaLabel, children: [_jsx(StripeCheckoutExpressCheckoutButton, { amountCents: amountCents, availabilityPaymentMethods: walletPaymentMethods, beforeConfirm: ensureCustomerEmail, className: 'shared-checkout-v2-express-buttons', confirmEmail: false, customerEmail: resolvedCustomerEmail, customerName: customerName, initialAvailable: initialWalletAvailable, keepInitialAvailableOnReady: initialWalletAvailable === true, onAvailabilityChange: handleWalletAvailabilityChange, onError: setSharedError, onPaymentInfoSubmitted: onPaymentInfoSubmitted, onSuccess: onSuccess, options: expressCheckoutOptions, returnUrl: returnUrl, summaryLabel: expressCheckoutSummaryLabel }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] })] }), _jsxs("div", { hidden: effectiveSelectedMethod !== 'card', className: [
263
264
  'shared-checkout-v2-card-panel',
264
265
  effectiveSelectedMethod === 'card' ? 'is-visible' : '',
265
266
  ]
@@ -15,7 +15,9 @@ export type StripeCheckoutExpressCheckoutButtonProps = {
15
15
  serverUpdateKey?: string | null;
16
16
  onServerUpdate?: () => Promise<boolean>;
17
17
  onAvailabilityChange?: (available: boolean) => void;
18
+ onCancel?: () => void;
18
19
  onError?: (message: string | null) => void;
20
+ onPaymentInfoSubmitted?: () => void | Promise<void>;
19
21
  onSuccess?: () => void | Promise<void>;
20
22
  };
21
- export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl, confirmEmail, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, serverUpdateKey, onServerUpdate, onAvailabilityChange, onError, onSuccess, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
23
+ export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl, confirmEmail, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, serverUpdateKey, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { ExpressCheckoutElement, useCheckout, } from '@stripe/react-stripe-js/checkout';
5
+ const ExpressCheckoutElementWithCancel = ExpressCheckoutElement;
5
6
  const buildDefaultOptions = () => ({
6
7
  buttonHeight: 55,
7
8
  buttonTheme: {
@@ -25,7 +26,7 @@ const buildDefaultOptions = () => ({
25
26
  paypal: 'never',
26
27
  },
27
28
  });
28
- export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl, confirmEmail = true, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, serverUpdateKey, onServerUpdate, onAvailabilityChange, onError, onSuccess, }) {
29
+ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl, confirmEmail = true, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, serverUpdateKey, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, }) {
29
30
  const checkoutState = useCheckout();
30
31
  const appliedServerUpdateKeyRef = useRef('');
31
32
  const serverUpdatePromiseRef = useRef(null);
@@ -122,6 +123,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl,
122
123
  event.paymentFailed({ reason: 'invalid_payment_data', message });
123
124
  return;
124
125
  }
126
+ await (onPaymentInfoSubmitted === null || onPaymentInfoSubmitted === void 0 ? void 0 : onPaymentInfoSubmitted());
125
127
  const result = await checkoutState.checkout.confirm(Object.assign(Object.assign({}, (confirmEmail
126
128
  ? { email: preparedCustomerEmail || (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || undefined }
127
129
  : {})), { expressCheckoutConfirmEvent: event, redirect: 'if_required' }));
@@ -143,7 +145,7 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl,
143
145
  effectiveWalletAvailable === false ? 'is-hidden' : '',
144
146
  ]
145
147
  .filter(Boolean)
146
- .join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
148
+ .join(' '), children: _jsx(ExpressCheckoutElementWithCancel, { options: resolvedOptions, onCancel: onCancel, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
147
149
  setWalletAvailable(false);
148
150
  onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
149
151
  onError === null || onError === void 0 ? void 0 : onError(error.message || 'Unable to load wallet checkout.');
@@ -12,7 +12,9 @@ export type StripeExpressCheckoutButtonProps = {
12
12
  initialAvailable?: boolean | null;
13
13
  keepInitialAvailableOnReady?: boolean;
14
14
  onAvailabilityChange?: (available: boolean) => void;
15
+ onCancel?: () => void;
15
16
  onError?: (message: string | null) => void;
17
+ onPaymentInfoSubmitted?: () => void | Promise<void>;
16
18
  onSuccess?: () => void | Promise<void>;
17
19
  };
18
- export declare function StripeExpressCheckoutButton({ amountCents, beforeConfirm, summaryLabel, returnUrl, customerEmail, customerName, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, onAvailabilityChange, onError, onSuccess, }: StripeExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function StripeExpressCheckoutButton({ amountCents, beforeConfirm, summaryLabel, returnUrl, customerEmail, customerName, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, }: StripeExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -53,7 +53,7 @@ const buildDefaultOptions = (input) => {
53
53
  const isSuccessfulIntentStatus = (status) => {
54
54
  return status === 'succeeded' || status === 'processing' || status === 'requires_capture';
55
55
  };
56
- export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, summaryLabel, returnUrl, customerEmail, customerName, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, onAvailabilityChange, onError, onSuccess, }) {
56
+ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, summaryLabel, returnUrl, customerEmail, customerName, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, }) {
57
57
  const stripe = useStripe();
58
58
  const elements = useElements();
59
59
  const [walletAvailable, setWalletAvailable] = useState(initialAvailable !== null && initialAvailable !== void 0 ? initialAvailable : null);
@@ -104,6 +104,7 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, summar
104
104
  event.paymentFailed({ reason: 'invalid_payment_data', message });
105
105
  return;
106
106
  }
107
+ await (onPaymentInfoSubmitted === null || onPaymentInfoSubmitted === void 0 ? void 0 : onPaymentInfoSubmitted());
107
108
  const result = await stripe.confirmPayment({
108
109
  elements,
109
110
  confirmParams: {
@@ -139,7 +140,7 @@ export function StripeExpressCheckoutButton({ amountCents, beforeConfirm, summar
139
140
  effectiveWalletAvailable === false ? 'is-hidden' : '',
140
141
  ]
141
142
  .filter(Boolean)
142
- .join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
143
+ .join(' '), children: _jsx(ExpressCheckoutElement, { options: resolvedOptions, onCancel: onCancel, onConfirm: (event) => void handleConfirm(event), onLoadError: ({ error }) => {
143
144
  setWalletAvailable(false);
144
145
  onAvailabilityChange === null || onAvailabilityChange === void 0 ? void 0 : onAvailabilityChange(false);
145
146
  onError === null || onError === void 0 ? void 0 : onError(error.message || 'Unable to load wallet checkout.');
@@ -31,7 +31,9 @@ export type WalletSubscriptionCheckoutSlotProps = {
31
31
  disabled?: boolean;
32
32
  expressCheckoutAllowed?: boolean;
33
33
  onAvailabilityChange?: (available: boolean) => void;
34
+ onCancel?: () => void;
34
35
  onError?: (message: string | null) => void;
36
+ onPaymentInfoSubmitted?: () => void | Promise<void>;
35
37
  onSuccess?: () => void | Promise<void>;
36
38
  onUnavailableClick?: () => Promise<void> | void;
37
39
  options?: Partial<StripeCheckoutExpressCheckoutElementOptions>;
@@ -44,5 +46,5 @@ export type WalletSubscriptionCheckoutSlotProps = {
44
46
  suspended?: boolean;
45
47
  };
46
48
  export declare function WalletSubscriptionCheckoutLoadingPlaceholder({ className, label, }: Pick<WalletPlaceholderButtonProps, 'className' | 'label'>): import("react/jsx-runtime").JSX.Element;
47
- export declare function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymentMethod, beforeConfirm, className, customerEmail, customerName, disabled, expressCheckoutAllowed, onAvailabilityChange, onError, onSuccess, onUnavailableClick, options, placeholderLabel, renderPreparingPlaceholder, renderPlaceholder, returnUrl, session, summaryLabel, suspended, }: WalletSubscriptionCheckoutSlotProps): import("react/jsx-runtime").JSX.Element;
49
+ export declare function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymentMethod, beforeConfirm, className, customerEmail, customerName, disabled, expressCheckoutAllowed, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, options, placeholderLabel, renderPreparingPlaceholder, renderPlaceholder, returnUrl, session, summaryLabel, suspended, }: WalletSubscriptionCheckoutSlotProps): import("react/jsx-runtime").JSX.Element;
48
50
  export {};
@@ -27,7 +27,7 @@ export function WalletSubscriptionCheckoutLoadingPlaceholder({ className, label,
27
27
  .filter(Boolean)
28
28
  .join(' '), "aria-label": label, role: 'status', children: [_jsx("span", { className: 'wallet-subscription-checkout-slot__loading-bar' }), _jsx("style", { children: walletSubscriptionCheckoutLoadingPlaceholderStyles })] }));
29
29
  }
30
- export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymentMethod, beforeConfirm, className, customerEmail, customerName, disabled = false, expressCheckoutAllowed = true, onAvailabilityChange, onError, onSuccess, onUnavailableClick, options, placeholderLabel, renderPreparingPlaceholder, renderPlaceholder, returnUrl, session, summaryLabel, suspended = false, }) {
30
+ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymentMethod, beforeConfirm, className, customerEmail, customerName, disabled = false, expressCheckoutAllowed = true, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onSuccess, onUnavailableClick, options, placeholderLabel, renderPreparingPlaceholder, renderPlaceholder, returnUrl, session, summaryLabel, suspended = false, }) {
31
31
  const [walletAvailability, setWalletAvailability] = useState({
32
32
  available: null,
33
33
  intentKey: session.intentKey,
@@ -131,7 +131,7 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
131
131
  .filter(Boolean)
132
132
  .join(' '), "aria-busy": placeholderIsPreparing || undefined, children: [shouldRenderExpressCheckout &&
133
133
  session.activeClientSecret &&
134
- session.stripePromise ? (_jsx(StripeExpressCheckoutElement, { amountCents: amountCents, availabilityPaymentMethods: [availabilityPaymentMethod], beforeConfirm: beforeConfirm, className: className, clientSecret: session.activeClientSecret, customerEmail: customerEmail, customerName: customerName, onAvailabilityChange: handleAvailabilityChange, onError: handleError, onSuccess: onSuccess, options: options, returnUrl: returnUrl, serverUpdateKey: serverUpdateKey, stripePromise: session.stripePromise, summaryLabel: summaryLabel, onServerUpdate: session.updateCheckoutSessionPlan })) : null, shouldRenderPlaceholder ? (_jsx("div", { className: 'wallet-subscription-checkout-slot__placeholder', children: placeholderRenderer({
134
+ session.stripePromise ? (_jsx(StripeExpressCheckoutElement, { amountCents: amountCents, availabilityPaymentMethods: [availabilityPaymentMethod], beforeConfirm: beforeConfirm, className: className, clientSecret: session.activeClientSecret, customerEmail: customerEmail, customerName: customerName, onAvailabilityChange: handleAvailabilityChange, onCancel: onCancel, onError: handleError, onPaymentInfoSubmitted: onPaymentInfoSubmitted, onSuccess: onSuccess, options: options, returnUrl: returnUrl, serverUpdateKey: serverUpdateKey, stripePromise: session.stripePromise, summaryLabel: summaryLabel, onServerUpdate: session.updateCheckoutSessionPlan })) : null, shouldRenderPlaceholder ? (_jsx("div", { className: 'wallet-subscription-checkout-slot__placeholder', children: placeholderRenderer({
135
135
  className,
136
136
  disabled: slotDisabled,
137
137
  label: placeholderLabel,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",
@@ -27,7 +27,7 @@
27
27
  "test:run": "vitest run --passWithNoTests"
28
28
  },
29
29
  "dependencies": {
30
- "@funnelsgrove/analytics": "^0.1.13",
30
+ "@funnelsgrove/analytics": "^0.1.15",
31
31
  "@funnelsgrove/runtime": "^0.1.18",
32
32
  "@stripe/react-stripe-js": "^5.6.0",
33
33
  "@stripe/stripe-js": "^8.7.0",