@funnelsgrove/payments 0.15.11 → 0.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/README.md +19 -0
  2. package/dist/components/ManageSubscriptionScreen.d.ts +30 -4
  3. package/dist/components/ManageSubscriptionScreen.js +45 -30
  4. package/dist/index.d.ts +1 -1
  5. package/dist/providers/solidgate/components/SolidgateCheckoutDialog.js +14 -7
  6. package/dist/providers/solidgate/services/solidgate.service.js +7 -5
  7. package/dist/providers/stripe/components/SharedStripeCheckoutV2Dialog.d.ts +3 -0
  8. package/dist/providers/stripe/components/SharedStripeCheckoutV2Form.internal.d.ts +1 -1
  9. package/dist/providers/stripe/components/SharedStripeCheckoutV2Form.internal.js +65 -46
  10. package/dist/providers/stripe/components/SharedStripeCheckoutV2Summary.internal.d.ts +2 -1
  11. package/dist/providers/stripe/components/SharedStripeCheckoutV2Summary.internal.js +2 -2
  12. package/dist/providers/stripe/components/StripeCheckoutExpressCheckoutButton.d.ts +3 -1
  13. package/dist/providers/stripe/components/StripeCheckoutExpressCheckoutButton.js +44 -4
  14. package/dist/providers/stripe/components/checkout-v2.content.d.ts +14 -0
  15. package/dist/providers/stripe/components/checkout-v2.content.js +13 -0
  16. package/dist/providers/stripe/hooks/useStripeOneTimeCheckoutSession.d.ts +3 -2
  17. package/dist/providers/stripe/hooks/useStripeOneTimeCheckoutSession.js +3 -3
  18. package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.d.ts +3 -2
  19. package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.js +2 -2
  20. package/dist/providers/stripe/services/stripeClient.d.ts +2 -2
  21. package/dist/providers/stripe/services/stripeClient.js +5 -4
  22. package/package.json +3 -3
package/README.md CHANGED
@@ -47,6 +47,25 @@ mounts one official Solidgate form per attempt, destroys the remote form on repl
47
47
  and never treats iframe `success` as funnel completion. Its `onSuccess` callback runs only after the
48
48
  provider-neutral status endpoint finds canonical shared-ledger payment evidence.
49
49
 
50
+ ## Localized funnel copy
51
+
52
+ Funnel content files own translated strings. Pass a complete `SharedStripeCheckoutV2Copy`
53
+ as the dialog's `copy` prop to translate its method tabs, country label and payment
54
+ status/fallback messages. Existing callers keep the English defaults. Stripe-owned
55
+ fields and provider errors use the `stripeLocale` option on both checkout-session
56
+ hooks; wallet surfaces accept it through their `checkout` input. Stripe clients are
57
+ cached by publishable key and locale so changing language cannot reuse another
58
+ language's client. Locale does not change a plan, currency or billing interval.
59
+
60
+ `ManageSubscriptionContent` exported by this package extends the runtime content
61
+ with optional `labels: ManageSubscriptionLabels`. A localized funnel should make
62
+ `labels` required in its own content type and provide the complete English and
63
+ translated objects. The labels own section/status text, date locale and interval
64
+ templates (`{count}`), while subscription data and cancellation behavior remain
65
+ unchanged. Set `errorMessagePolicy: 'content'` to render load/cancel failures from
66
+ the content's `errorMessages`. The default `'server'` policy preserves existing
67
+ error rendering independently of the selected labels.
68
+
50
69
  ## Advanced Wallet Primitives
51
70
 
52
71
  `ApplePaySubscriptionCheckoutSlot`, `GooglePaySubscriptionCheckoutSlot`, and `WalletSubscriptionCheckoutSlot` stay exported and runtime-compatible. They are backward-compatible advanced low-level primitives: reach for them only when a placement needs wiring the shared wallet surfaces do not cover. Never share one checkout hook result across placements; give each visible placement its own session.
@@ -1,6 +1,31 @@
1
- import { type ManageSubscriptionContent, type ManageSubscriptionsResponse } from '@funnelsgrove/runtime';
1
+ import { type ManageSubscriptionContent as RuntimeManageSubscriptionContent, type ManageSubscriptionsResponse } from '@funnelsgrove/runtime';
2
2
  import type { BillingFallbackPlanConfig, BillingPlanCatalog } from '../config/billing.config.js';
3
- export type { ManageSubscriptionContent } from '@funnelsgrove/runtime';
3
+ export type ManageSubscriptionLabels = {
4
+ locale: string;
5
+ unavailableDate: string;
6
+ activeTitle: string;
7
+ cancelledTitle: string;
8
+ pastTitle: string;
9
+ pastNote: string;
10
+ cancelledNote: string;
11
+ subscriptionNumber: string;
12
+ subscriptionFallback: string;
13
+ activeUntil: string;
14
+ endedOn: string;
15
+ historicalEnd: string;
16
+ nextCharge: string;
17
+ createdAt: string;
18
+ cancelledStatus: string;
19
+ unknownStatus: string;
20
+ statuses: Readonly<Record<string, string>>;
21
+ intervals: Readonly<Record<string, string>>;
22
+ };
23
+ export type ManageSubscriptionContent = RuntimeManageSubscriptionContent & {
24
+ /** Complete localized presentation; omitted by existing English funnels. */
25
+ labels?: ManageSubscriptionLabels;
26
+ /** Use authored errorMessages instead of raw server exception text. */
27
+ errorMessagePolicy?: 'content' | 'server';
28
+ };
4
29
  export type ManageSubscriptionScreenProps = {
5
30
  stepId: string;
6
31
  content: ManageSubscriptionContent;
@@ -24,6 +49,7 @@ export declare const __manageSubscriptionScreenTestables: {
24
49
  resolveBillingDateLabel: (subscription: Pick<ManageSubscription, "cancelAtPeriodEnd" | "currentPeriodEnd" | "status">, options?: {
25
50
  cancelled?: boolean;
26
51
  past?: boolean;
27
- }) => string;
28
- resolveSubscriptionStatusLabel: (subscription: ManageSubscription) => string;
52
+ }, labels?: ManageSubscriptionLabels) => string;
53
+ resolveSubscriptionStatusLabel: (subscription: ManageSubscription, labels?: ManageSubscriptionLabels) => string;
29
54
  };
55
+ export {};
@@ -13,15 +13,16 @@ const isCancelledActiveSubscription = (subscription) => {
13
13
  const isCancellableSubscription = (subscription) => {
14
14
  return getNormalizedStatus(subscription) !== 'canceled' && !subscription.cancelAtPeriodEnd;
15
15
  };
16
- const formatDate = (value) => {
16
+ const formatDate = (value, labels) => {
17
+ var _a, _b, _c;
17
18
  if (!value) {
18
- return 'n/a';
19
+ return (_a = labels === null || labels === void 0 ? void 0 : labels.unavailableDate) !== null && _a !== void 0 ? _a : 'n/a';
19
20
  }
20
21
  const timestamp = Date.parse(value);
21
22
  if (!Number.isFinite(timestamp)) {
22
- return 'n/a';
23
+ return (_b = labels === null || labels === void 0 ? void 0 : labels.unavailableDate) !== null && _b !== void 0 ? _b : 'n/a';
23
24
  }
24
- return new Intl.DateTimeFormat('en-US', {
25
+ return new Intl.DateTimeFormat((_c = labels === null || labels === void 0 ? void 0 : labels.locale) !== null && _c !== void 0 ? _c : 'en-US', {
25
26
  month: 'numeric',
26
27
  day: 'numeric',
27
28
  year: 'numeric',
@@ -32,9 +33,12 @@ const formatSubscriptionNumber = (subscription) => {
32
33
  const normalized = source.replace(/[^a-zA-Z0-9]/g, '');
33
34
  return normalized.slice(-7) || source.slice(-7) || subscription.id;
34
35
  };
35
- const formatInterval = (interval, count) => {
36
+ const formatInterval = (interval, count, labels) => {
36
37
  const normalizedCount = count && Number.isFinite(count) && count > 0 ? count : 1;
37
38
  const normalizedInterval = (interval === null || interval === void 0 ? void 0 : interval.trim()) || 'month';
39
+ const translated = labels === null || labels === void 0 ? void 0 : labels.intervals[normalizedInterval];
40
+ if (translated)
41
+ return translated.replace('{count}', String(normalizedCount));
38
42
  const pluralSuffix = normalizedCount === 1 ? '' : 's';
39
43
  return `${normalizedCount} ${normalizedInterval}${pluralSuffix}`;
40
44
  };
@@ -42,46 +46,54 @@ const formatMoney = (amountCents, currency) => {
42
46
  const normalizedCurrency = (currency === null || currency === void 0 ? void 0 : currency.trim().toLowerCase()) || 'usd';
43
47
  return `${(amountCents / 100).toFixed(2)} ${normalizedCurrency}`;
44
48
  };
45
- const resolvePlanLine = (subscription, plans) => {
49
+ const resolvePlanLine = (subscription, plans, labels) => {
46
50
  const plan = plans.find((item) => item.providerPlanId === subscription.providerPlanId);
47
51
  if (plan) {
48
- return `${formatMoney(plan.amountCents, 'usd')} / ${formatInterval(plan.billingInterval, 1)}`;
52
+ return `${formatMoney(plan.amountCents, 'usd')} / ${formatInterval(plan.billingInterval, 1, labels)}`;
49
53
  }
50
54
  if (typeof subscription.amountCents === 'number' && subscription.amountCents > 0) {
51
- return `${formatMoney(subscription.amountCents, subscription.currency)} / ${formatInterval(subscription.billingInterval, subscription.billingIntervalCount)}`;
55
+ return `${formatMoney(subscription.amountCents, subscription.currency)} / ${formatInterval(subscription.billingInterval, subscription.billingIntervalCount, labels)}`;
52
56
  }
53
- return `${subscription.environment.toUpperCase()} subscription`;
57
+ return labels
58
+ ? labels.subscriptionFallback.replace('{environment}', subscription.environment.toUpperCase())
59
+ : `${subscription.environment.toUpperCase()} subscription`;
54
60
  };
55
- const resolveCreatedDate = (subscription) => {
56
- return formatDate(subscription.createdAt || subscription.currentPeriodStart);
61
+ const resolveCreatedDate = (subscription, labels) => {
62
+ return formatDate(subscription.createdAt || subscription.currentPeriodStart, labels);
57
63
  };
58
- const resolveBillingDateLabel = (subscription, options = {}) => {
64
+ const resolveBillingDateLabel = (subscription, options = {}, labels) => {
65
+ var _a, _b, _c, _d;
59
66
  const normalizedStatus = getNormalizedStatus(subscription);
60
67
  if (options.cancelled || subscription.cancelAtPeriodEnd) {
61
- return `Active until: ${formatDate(subscription.currentPeriodEnd)}`;
68
+ return `${(_a = labels === null || labels === void 0 ? void 0 : labels.activeUntil) !== null && _a !== void 0 ? _a : 'Active until:'} ${formatDate(subscription.currentPeriodEnd, labels)}`;
62
69
  }
63
70
  if (options.past && normalizedStatus === 'canceled') {
64
- return `Ended on: ${formatDate(subscription.currentPeriodEnd)}`;
71
+ return `${(_b = labels === null || labels === void 0 ? void 0 : labels.endedOn) !== null && _b !== void 0 ? _b : 'Ended on:'} ${formatDate(subscription.currentPeriodEnd, labels)}`;
65
72
  }
66
73
  if (options.past) {
67
- return `Historical period ended: ${formatDate(subscription.currentPeriodEnd)}`;
74
+ return `${(_c = labels === null || labels === void 0 ? void 0 : labels.historicalEnd) !== null && _c !== void 0 ? _c : 'Historical period ended:'} ${formatDate(subscription.currentPeriodEnd, labels)}`;
68
75
  }
69
- return `Next charge: ${formatDate(subscription.currentPeriodEnd)}`;
76
+ return `${(_d = labels === null || labels === void 0 ? void 0 : labels.nextCharge) !== null && _d !== void 0 ? _d : 'Next charge:'} ${formatDate(subscription.currentPeriodEnd, labels)}`;
70
77
  };
71
78
  const toManageSubscriptions = (subscriptions) => subscriptions;
72
- const resolveSubscriptionStatusLabel = (subscription) => {
79
+ const resolveSubscriptionStatusLabel = (subscription, labels) => {
80
+ var _a, _b, _c;
73
81
  if (isCancelledActiveSubscription(subscription)) {
74
- return 'Cancelled';
82
+ return (_a = labels === null || labels === void 0 ? void 0 : labels.cancelledStatus) !== null && _a !== void 0 ? _a : 'Cancelled';
75
83
  }
76
84
  const normalizedStatus = getNormalizedStatus(subscription);
77
85
  if (!normalizedStatus) {
78
- return 'Active';
86
+ return labels ? (_b = labels.statuses.active) !== null && _b !== void 0 ? _b : labels.unknownStatus : 'Active';
79
87
  }
88
+ if (labels)
89
+ return (_c = labels.statuses[normalizedStatus]) !== null && _c !== void 0 ? _c : labels.unknownStatus;
80
90
  return `${normalizedStatus.charAt(0).toUpperCase()}${normalizedStatus.slice(1)}`;
81
91
  };
82
92
  export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId, plans, }) {
83
- var _a, _b;
93
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
84
94
  const { attributes, goToStep, setAnswer, setUser, user } = useFunnel();
95
+ const labels = content.labels;
96
+ const useContentErrors = content.errorMessagePolicy === 'content';
85
97
  const { config: publishedRuntimeConfig } = useFunnelRuntimeConfig();
86
98
  const paymentsApiVersion = publishedRuntimeConfig === null || publishedRuntimeConfig === void 0 ? void 0 : publishedRuntimeConfig.paymentsApiVersion;
87
99
  const supportEmail = runtimePublicConfig.supportEmail;
@@ -115,7 +127,7 @@ export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId,
115
127
  }
116
128
  catch (nextError) {
117
129
  if (active) {
118
- setError(nextError instanceof Error ? nextError.message : content.errorMessages.load);
130
+ setError(!useContentErrors && nextError instanceof Error ? nextError.message : content.errorMessages.load);
119
131
  }
120
132
  }
121
133
  finally {
@@ -128,7 +140,7 @@ export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId,
128
140
  return () => {
129
141
  active = false;
130
142
  };
131
- }, [content.errorMessages.load, paymentsApiVersion]);
143
+ }, [content.errorMessages.load, useContentErrors, paymentsApiVersion]);
132
144
  const subscriptions = useMemo(() => { var _a; return toManageSubscriptions((_a = data === null || data === void 0 ? void 0 : data.subscriptions) !== null && _a !== void 0 ? _a : []); }, [data]);
133
145
  const activeSubscriptions = useMemo(() => subscriptions.filter(isCancellableSubscription), [subscriptions]);
134
146
  const cancelledSubscriptions = useMemo(() => subscriptions.filter(isCancelledActiveSubscription), [subscriptions]);
@@ -188,7 +200,7 @@ export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId,
188
200
  });
189
201
  setData(payload);
190
202
  const refreshedSubscription = toManageSubscriptions(payload.subscriptions).find((subscription) => subscription.id === selectedSubscription.id);
191
- setCancelledActiveUntilDate(formatDate((refreshedSubscription === null || refreshedSubscription === void 0 ? void 0 : refreshedSubscription.currentPeriodEnd) || selectedSubscription.currentPeriodEnd));
203
+ setCancelledActiveUntilDate(formatDate((refreshedSubscription === null || refreshedSubscription === void 0 ? void 0 : refreshedSubscription.currentPeriodEnd) || selectedSubscription.currentPeriodEnd, labels));
192
204
  await persistCancellationAnswers({
193
205
  cancelled: true,
194
206
  reasonId: selectedReason.id,
@@ -198,17 +210,17 @@ export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId,
198
210
  setStage('done');
199
211
  }
200
212
  catch (nextError) {
201
- setError(nextError instanceof Error ? nextError.message : content.errorMessages.cancel);
213
+ setError(!useContentErrors && nextError instanceof Error ? nextError.message : content.errorMessages.cancel);
202
214
  }
203
215
  finally {
204
216
  setCancelInFlight(false);
205
217
  }
206
218
  };
207
- return (_jsxs(_Fragment, { children: [_jsxs("section", { className: 'manage-subscription', "data-node-id": nodeId || stepId, children: [stage === 'subscriptions' ? (_jsxs("div", { className: 'manage-subscription-inner', children: [_jsx("h1", { className: 'manage-subscription-title', children: content.subscriptionsStage.title }), loading ? (_jsx("p", { className: 'manage-subscription-muted', children: content.subscriptionsStage.loadingLabel })) : null, error ? _jsx("p", { className: 'manage-subscription-error', children: error }) : null, !loading && !error && subscriptions.length === 0 ? (_jsx("p", { className: 'manage-subscription-empty', children: content.subscriptionsStage.emptyLabel })) : null, !loading && activeSubscriptions.length > 0 ? (_jsx(SubscriptionSection, { title: 'Active Subscriptions', subscriptions: activeSubscriptions, plans: planList, onSelect: handleSubscriptionSelected })) : null, !loading && cancelledSubscriptions.length > 0 ? (_jsx(SubscriptionSection, { title: 'Cancelled Subscriptions', subscriptions: cancelledSubscriptions, plans: planList, onSelect: handleSubscriptionSelected, description: cancelledSubscriptionsNote, cancelled: true })) : null, !loading && pastSubscriptions.length > 0 ? (_jsx(SubscriptionSection, { title: 'Past Subscriptions', subscriptions: pastSubscriptions, plans: planList, onSelect: handleSubscriptionSelected, description: pastSubscriptionsNote, past: true })) : null, _jsxs("p", { className: 'manage-subscription-support', children: [content.subscriptionsStage.supportPrefix, ' ', _jsx("a", { href: `mailto:${supportEmail}`, children: supportEmail })] })] })) : null, stage === 'why' ? (_jsxs("div", { className: 'manage-subscription-inner', children: [_jsx("h1", { className: 'manage-subscription-title is-reason', children: content.whyStage.title }), _jsx("ul", { className: 'manage-subscription-reasons', role: 'list', "aria-label": content.whyStage.reasonsAriaLabel, children: content.whyStage.reasons.map((reason) => (_jsx("li", { children: _jsxs("button", { type: 'button', className: 'manage-subscription-reason', onClick: () => handleReasonSelected(reason.id), children: [_jsx("span", { className: 'manage-subscription-reason-icon', "aria-hidden": true, children: reason.icon }), _jsx("span", { children: reason.label }), _jsx("span", { className: 'manage-subscription-reason-arrow', "aria-hidden": true, children: ">" })] }) }, reason.id))) })] })) : null, stage === 'confirm' ? (_jsxs("div", { className: 'manage-subscription-inner is-short', children: [_jsx("h1", { className: 'manage-subscription-title', children: content.confirmStage.title }), error ? _jsx("p", { className: 'manage-subscription-error', children: error }) : null, _jsx("button", { type: 'button', className: 'manage-subscription-primary', disabled: !selectedSubscription || !selectedReason || cancelInFlight, onClick: () => {
219
+ return (_jsxs(_Fragment, { children: [_jsxs("section", { className: 'manage-subscription', "data-node-id": nodeId || stepId, children: [stage === 'subscriptions' ? (_jsxs("div", { className: 'manage-subscription-inner', children: [_jsx("h1", { className: 'manage-subscription-title', children: content.subscriptionsStage.title }), loading ? (_jsx("p", { className: 'manage-subscription-muted', children: content.subscriptionsStage.loadingLabel })) : null, error ? _jsx("p", { className: 'manage-subscription-error', children: error }) : null, !loading && !error && subscriptions.length === 0 ? (_jsx("p", { className: 'manage-subscription-empty', children: content.subscriptionsStage.emptyLabel })) : null, !loading && activeSubscriptions.length > 0 ? (_jsx(SubscriptionSection, { title: (_c = labels === null || labels === void 0 ? void 0 : labels.activeTitle) !== null && _c !== void 0 ? _c : 'Active Subscriptions', subscriptions: activeSubscriptions, plans: planList, labels: labels, onSelect: handleSubscriptionSelected })) : null, !loading && cancelledSubscriptions.length > 0 ? (_jsx(SubscriptionSection, { title: (_d = labels === null || labels === void 0 ? void 0 : labels.cancelledTitle) !== null && _d !== void 0 ? _d : 'Cancelled Subscriptions', subscriptions: cancelledSubscriptions, plans: planList, labels: labels, onSelect: handleSubscriptionSelected, description: (_e = labels === null || labels === void 0 ? void 0 : labels.cancelledNote) !== null && _e !== void 0 ? _e : cancelledSubscriptionsNote, cancelled: true })) : null, !loading && pastSubscriptions.length > 0 ? (_jsx(SubscriptionSection, { title: (_f = labels === null || labels === void 0 ? void 0 : labels.pastTitle) !== null && _f !== void 0 ? _f : 'Past Subscriptions', subscriptions: pastSubscriptions, plans: planList, labels: labels, onSelect: handleSubscriptionSelected, description: (_g = labels === null || labels === void 0 ? void 0 : labels.pastNote) !== null && _g !== void 0 ? _g : pastSubscriptionsNote, past: true })) : null, _jsxs("p", { className: 'manage-subscription-support', children: [content.subscriptionsStage.supportPrefix, ' ', _jsx("a", { href: `mailto:${supportEmail}`, children: supportEmail })] })] })) : null, stage === 'why' ? (_jsxs("div", { className: 'manage-subscription-inner', children: [_jsx("h1", { className: 'manage-subscription-title is-reason', children: content.whyStage.title }), _jsx("ul", { className: 'manage-subscription-reasons', role: 'list', "aria-label": content.whyStage.reasonsAriaLabel, children: content.whyStage.reasons.map((reason) => (_jsx("li", { children: _jsxs("button", { type: 'button', className: 'manage-subscription-reason', onClick: () => handleReasonSelected(reason.id), children: [_jsx("span", { className: 'manage-subscription-reason-icon', "aria-hidden": true, children: reason.icon }), _jsx("span", { children: reason.label }), _jsx("span", { className: 'manage-subscription-reason-arrow', "aria-hidden": true, children: ">" })] }) }, reason.id))) })] })) : null, stage === 'confirm' ? (_jsxs("div", { className: 'manage-subscription-inner is-short', children: [_jsx("h1", { className: 'manage-subscription-title', children: content.confirmStage.title }), error ? _jsx("p", { className: 'manage-subscription-error', children: error }) : null, _jsx("button", { type: 'button', className: 'manage-subscription-primary', disabled: !selectedSubscription || !selectedReason || cancelInFlight, onClick: () => {
208
220
  void handleCancelSubscription();
209
221
  }, children: cancelInFlight
210
222
  ? content.confirmStage.cancellingLabel
211
- : content.confirmStage.cancelLabel })] })) : null, stage === 'done' ? (_jsxs("div", { className: 'manage-subscription-inner is-short', children: [_jsx("h1", { className: 'manage-subscription-title', children: content.doneStage.title }), _jsx("p", { className: 'manage-subscription-done-status', children: "Cancelled" }), _jsx("p", { className: 'manage-subscription-done-copy', children: content.doneStage.cancelledMessage }), cancelledActiveUntilDate ? (_jsxs("p", { className: 'manage-subscription-done-copy is-active-until', children: ["Active until: ", cancelledActiveUntilDate] })) : null, _jsx("button", { type: 'button', className: 'manage-subscription-primary', onClick: () => {
223
+ : content.confirmStage.cancelLabel })] })) : null, stage === 'done' ? (_jsxs("div", { className: 'manage-subscription-inner is-short', children: [_jsx("h1", { className: 'manage-subscription-title', children: content.doneStage.title }), _jsx("p", { className: 'manage-subscription-done-status', children: (_h = labels === null || labels === void 0 ? void 0 : labels.cancelledStatus) !== null && _h !== void 0 ? _h : 'Cancelled' }), _jsx("p", { className: 'manage-subscription-done-copy', children: content.doneStage.cancelledMessage }), cancelledActiveUntilDate ? (_jsxs("p", { className: 'manage-subscription-done-copy is-active-until', children: [(_j = labels === null || labels === void 0 ? void 0 : labels.activeUntil) !== null && _j !== void 0 ? _j : 'Active until:', " ", cancelledActiveUntilDate] })) : null, _jsx("button", { type: 'button', className: 'manage-subscription-primary', onClick: () => {
212
224
  if (homeStepId) {
213
225
  goToStep(homeStepId, { type: 'exit', reason: 'cancel' });
214
226
  return;
@@ -216,16 +228,19 @@ export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId,
216
228
  setStage('subscriptions');
217
229
  }, children: content.doneStage.returnHomeLabel })] })) : null] }), _jsx("style", { children: manageSubscriptionStyles })] }));
218
230
  }
219
- function SubscriptionSection({ title, subscriptions, plans, onSelect, description, cancelled = false, past = false, }) {
231
+ function SubscriptionSection({ title, subscriptions, plans, onSelect, description, labels, cancelled = false, past = false, }) {
220
232
  const isDisabled = cancelled || past;
221
233
  const rowClassName = cancelled
222
234
  ? 'manage-subscription-row is-cancelled'
223
235
  : past
224
236
  ? 'manage-subscription-row is-past'
225
237
  : 'manage-subscription-row';
226
- return (_jsxs("section", { className: 'manage-subscription-section', "aria-label": title, children: [_jsx("h2", { children: title }), description ? _jsx("p", { className: 'manage-subscription-section-note', children: description }) : null, _jsx("ul", { className: 'manage-subscription-list', role: 'list', children: subscriptions.map((subscription) => (_jsx("li", { children: _jsxs("button", { type: 'button', className: rowClassName, disabled: isDisabled, onClick: () => onSelect(subscription), children: [_jsx("span", { className: 'manage-subscription-radio', "aria-hidden": true }), _jsxs("span", { className: 'manage-subscription-row-heading', children: [_jsxs("span", { className: 'manage-subscription-row-title', children: ["Subscription #", formatSubscriptionNumber(subscription)] }), _jsx("span", { className: 'manage-subscription-row-status', children: resolveSubscriptionStatusLabel(subscription) })] }), _jsx("span", { className: 'manage-subscription-row-plan', children: resolvePlanLine(subscription, plans) }), _jsxs("span", { className: 'manage-subscription-row-meta', children: [_jsx("span", { className: cancelled ? 'is-active-until' : undefined, children: cancelled
227
- ? resolveBillingDateLabel(subscription, { cancelled })
228
- : resolveBillingDateLabel(subscription, { past }) }), _jsxs("span", { children: ["Created at: ", resolveCreatedDate(subscription)] })] })] }) }, subscription.id))) })] }));
238
+ return (_jsxs("section", { className: 'manage-subscription-section', "aria-label": title, children: [_jsx("h2", { children: title }), description ? _jsx("p", { className: 'manage-subscription-section-note', children: description }) : null, _jsx("ul", { className: 'manage-subscription-list', role: 'list', children: subscriptions.map((subscription) => {
239
+ var _a, _b;
240
+ return (_jsx("li", { children: _jsxs("button", { type: 'button', className: rowClassName, disabled: isDisabled, onClick: () => onSelect(subscription), children: [_jsx("span", { className: 'manage-subscription-radio', "aria-hidden": true }), _jsxs("span", { className: 'manage-subscription-row-heading', children: [_jsx("span", { className: 'manage-subscription-row-title', children: ((_a = labels === null || labels === void 0 ? void 0 : labels.subscriptionNumber) !== null && _a !== void 0 ? _a : 'Subscription #{number}').replace('{number}', formatSubscriptionNumber(subscription)) }), _jsx("span", { className: 'manage-subscription-row-status', children: resolveSubscriptionStatusLabel(subscription, labels) })] }), _jsx("span", { className: 'manage-subscription-row-plan', children: resolvePlanLine(subscription, plans, labels) }), _jsxs("span", { className: 'manage-subscription-row-meta', children: [_jsx("span", { className: cancelled ? 'is-active-until' : undefined, children: cancelled
241
+ ? resolveBillingDateLabel(subscription, { cancelled }, labels)
242
+ : resolveBillingDateLabel(subscription, { past }, labels) }), _jsxs("span", { children: [(_b = labels === null || labels === void 0 ? void 0 : labels.createdAt) !== null && _b !== void 0 ? _b : 'Created at:', " ", resolveCreatedDate(subscription, labels)] })] })] }) }, subscription.id));
243
+ }) })] }));
229
244
  }
230
245
  export const __manageSubscriptionScreenTestables = {
231
246
  resolveBillingDateLabel,
package/dist/index.d.ts CHANGED
@@ -11,6 +11,6 @@ export * from './services/checkoutObservability.service.js';
11
11
  export * from './providers/paymentProvider.types.js';
12
12
  export * from './providers/stripe/index.js';
13
13
  export * from './providers/solidgate/index.js';
14
- export { ManageSubscriptionScreen, type ManageSubscriptionContent, type ManageSubscriptionScreenProps, } from './components/ManageSubscriptionScreen.js';
14
+ export { ManageSubscriptionScreen, type ManageSubscriptionContent, type ManageSubscriptionScreenProps, type ManageSubscriptionLabels, } from './components/ManageSubscriptionScreen.js';
15
15
  export { isCheckoutEmailError, isValidCheckoutEmail } from './services/checkoutEmail.js';
16
16
  export * from './components/CheckoutEmailDialog.js';
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { completePaywallCheckout } from '@funnelsgrove/runtime';
3
4
  import { publicAnalyticsSdk } from '@funnelsgrove/analytics';
4
5
  import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
5
6
  import { pollSolidgateCheckoutStatus, prepareSolidgateCheckout, } from '../services/solidgate.service.js';
@@ -41,7 +42,7 @@ const trackSolidgateCheckoutEvent = async (eventName, attemptId, context) => {
41
42
  return;
42
43
  }
43
44
  const input = {
44
- eventId: eventName === 'checkout_started' ? attemptId : `add_payment_info:${attemptId}`,
45
+ eventId: eventName === 'add_payment_info' ? `add_payment_info:${attemptId}` : attemptId,
45
46
  featureFlags: context.featureFlags,
46
47
  stepId: context.stepId,
47
48
  stepName: context.stepName,
@@ -52,7 +53,9 @@ const trackSolidgateCheckoutEvent = async (eventName, attemptId, context) => {
52
53
  try {
53
54
  const trackedEventId = eventName === 'checkout_started'
54
55
  ? publicAnalyticsSdk.trackCheckoutStarted(input)
55
- : publicAnalyticsSdk.trackPaymentInfoSubmitted(input);
56
+ : eventName === 'checkout_completed'
57
+ ? publicAnalyticsSdk.trackCheckoutCompleted(input)
58
+ : publicAnalyticsSdk.trackPaymentInfoSubmitted(input);
56
59
  if (trackedEventId) {
57
60
  await publicAnalyticsSdk.flush().catch(() => 0);
58
61
  }
@@ -269,11 +272,15 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
269
272
  return;
270
273
  }
271
274
  if (result.status === 'succeeded') {
272
- setState({ state: 'success', checkout, canResumeVerification: false });
273
275
  if (successAttemptRef.current !== checkout.attemptId) {
276
+ const embedded = await completePaywallCheckout('solidgate', checkout.attemptId, {
277
+ beforeNotify: () => trackSolidgateCheckoutEvent('checkout_completed', checkout.attemptId, checkoutAnalyticsRef.current),
278
+ });
279
+ if (!embedded)
280
+ await ((_b = onSuccessRef.current) === null || _b === void 0 ? void 0 : _b.call(onSuccessRef, checkout));
274
281
  successAttemptRef.current = checkout.attemptId;
275
- await ((_b = onSuccessRef.current) === null || _b === void 0 ? void 0 : _b.call(onSuccessRef, checkout));
276
282
  }
283
+ setState({ state: 'success', checkout, canResumeVerification: false });
277
284
  }
278
285
  else if (result.status === 'expired') {
279
286
  setState({ state: 'expired', checkout, canResumeVerification: false });
@@ -291,8 +298,8 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
291
298
  setState({ state: 'verifying', checkout, canResumeVerification: result.timedOut });
292
299
  }
293
300
  }
294
- catch (error) {
295
- if (!controller.signal.aborted && !isAbortError(error)) {
301
+ catch (_e) {
302
+ if (!controller.signal.aborted) {
296
303
  setState({ state: 'recoverable_error', checkout, canResumeVerification: true });
297
304
  trackFailure(copyRef.current.error, 'payment_verification', checkout.attemptId);
298
305
  (_d = onErrorRef.current) === null || _d === void 0 ? void 0 : _d.call(onErrorRef, copyRef.current.error);
@@ -385,5 +392,5 @@ export function SolidgateCheckoutDialog({ open, request, customerEmail, checkout
385
392
  setState(Object.assign(Object.assign({}, view), { state: 'recoverable_error', canResumeVerification: false }));
386
393
  trackFailure(copyRef.current.error, 'payment_form', (_a = view.checkout) === null || _a === void 0 ? void 0 : _a.attemptId);
387
394
  (_b = onErrorRef.current) === null || _b === void 0 ? void 0 : _b.call(onErrorRef, copyRef.current.error);
388
- } }, view.checkout.attemptId)) : null, presentation && view.checkout && showForm ? (_jsx("p", { className: 'solidgate-checkout-secure', children: presentation.secureLabel })) : null, statusMessage && view.state !== 'ready' ? (_jsx("div", { className: 'solidgate-checkout-status', children: _jsx("p", { role: 'status', "aria-live": 'polite', children: statusMessage }) })) : null, view.state === 'verifying' && view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => void verifyCanonicalStatus(), className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.resumeVerification })) : null, (view.state === 'recoverable_error' || view.state === 'expired') && onRetry ? (_jsx("button", { type: 'button', onClick: onRetry, className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.retry })) : null] })) : null] }), _jsx("style", { children: solidgateCheckoutDialogStyles })] }));
395
+ } }, view.checkout.attemptId)) : null, presentation && view.checkout && showForm ? (_jsx("p", { className: 'solidgate-checkout-secure', children: presentation.secureLabel })) : null, statusMessage && view.state !== 'ready' ? (_jsx("div", { className: 'solidgate-checkout-status', children: _jsx("p", { role: 'status', "aria-live": 'polite', children: statusMessage }) })) : null, view.canResumeVerification ? (_jsx("button", { type: 'button', onClick: () => void verifyCanonicalStatus(), className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.resumeVerification })) : null, (view.state === 'recoverable_error' || view.state === 'expired') && !view.canResumeVerification && onRetry ? (_jsx("button", { type: 'button', onClick: onRetry, className: 'solidgate-checkout-action solidgate-checkout-action--spaced', children: copy.retry })) : null] })) : null] }), _jsx("style", { children: solidgateCheckoutDialogStyles })] }));
389
396
  }
@@ -1,4 +1,4 @@
1
- import { FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, buildMainApiUrl, resolvePreviewPaymentPublishableKey, } from '@funnelsgrove/runtime';
1
+ import { paywallCheckoutReturnUrl, retainPaywallCheckout, FUNNEL_ID, FUNNEL_SDK_PUBLISHABLE_KEY, buildMainApiUrl, resolvePreviewPaymentPublishableKey, } from '@funnelsgrove/runtime';
2
2
  const trimTrailingSlash = (value) => value.replace(/\/+$/, '');
3
3
  const requiredString = (value, name) => {
4
4
  const normalized = typeof value === 'string' ? value.trim() : '';
@@ -118,7 +118,7 @@ export const prepareSolidgateCheckout = async (input) => {
118
118
  if (!paymentProfileId || !providerPlanId || !checkoutType || !environment) {
119
119
  throw new Error('Solidgate V2 checkout selection is incomplete');
120
120
  }
121
- return parsePreparation(await postJson('/sdk/public/v2/payments/checkout-sessions', {
121
+ const result = parsePreparation(await postJson('/sdk/public/v2/payments/checkout-sessions', {
122
122
  checkoutType,
123
123
  uiMode: 'custom',
124
124
  environment,
@@ -131,10 +131,12 @@ export const prepareSolidgateCheckout = async (input) => {
131
131
  offerSetId: requiredString(input.offerSetId, 'offerSetId'),
132
132
  idempotencyKey: requiredString(input.idempotencyKey, 'idempotencyKey'),
133
133
  couponId: ((_c = input.discountKeyOrProviderId) === null || _c === void 0 ? void 0 : _c.trim()) || null,
134
- returnUrl: requiredString(input.returnUrl, 'returnUrl'),
135
- successUrl: requiredString(input.successUrl, 'successUrl'),
136
- failUrl: requiredString(input.failUrl, 'failUrl'),
134
+ returnUrl: paywallCheckoutReturnUrl(requiredString(input.returnUrl, 'returnUrl'), 'solidgate'),
135
+ successUrl: paywallCheckoutReturnUrl(requiredString(input.successUrl, 'successUrl'), 'solidgate'),
136
+ failUrl: paywallCheckoutReturnUrl(requiredString(input.failUrl, 'failUrl'), 'solidgate', true),
137
137
  }, input.runtimeConfig, input.signal));
138
+ retainPaywallCheckout('solidgate', result.attemptId);
139
+ return result;
138
140
  };
139
141
  export const retrieveSolidgateCheckoutStatus = async (input) => {
140
142
  const attemptId = requiredString(input.attemptId, 'attemptId');
@@ -1,11 +1,14 @@
1
1
  import { type CSSProperties } from 'react';
2
2
  import type { Stripe } from '@stripe/stripe-js';
3
3
  import { type StripeSubscriptionCheckoutAnalyticsContext } from '../services/checkoutCompletionAnalytics.service.js';
4
+ import type { SharedStripeCheckoutV2Copy } from './checkout-v2.content.js';
5
+ export type { SharedStripeCheckoutV2Copy } from './checkout-v2.content.js';
4
6
  export type SharedCheckoutV2CssVars = CSSProperties & {
5
7
  '--shared-checkout-v2-theme-color': string;
6
8
  '--shared-checkout-v2-button-color': string;
7
9
  };
8
10
  export type SharedStripeCheckoutV2DialogProps = {
11
+ copy?: SharedStripeCheckoutV2Copy;
9
12
  amountCents: number;
10
13
  buttonColor: string;
11
14
  cardSubmitLabel: string;
@@ -3,5 +3,5 @@ export declare const getFixedSupportedBillingCountry: (supportedCountries: reado
3
3
  type SharedStripeCheckoutV2FormProps = Omit<SharedStripeCheckoutV2DialogProps, 'stripePromise' | 'onClose'> & {
4
4
  onClose: () => void;
5
5
  };
6
- export declare function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, checkoutAnalytics, checkoutSessionId, customerEmail, customerEmailEditable: allowEmailEditing, customerEmailLabel, customerEmailPlaceholder, customerEmailInvalidMessage, customerName, discountAmountLabel, discountLabel, discountPercent, initialWalletAvailable, onClose, onCustomerEmailChange, onCustomerEmailCommit, onError, onInactiveCheckoutSession, onPaymentInfoSubmitted, onSuccess, paymentMethodLabels, promoActive, processingLabel, promoCode, promoCodeLabel, remainingSeconds, returnUrl, savedLabel, secureLabel, satisfactionLabel, satisfactionPercent, summaryLabel, supportedCountries, themeColor, title, totalLabel, totalValue, unsupportedCountryMessage, walletButtonLabel, originalPriceLabel, originalPriceValue, variant, }: SharedStripeCheckoutV2FormProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function SharedStripeCheckoutV2CheckoutSessionForm({ copy, amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, checkoutAnalytics, checkoutSessionId, customerEmail, customerEmailEditable: allowEmailEditing, customerEmailLabel, customerEmailPlaceholder, customerEmailInvalidMessage, customerName, discountAmountLabel, discountLabel, discountPercent, initialWalletAvailable, onClose, onCustomerEmailChange, onCustomerEmailCommit, onError, onInactiveCheckoutSession, onPaymentInfoSubmitted, onSuccess, paymentMethodLabels, promoActive, processingLabel, promoCode, promoCodeLabel, remainingSeconds, returnUrl, savedLabel, secureLabel, satisfactionLabel, satisfactionPercent, summaryLabel, supportedCountries, themeColor, title, totalLabel, totalValue, unsupportedCountryMessage, walletButtonLabel, originalPriceLabel, originalPriceValue, variant, }: SharedStripeCheckoutV2FormProps): import("react/jsx-runtime").JSX.Element;
7
7
  export {};
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { completePaywallCheckout } from '@funnelsgrove/runtime';
3
4
  import { useEffect, useMemo, useRef, useState } from 'react';
4
5
  import { PaymentElement, useCheckout } from '@stripe/react-stripe-js/checkout';
5
6
  import { isValidCheckoutEmail as isValidEmail, isCheckoutEmailError } from '../../../services/checkoutEmail.js';
@@ -12,18 +13,16 @@ import { trackPaidStripeSubscriptionCheckoutCompleted, trackStripeSubscriptionPa
12
13
  import { trackCheckoutFailureShown } from '../../../services/checkoutObservability.service.js';
13
14
  import { SharedStripeCheckoutV2Summary } from './SharedStripeCheckoutV2Summary.internal.js';
14
15
  import { useCheckoutEmail } from './useCheckoutEmail.internal.js';
16
+ import { defaultCheckoutV2Copy } from './checkout-v2.content.js';
15
17
  const defaultPaymentMethodLabels = ['Visa', 'Mastercard', 'Maestro', 'Discover'];
16
18
  export const getFixedSupportedBillingCountry = (supportedCountries) => supportedCountries.length === 1 ? supportedCountries[0] : null;
17
- const checkoutCountryLabels = {
18
- US: 'United States',
19
- };
20
- const getCheckoutCountryLabel = (countryCode) => { var _a; return (_a = checkoutCountryLabels[countryCode]) !== null && _a !== void 0 ? _a : countryCode; };
21
- function getFriendlyCardErrorMessage(error) {
19
+ const getCheckoutCountryLabel = (countryCode, copy) => { var _a; return (_a = copy.countryNames[countryCode]) !== null && _a !== void 0 ? _a : countryCode; };
20
+ function getFriendlyCardErrorMessage(error, fallback) {
22
21
  return error instanceof Error && error.message
23
22
  ? error.message
24
- : 'Payment failed. Please review your card details and try again.';
23
+ : fallback;
25
24
  }
26
- export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, checkoutAnalytics, checkoutSessionId, customerEmail, customerEmailEditable: allowEmailEditing = 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, supportedCountries, themeColor, title, totalLabel, totalValue, unsupportedCountryMessage, walletButtonLabel, originalPriceLabel, originalPriceValue, variant = 'standard', }) {
25
+ export function SharedStripeCheckoutV2CheckoutSessionForm({ copy = defaultCheckoutV2Copy, amountCents, buttonColor, cardSubmitLabel, closeAriaLabel, countdownLabel, checkoutAnalytics, checkoutSessionId, customerEmail, customerEmailEditable: allowEmailEditing = 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, supportedCountries, themeColor, title, totalLabel, totalValue, unsupportedCountryMessage, walletButtonLabel, originalPriceLabel, originalPriceValue, variant = 'standard', }) {
27
26
  const checkoutState = useCheckout();
28
27
  const walletPaymentMethods = usePlatformWalletPaymentMethods();
29
28
  const [selectedMethod, setSelectedMethod] = useState('wallet');
@@ -32,6 +31,8 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
32
31
  ? true
33
32
  : walletAvailable;
34
33
  const [submitting, setSubmitting] = useState(false);
34
+ const [completed, setCompleted] = useState(false);
35
+ const [providerConfirmed, setProviderConfirmed] = useState(false);
35
36
  const submitInFlightRef = useRef(false);
36
37
  const [paymentElementReady, setPaymentElementReady] = useState(false);
37
38
  const [paymentElementComplete, setPaymentElementComplete] = useState(false);
@@ -44,7 +45,7 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
44
45
  const normalizedSupportedCountries = useMemo(() => normalizeSupportedCheckoutCountries(supportedCountries), [supportedCountries]);
45
46
  const fixedBillingCountry = getFixedSupportedBillingCountry(normalizedSupportedCountries);
46
47
  const fixedBillingCountryLabel = fixedBillingCountry
47
- ? getCheckoutCountryLabel(fixedBillingCountry)
48
+ ? getCheckoutCountryLabel(fixedBillingCountry, copy)
48
49
  : null;
49
50
  const cardBrandLabels = useMemo(() => paymentMethodLabels.length > 0 ? paymentMethodLabels : defaultPaymentMethodLabels, [paymentMethodLabels]);
50
51
  const normalizedPromoCode = promoCode.trim();
@@ -164,11 +165,12 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
164
165
  setPaymentElementComplete(event.complete);
165
166
  setBillingCountry((_b = (_a = event.value.billingDetails) === null || _a === void 0 ? void 0 : _a.address.country) !== null && _b !== void 0 ? _b : fixedBillingCountry);
166
167
  };
167
- const trackCheckoutSuccess = async () => {
168
+ const trackCheckoutSuccess = async (embedded = false) => {
168
169
  if (checkoutAnalytics) {
169
170
  await trackPaidStripeSubscriptionCheckoutCompleted(Object.assign({ checkoutSessionId }, checkoutAnalytics));
170
171
  }
171
- await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess());
172
+ if (!embedded)
173
+ await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess());
172
174
  };
173
175
  const trackPaymentInfoSubmitted = async () => {
174
176
  if (checkoutAnalytics) {
@@ -187,23 +189,25 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
187
189
  }, [effectiveSelectedMethod]);
188
190
  const handleSubmit = async (event) => {
189
191
  event.preventDefault();
190
- if (submitInFlightRef.current || emailSessionChanged) {
192
+ if (submitInFlightRef.current || (emailSessionChanged && !providerConfirmed)) {
191
193
  return;
192
194
  }
193
- const billingCountryError = getBillingCountryError(billingCountry !== null && billingCountry !== void 0 ? billingCountry : fixedBillingCountry);
194
- if (billingCountryError) {
195
- setSharedError(billingCountryError);
196
- return;
197
- }
198
- if (checkoutState.type !== 'success') {
199
- setSharedError(checkoutState.type === 'error'
200
- ? checkoutState.error.message
201
- : 'Payment form is not ready yet.');
202
- return;
203
- }
204
- if (!paymentElementReady) {
205
- setSharedError('Payment form is not ready yet.', true);
206
- return;
195
+ if (!providerConfirmed) {
196
+ const billingCountryError = getBillingCountryError(billingCountry !== null && billingCountry !== void 0 ? billingCountry : fixedBillingCountry);
197
+ if (billingCountryError) {
198
+ setSharedError(billingCountryError);
199
+ return;
200
+ }
201
+ if (checkoutState.type !== 'success') {
202
+ setSharedError(checkoutState.type === 'error'
203
+ ? checkoutState.error.message
204
+ : copy.formNotReady);
205
+ return;
206
+ }
207
+ if (!paymentElementReady) {
208
+ setSharedError(copy.formNotReady, true);
209
+ return;
210
+ }
207
211
  }
208
212
  submitInFlightRef.current = true;
209
213
  setSubmitting(true);
@@ -212,25 +216,36 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
212
216
  submitInFlightRef.current = false;
213
217
  setSubmitting(false);
214
218
  };
219
+ let confirmed = providerConfirmed;
215
220
  try {
216
- const syncedEmail = await ensureCustomerEmail();
217
- if (!syncedEmail) {
218
- finishSubmitting();
219
- return;
220
- }
221
- if (paymentElementComplete) {
222
- await trackPaymentInfoSubmitted();
223
- }
224
- const result = await checkoutState.checkout.confirm({
225
- redirect: 'if_required',
226
- });
227
- if (result.type === 'error') {
228
- const message = result.error.message || 'Payment failed.';
229
- setSharedError(message, false, result.error);
230
- finishSubmitting();
231
- return;
221
+ if (!providerConfirmed && checkoutState.type === 'success') {
222
+ const syncedEmail = await ensureCustomerEmail();
223
+ if (!syncedEmail) {
224
+ finishSubmitting();
225
+ return;
226
+ }
227
+ if (paymentElementComplete) {
228
+ await trackPaymentInfoSubmitted();
229
+ }
230
+ const result = await checkoutState.checkout.confirm({
231
+ redirect: 'if_required',
232
+ });
233
+ if (result.type === 'error') {
234
+ const message = result.error.message || copy.paymentFailed;
235
+ setSharedError(message, false, result.error);
236
+ finishSubmitting();
237
+ return;
238
+ }
239
+ confirmed = true;
240
+ setProviderConfirmed(true);
232
241
  }
233
242
  if (typeof window !== 'undefined') {
243
+ if (await completePaywallCheckout('stripe', checkoutSessionId || '', { beforeNotify: async () => {
244
+ setCompleted(true);
245
+ await trackCheckoutSuccess(true);
246
+ } })) {
247
+ return;
248
+ }
234
249
  await trackCheckoutSuccess();
235
250
  window.location.assign(returnUrl);
236
251
  return;
@@ -238,12 +253,16 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
238
253
  finishSubmitting();
239
254
  }
240
255
  catch (submitError) {
241
- setSharedError(getFriendlyCardErrorMessage(submitError));
256
+ setSharedError(getFriendlyCardErrorMessage(submitError, copy.cardError), confirmed);
242
257
  finishSubmitting();
243
258
  }
244
259
  };
245
260
  const visibleError = error;
246
- return (_jsxs("div", { className: 'shared-checkout-v2-shell', "data-variant": variant, style: checkoutStyle, children: [_jsxs("header", { className: 'shared-checkout-v2-header', children: [_jsx("button", { type: 'button', className: 'shared-checkout-v2-close', onClick: onClose, "aria-label": closeAriaLabel, children: _jsx("span", { "aria-hidden": 'true' }) }), _jsx("h2", { id: 'shared-checkout-v2-title', children: title })] }), _jsx(SharedStripeCheckoutV2Summary, { discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo }), _jsxs("form", { className: 'shared-checkout-v2-payment', onSubmit: (event) => void handleSubmit(event), children: [showWalletMethod ? (_jsxs("div", { className: 'shared-checkout-v2-methods', role: 'tablist', "aria-label": 'Payment method', children: [_jsxs("button", { type: 'button', className: [
261
+ if (providerConfirmed && !completed)
262
+ return _jsxs("form", { onSubmit: handleSubmit, className: 'bg-background text-foreground p-6', children: [_jsx("p", { role: error ? 'alert' : 'status', children: error || copy.verifyingPayment }), _jsx("button", { type: 'submit', disabled: submitting, className: 'bg-accent text-foreground border-border rounded border p-3', children: copy.checkPaymentStatus })] });
263
+ if (completed)
264
+ return _jsx("div", { role: 'status', className: 'bg-background text-foreground p-6', children: copy.paymentConfirmed });
265
+ return (_jsxs("div", { className: 'shared-checkout-v2-shell', "data-variant": variant, style: checkoutStyle, children: [_jsxs("header", { className: 'shared-checkout-v2-header', children: [_jsx("button", { type: 'button', className: 'shared-checkout-v2-close', onClick: onClose, "aria-label": closeAriaLabel, children: _jsx("span", { "aria-hidden": 'true' }) }), _jsx("h2", { id: 'shared-checkout-v2-title', children: title })] }), _jsx(SharedStripeCheckoutV2Summary, { countdownUnit: copy.countdownUnit, discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo }), _jsxs("form", { className: 'shared-checkout-v2-payment', onSubmit: (event) => void handleSubmit(event), children: [showWalletMethod ? (_jsxs("div", { className: 'shared-checkout-v2-methods', role: 'tablist', "aria-label": copy.paymentMethodLabel, children: [_jsxs("button", { type: 'button', className: [
247
266
  'shared-checkout-v2-method',
248
267
  effectiveSelectedMethod === 'wallet' ? 'is-selected' : '',
249
268
  ]
@@ -253,12 +272,12 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
253
272
  effectiveSelectedMethod === 'card' ? 'is-selected' : '',
254
273
  ]
255
274
  .filter(Boolean)
256
- .join(' '), onClick: selectCardMethod, role: 'tab', "aria-selected": effectiveSelectedMethod === 'card', children: [_jsx("strong", { children: "Credit card" }), _jsx("span", { className: 'shared-checkout-v2-tab-brands', "aria-hidden": 'true', children: cardBrandLabels.slice(0, 4).map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-tab-brand' }, label))) })] })] })) : null, _jsxs("div", { hidden: effectiveSelectedMethod !== 'wallet', className: [
275
+ .join(' '), onClick: selectCardMethod, role: 'tab', "aria-selected": effectiveSelectedMethod === 'card', children: [_jsx("strong", { children: copy.cardLabel }), _jsx("span", { className: 'shared-checkout-v2-tab-brands', "aria-hidden": 'true', children: cardBrandLabels.slice(0, 4).map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-tab-brand' }, label))) })] })] })) : null, _jsxs("div", { hidden: effectiveSelectedMethod !== 'wallet', className: [
257
276
  'shared-checkout-v2-wallet-panel',
258
277
  effectiveSelectedMethod === 'wallet' ? 'is-visible' : '',
259
278
  ]
260
279
  .filter(Boolean)
261
- .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, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, className: 'shared-checkout-v2-express-buttons', confirmEmail: false, customerEmail: resolvedCustomerEmail, customerName: customerName, initialAvailable: initialWalletAvailable, keepInitialAvailableOnReady: initialWalletAvailable === true, onAvailabilityChange: handleWalletAvailabilityChange, onError: (message) => setSharedError(message, true), onPaymentInfoSubmitted: onPaymentInfoSubmitted, onPaymentRejected: () => setSelectedMethod('card'), onSuccess: trackCheckoutSuccess, options: expressCheckoutOptions, returnUrl: returnUrl, summaryLabel: expressCheckoutSummaryLabel, supportedCountries: normalizedSupportedCountries, unsupportedCountryMessage: unsupportedCountryMessage }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] })] }), _jsxs("div", { hidden: effectiveSelectedMethod !== 'card', className: [
280
+ .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, checkoutAnalytics: checkoutAnalytics, checkoutSessionId: checkoutSessionId, className: 'shared-checkout-v2-express-buttons', confirmEmail: false, customerEmail: resolvedCustomerEmail, customerName: customerName, initialAvailable: initialWalletAvailable, keepInitialAvailableOnReady: initialWalletAvailable === true, onAvailabilityChange: handleWalletAvailabilityChange, onError: (message) => setSharedError(message, true), onPaymentInfoSubmitted: onPaymentInfoSubmitted, onPaymentRejected: () => setSelectedMethod('card'), onSuccess: () => trackCheckoutSuccess(), onEmbeddedComplete: () => setCompleted(true), onVerificationPending: () => setProviderConfirmed(true), options: expressCheckoutOptions, returnUrl: returnUrl, summaryLabel: expressCheckoutSummaryLabel, supportedCountries: normalizedSupportedCountries, unsupportedCountryMessage: unsupportedCountryMessage }), _jsx("span", { className: 'shared-checkout-v2-sr-only', children: walletAriaLabel })] })] }), _jsxs("div", { hidden: effectiveSelectedMethod !== 'card', className: [
262
281
  'shared-checkout-v2-card-panel',
263
282
  effectiveSelectedMethod === 'card' ? 'is-visible' : '',
264
283
  ]
@@ -268,5 +287,5 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
268
287
  return;
269
288
  }
270
289
  setEmailDraft(event.target.value);
271
- } }) })] }), _jsx(PaymentElement, { options: paymentElementOptions, onChange: handlePaymentElementChange, onReady: () => setPaymentElementReady(true) }), fixedBillingCountry && fixedBillingCountryLabel ? (_jsxs("label", { className: 'shared-checkout-v2-country-field', children: [_jsx("span", { className: 'shared-checkout-v2-country-label', children: "Country" }), _jsx("span", { className: 'shared-checkout-v2-country-control', children: _jsx("select", { "aria-label": 'Country', className: 'shared-checkout-v2-country-select', value: fixedBillingCountry, onChange: () => setBillingCountry(fixedBillingCountry), children: _jsx("option", { value: fixedBillingCountry, children: fixedBillingCountryLabel }) }) })] })) : null] }), visibleError ? _jsx("p", { className: 'shared-checkout-v2-error', children: visibleError }) : null, _jsxs("button", { type: 'submit', className: 'shared-checkout-v2-card-submit', disabled: submitting || emailSaving || emailSessionChanged || !paymentElementReady, children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-card-submit-icon' }), submitting || emailSaving || emailSessionChanged ? processingLabel : cardSubmitLabel] }), _jsx("div", { className: 'shared-checkout-v2-card-brands', "aria-hidden": 'true', children: cardBrandLabels.map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-card-brand' }, label))) }), _jsxs("p", { className: 'shared-checkout-v2-secure-pill shared-checkout-v2-secure-pill--card', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] })] })] })] }));
290
+ } }) })] }), _jsx(PaymentElement, { options: paymentElementOptions, onChange: handlePaymentElementChange, onReady: () => setPaymentElementReady(true) }), fixedBillingCountry && fixedBillingCountryLabel ? (_jsxs("label", { className: 'shared-checkout-v2-country-field', children: [_jsx("span", { className: 'shared-checkout-v2-country-label', children: copy.billingCountryLabel }), _jsx("span", { className: 'shared-checkout-v2-country-control', children: _jsx("select", { "aria-label": copy.billingCountryLabel, className: 'shared-checkout-v2-country-select', value: fixedBillingCountry, onChange: () => setBillingCountry(fixedBillingCountry), children: _jsx("option", { value: fixedBillingCountry, children: fixedBillingCountryLabel }) }) })] })) : null] }), visibleError ? _jsx("p", { className: 'shared-checkout-v2-error', children: visibleError }) : null, _jsxs("button", { type: 'submit', className: 'shared-checkout-v2-card-submit', disabled: submitting || emailSaving || emailSessionChanged || !paymentElementReady, children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-card-submit-icon' }), submitting || emailSaving || emailSessionChanged ? processingLabel : cardSubmitLabel] }), _jsx("div", { className: 'shared-checkout-v2-card-brands', "aria-hidden": 'true', children: cardBrandLabels.map((label) => (_jsx(PaymentBrandMark, { label: label, className: 'shared-checkout-v2-card-brand' }, label))) }), _jsxs("p", { className: 'shared-checkout-v2-secure-pill shared-checkout-v2-secure-pill--card', children: [_jsx(SecurityLockIcon, { className: 'shared-checkout-v2-secure-pill-icon' }), secureLabel] })] })] })] }));
272
291
  }
@@ -1,6 +1,7 @@
1
1
  import type { SharedStripeCheckoutV2DialogProps } from './SharedStripeCheckoutV2Dialog.js';
2
2
  type SummaryProps = Pick<SharedStripeCheckoutV2DialogProps, 'discountPercent' | 'countdownLabel' | 'remainingSeconds' | 'variant' | 'satisfactionPercent' | 'satisfactionLabel' | 'originalPriceLabel' | 'originalPriceValue' | 'discountLabel' | 'discountAmountLabel' | 'promoCodeLabel' | 'promoCode' | 'totalLabel' | 'totalValue' | 'savedLabel'> & {
3
3
  showPromo: boolean;
4
+ countdownUnit: string;
4
5
  };
5
- export declare function SharedStripeCheckoutV2Summary({ discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo }: SummaryProps): import("react/jsx-runtime").JSX.Element;
6
+ export declare function SharedStripeCheckoutV2Summary({ discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo, countdownUnit }: SummaryProps): import("react/jsx-runtime").JSX.Element;
6
7
  export {};
@@ -7,10 +7,10 @@ const formatCountdown = (remainingSeconds) => {
7
7
  const seconds = normalizedSeconds % 60;
8
8
  return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
9
9
  };
10
- export function SharedStripeCheckoutV2Summary({ discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo }) {
10
+ export function SharedStripeCheckoutV2Summary({ discountPercent, countdownLabel, remainingSeconds, variant, satisfactionPercent, satisfactionLabel, originalPriceLabel, originalPriceValue, discountLabel, discountAmountLabel, promoCodeLabel, promoCode, totalLabel, totalValue, savedLabel, showPromo, countdownUnit }) {
11
11
  const countdownValue = formatCountdown(remainingSeconds);
12
12
  const normalizedPromoCode = promoCode.trim();
13
- return (_jsxs("section", { className: 'shared-checkout-v2-summary', children: [showPromo ? (_jsxs("p", { className: 'shared-checkout-v2-countdown', children: [discountPercent, "% ", countdownLabel, " ", countdownValue, " min"] })) : null, variant === 'standard' ? (_jsxs("p", { className: 'shared-checkout-v2-satisfaction', children: [_jsx("strong", { children: satisfactionPercent }), " ", satisfactionLabel] })) : null, showPromo ? (_jsxs("div", { className: 'shared-checkout-v2-price-stack', children: [_jsxs("div", { className: 'shared-checkout-v2-price-row is-muted', children: [_jsx("span", { children: originalPriceLabel }), _jsx("span", { className: 'shared-checkout-v2-strike', children: originalPriceValue })] }), _jsxs("div", { className: 'shared-checkout-v2-price-row is-discount', children: [_jsx("strong", { children: discountLabel }), _jsx("strong", { children: discountAmountLabel })] }), _jsxs("div", { className: 'shared-checkout-v2-promo', children: [_jsx("span", { children: promoCodeLabel }), _jsx("strong", { children: normalizedPromoCode })] })] })) : null, _jsxs("div", { className: [
13
+ return (_jsxs("section", { className: 'shared-checkout-v2-summary', children: [showPromo ? (_jsxs("p", { className: 'shared-checkout-v2-countdown', children: [discountPercent, "% ", countdownLabel, " ", countdownValue, " ", countdownUnit] })) : null, variant === 'standard' ? (_jsxs("p", { className: 'shared-checkout-v2-satisfaction', children: [_jsx("strong", { children: satisfactionPercent }), " ", satisfactionLabel] })) : null, showPromo ? (_jsxs("div", { className: 'shared-checkout-v2-price-stack', children: [_jsxs("div", { className: 'shared-checkout-v2-price-row is-muted', children: [_jsx("span", { children: originalPriceLabel }), _jsx("span", { className: 'shared-checkout-v2-strike', children: originalPriceValue })] }), _jsxs("div", { className: 'shared-checkout-v2-price-row is-discount', children: [_jsx("strong", { children: discountLabel }), _jsx("strong", { children: discountAmountLabel })] }), _jsxs("div", { className: 'shared-checkout-v2-promo', children: [_jsx("span", { children: promoCodeLabel }), _jsx("strong", { children: normalizedPromoCode })] })] })) : null, _jsxs("div", { className: [
14
14
  'shared-checkout-v2-total',
15
15
  showPromo ? '' : 'is-plain',
16
16
  ]
@@ -30,7 +30,9 @@ export type StripeCheckoutExpressCheckoutButtonProps = {
30
30
  message: string;
31
31
  paymentMethod?: string;
32
32
  }) => void;
33
+ onEmbeddedComplete?: () => void;
34
+ onVerificationPending?: () => void;
33
35
  onSuccess?: (result: StripeWalletCheckoutSuccess) => void | Promise<void>;
34
36
  unsupportedCountryMessage?: string;
35
37
  };
36
- export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, unsupportedCountryMessage, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
38
+ export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, onEmbeddedComplete, onVerificationPending, unsupportedCountryMessage, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -1,5 +1,6 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { completePaywallCheckout } from '@funnelsgrove/runtime';
3
4
  import { isValidCheckoutEmail, isCheckoutEmailError } from '../../../services/checkoutEmail.js';
4
5
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
5
6
  import { ExpressCheckoutElement, useCheckout, } from '@stripe/react-stripe-js/checkout';
@@ -52,8 +53,13 @@ const normalizeStripeCheckoutProviderError = (error) => {
52
53
  type: typeof record.type === 'string' ? record.type : undefined,
53
54
  };
54
55
  };
55
- export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail = true, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, unsupportedCountryMessage, }) {
56
+ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAnalytics, checkoutSessionId, returnUrl, confirmEmail = true, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, serverUpdateKey, supportedCountries, onServerUpdate, onAvailabilityChange, onCancel, onError, onPaymentInfoSubmitted, onPaymentRejected, onSuccess, onEmbeddedComplete, onVerificationPending, unsupportedCountryMessage, }) {
56
57
  const checkoutState = useCheckout();
58
+ const [completed, setCompleted] = useState(false);
59
+ const completedRef = useRef(false);
60
+ const verificationInFlightRef = useRef(false);
61
+ const [verificationInFlight, setVerificationInFlight] = useState(false);
62
+ const [verificationError, setVerificationError] = useState(null);
57
63
  const [loadRetry, setLoadRetry] = useState(0);
58
64
  const loadRetryUsedRef = useRef(false);
59
65
  const walletReadyRef = useRef(false);
@@ -160,11 +166,12 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
160
166
  }
161
167
  void ensureServerUpdated();
162
168
  }, [checkoutState.type, ensureServerUpdated, normalizedServerUpdateKey, onServerUpdate]);
163
- const trackCheckoutSuccess = async () => {
169
+ const trackCheckoutSuccess = async (embedded = false) => {
164
170
  if (checkoutAnalytics) {
165
171
  await trackPaidStripeSubscriptionCheckoutCompleted(Object.assign({ checkoutSessionId }, checkoutAnalytics));
166
172
  }
167
- await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess({ checkoutSessionId: checkoutSessionId !== null && checkoutSessionId !== void 0 ? checkoutSessionId : null }));
173
+ if (!embedded)
174
+ await (onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess({ checkoutSessionId: checkoutSessionId !== null && checkoutSessionId !== void 0 ? checkoutSessionId : null }));
168
175
  };
169
176
  const queueCheckoutStarted = (paymentMethod, checkoutStartSource = 'wallet_click') => {
170
177
  if (!checkoutAnalytics) {
@@ -191,8 +198,35 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
191
198
  }
192
199
  onError === null || onError === void 0 ? void 0 : onError(isCheckoutEmailError(providerError) ? 'Enter a valid email address to continue.' : message);
193
200
  };
201
+ const verifyPaywall = async () => {
202
+ if (verificationInFlightRef.current || completedRef.current)
203
+ return true;
204
+ verificationInFlightRef.current = true;
205
+ setVerificationInFlight(true);
206
+ try {
207
+ return await completePaywallCheckout('stripe', checkoutSessionId || '', { beforeNotify: async () => {
208
+ completedRef.current = true;
209
+ setCompleted(true);
210
+ onEmbeddedComplete === null || onEmbeddedComplete === void 0 ? void 0 : onEmbeddedComplete();
211
+ await trackCheckoutSuccess(true);
212
+ } });
213
+ }
214
+ catch (error) {
215
+ const message = error instanceof Error ? error.message : 'Unable to confirm payment';
216
+ setVerificationError(message);
217
+ onError === null || onError === void 0 ? void 0 : onError(message);
218
+ onVerificationPending === null || onVerificationPending === void 0 ? void 0 : onVerificationPending();
219
+ return true;
220
+ }
221
+ finally {
222
+ verificationInFlightRef.current = false;
223
+ setVerificationInFlight(false);
224
+ }
225
+ };
194
226
  const handleConfirm = async (event) => {
195
227
  var _a, _b, _c, _d, _e, _f;
228
+ if (completedRef.current)
229
+ return;
196
230
  // Checkout Sessions omits the Express Checkout onClick callback. Confirmation
197
231
  // and cancellation are the reliable wallet-intent boundaries for this provider.
198
232
  // Session-based analytics deduplication keeps this idempotent if onClick fires.
@@ -287,6 +321,8 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
287
321
  }
288
322
  onError === null || onError === void 0 ? void 0 : onError(null);
289
323
  if (typeof window !== 'undefined') {
324
+ if (await verifyPaywall())
325
+ return;
290
326
  await trackCheckoutSuccess();
291
327
  window.location.assign(returnUrl);
292
328
  }
@@ -320,6 +356,10 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, checkoutAna
320
356
  onCancel === null || onCancel === void 0 ? void 0 : onCancel();
321
357
  };
322
358
  const inertWhileUpdating = (serverUpdatePending ? { inert: true } : {});
359
+ if (verificationError && !completed)
360
+ return _jsxs("div", { className: 'bg-background text-foreground p-6', children: [_jsx("p", { role: 'alert', children: verificationError }), _jsx("button", { type: 'button', disabled: verificationInFlight, className: 'bg-accent text-foreground border-border rounded border p-3', onClick: () => void verifyPaywall(), children: "Check payment status" })] });
361
+ if (completed)
362
+ return _jsx("div", { role: 'status', className: 'bg-background text-foreground p-6', children: "Payment confirmed. You can return to the app." });
323
363
  return (_jsxs(_Fragment, { children: [_jsx("div", Object.assign({}, inertWhileUpdating, { "aria-busy": serverUpdatePending || undefined, "aria-disabled": serverUpdatePending || undefined, className: [
324
364
  'stripe-express-checkout-button',
325
365
  className !== null && className !== void 0 ? className : '',
@@ -0,0 +1,14 @@
1
+ export type SharedStripeCheckoutV2Copy = {
2
+ paymentMethodLabel: string;
3
+ countdownUnit: string;
4
+ cardLabel: string;
5
+ formNotReady: string;
6
+ paymentFailed: string;
7
+ cardError: string;
8
+ verifyingPayment: string;
9
+ checkPaymentStatus: string;
10
+ paymentConfirmed: string;
11
+ billingCountryLabel: string;
12
+ countryNames: Readonly<Record<string, string>>;
13
+ };
14
+ export declare const defaultCheckoutV2Copy: SharedStripeCheckoutV2Copy;
@@ -0,0 +1,13 @@
1
+ export const defaultCheckoutV2Copy = {
2
+ paymentMethodLabel: 'Payment method',
3
+ countdownUnit: 'min',
4
+ cardLabel: 'Credit card',
5
+ formNotReady: 'Payment form is not ready yet.',
6
+ paymentFailed: 'Payment failed.',
7
+ cardError: 'Payment failed. Please review your card details and try again.',
8
+ verifyingPayment: 'Verifying payment…',
9
+ checkPaymentStatus: 'Check payment status',
10
+ paymentConfirmed: 'Payment confirmed. You can return to the app.',
11
+ billingCountryLabel: 'Country',
12
+ countryNames: { US: 'United States' },
13
+ };
@@ -1,4 +1,4 @@
1
- import type { Stripe } from '@stripe/stripe-js';
1
+ import type { Stripe, StripeConstructorOptions } from '@stripe/stripe-js';
2
2
  import type { RuntimeMode } from '@funnelsgrove/runtime';
3
3
  import { type CheckoutPreparationLoading, type CheckoutPreparationSource } from '../../paymentProvider.types.js';
4
4
  import { type PaywallPlan, type StripeRuntimeConfigOverrides } from '../services/stripe.service.js';
@@ -20,6 +20,7 @@ export type StripeOneTimeCheckoutSessionInput = {
20
20
  returnUrl: string;
21
21
  runtimeConfig?: StripeRuntimeConfigOverrides;
22
22
  stripePublishableKey?: string | null;
23
+ stripeLocale?: StripeConstructorOptions['locale'];
23
24
  userId?: string | null;
24
25
  };
25
26
  export type StripeOneTimeCheckoutPreparationOptions = {
@@ -51,4 +52,4 @@ export type StripeOneTimeCheckoutIntentKeyInput = Pick<StripeOneTimeCheckoutSess
51
52
  };
52
53
  export declare function buildStripeOneTimeCheckoutIntentKey({ runtimeConfigRevisionId, paymentProfileId, provider, checkoutMode, couponId, customerEmail, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }: StripeOneTimeCheckoutIntentKeyInput): string;
53
54
  export declare function shouldResetStripeOneTimeCheckoutSessionForIntentKey(previousIntentKey: string | null, nextIntentKey: string): boolean;
54
- export declare function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId, paymentProfileId, provider, analyticsMetadata, checkoutAnalytics, checkoutMode, couponId, customerEmail, enabled, onError, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }: StripeOneTimeCheckoutSessionInput): StripeOneTimeCheckoutSession;
55
+ export declare function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId, paymentProfileId, provider, analyticsMetadata, checkoutAnalytics, checkoutMode, couponId, customerEmail, enabled, onError, plan, returnUrl, runtimeConfig, stripePublishableKey, stripeLocale, userId, }: StripeOneTimeCheckoutSessionInput): StripeOneTimeCheckoutSession;
@@ -71,7 +71,7 @@ export function buildStripeOneTimeCheckoutIntentKey({ runtimeConfigRevisionId, p
71
71
  export function shouldResetStripeOneTimeCheckoutSessionForIntentKey(previousIntentKey, nextIntentKey) {
72
72
  return previousIntentKey !== null && previousIntentKey !== nextIntentKey;
73
73
  }
74
- export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId, paymentProfileId, provider, analyticsMetadata, checkoutAnalytics, checkoutMode, couponId, customerEmail, enabled = true, onError, plan, returnUrl, runtimeConfig, stripePublishableKey, userId, }) {
74
+ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId, paymentProfileId, provider, analyticsMetadata, checkoutAnalytics, checkoutMode, couponId, customerEmail, enabled = true, onError, plan, returnUrl, runtimeConfig, stripePublishableKey, stripeLocale, userId, }) {
75
75
  var _a, _b, _c;
76
76
  const [clientSecret, setClientSecret] = useState(null);
77
77
  const [clientSecretIntentKey, setClientSecretIntentKey] = useState(null);
@@ -114,8 +114,8 @@ export function useStripeOneTimeCheckoutSession({ runtimeConfigRevisionId, payme
114
114
  const activeCheckoutSessionId = clientSecretIntentKey === intentKey ? checkoutSessionId : null;
115
115
  const stripePromise = useMemo(() => {
116
116
  var _a;
117
- return getStripePromise(checkoutMode, (_a = runtimeStripePublishableKey !== null && runtimeStripePublishableKey !== void 0 ? runtimeStripePublishableKey : stripePublishableKey) !== null && _a !== void 0 ? _a : runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey);
118
- }, [checkoutMode, runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey, runtimeStripePublishableKey, stripePublishableKey]);
117
+ return getStripePromise(checkoutMode, (_a = runtimeStripePublishableKey !== null && runtimeStripePublishableKey !== void 0 ? runtimeStripePublishableKey : stripePublishableKey) !== null && _a !== void 0 ? _a : runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey, stripeLocale);
118
+ }, [checkoutMode, runtimeConfig === null || runtimeConfig === void 0 ? void 0 : runtimeConfig.funnelSdkPublishableKey, runtimeStripePublishableKey, stripePublishableKey, stripeLocale]);
119
119
  const previousIntentKeyRef = useRef(intentKey);
120
120
  const analyticsMetadataRef = useRef(analyticsMetadata);
121
121
  useEffect(() => {
@@ -1,4 +1,4 @@
1
- import type { Stripe } from '@stripe/stripe-js';
1
+ import type { Stripe, StripeConstructorOptions } from '@stripe/stripe-js';
2
2
  import type { RuntimeMode } from '@funnelsgrove/runtime';
3
3
  import { type CheckoutPreparationLoading, type CheckoutPreparationSource } from '../../paymentProvider.types.js';
4
4
  import type { PaywallDisplayPlan } from '../../../services/paywallOffer.service.js';
@@ -21,6 +21,7 @@ export type StripeSubscriptionCheckoutSessionInput = {
21
21
  paymentProfileId?: string | null;
22
22
  provider?: 'stripe' | null;
23
23
  stripePublishableKey?: string | null;
24
+ stripeLocale?: StripeConstructorOptions['locale'];
24
25
  userId?: string | null;
25
26
  };
26
27
  export type StripeSubscriptionCheckoutPreparationOptions = {
@@ -58,4 +59,4 @@ export declare function shouldResetStripeSubscriptionCheckoutSessionForIntentKey
58
59
  export declare function isInactiveCheckoutSessionError(message?: string | null): boolean;
59
60
  export declare function shouldAutoRecoverStripeSubscriptionCheckoutSession(message: string | null | undefined, alreadyRecovered: boolean): boolean;
60
61
  export declare const shouldReportStripeCheckoutFailureShown: (message: string) => boolean;
61
- export declare function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, checkoutAnalytics, couponId, customerEmail, displayPlan, enabled, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, userId, }: StripeSubscriptionCheckoutSessionInput): StripeSubscriptionCheckoutSession;
62
+ export declare function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, checkoutAnalytics, couponId, customerEmail, displayPlan, enabled, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, stripeLocale, userId, }: StripeSubscriptionCheckoutSessionInput): StripeSubscriptionCheckoutSession;
@@ -71,7 +71,7 @@ export function shouldAutoRecoverStripeSubscriptionCheckoutSession(message, alre
71
71
  return !alreadyRecovered && isInactiveCheckoutSessionError(message);
72
72
  }
73
73
  export const shouldReportStripeCheckoutFailureShown = (message) => !isInactiveCheckoutSessionError(message);
74
- export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, checkoutAnalytics, couponId, customerEmail, displayPlan, enabled = true, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, userId, }) {
74
+ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMetadata, checkoutAnalytics, couponId, customerEmail, displayPlan, enabled = true, onError, plan, returnUrl, runtimeConfig, runtimeConfigRevisionId, paymentProfileId, provider, stripePublishableKey, stripeLocale, userId, }) {
75
75
  var _a, _b, _c, _d, _e;
76
76
  const [clientSecret, setClientSecret] = useState(null);
77
77
  const [clientSecretIntentKey, setClientSecretIntentKey] = useState(null);
@@ -119,7 +119,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, analyticsMe
119
119
  const activeClientSecret = clientSecretIntentKey === intentKey ? clientSecret : null;
120
120
  const activeCheckoutSessionId = clientSecretIntentKey === intentKey ? checkoutSessionId : null;
121
121
  const activePlanKey = clientSecretIntentKey === intentKey ? clientSecretPlanKey : null;
122
- const stripePromise = useMemo(() => getStripePromise(checkoutMode, runtimeStripePublishableKey !== null && runtimeStripePublishableKey !== void 0 ? runtimeStripePublishableKey : stripePublishableKey), [checkoutMode, runtimeStripePublishableKey, stripePublishableKey]);
122
+ const stripePromise = useMemo(() => getStripePromise(checkoutMode, runtimeStripePublishableKey !== null && runtimeStripePublishableKey !== void 0 ? runtimeStripePublishableKey : stripePublishableKey, stripeLocale), [checkoutMode, runtimeStripePublishableKey, stripePublishableKey, stripeLocale]);
123
123
  const previousIntentKeyRef = useRef(intentKey);
124
124
  const analyticsMetadataRef = useRef(analyticsMetadata);
125
125
  useEffect(() => {
@@ -1,4 +1,4 @@
1
- import type { Stripe } from '@stripe/stripe-js';
1
+ import type { Stripe, StripeConstructorOptions } from '@stripe/stripe-js';
2
2
  import type { RuntimeMode } from '@funnelsgrove/runtime';
3
3
  export declare function isStripeConfigured(mode: RuntimeMode): boolean;
4
- export declare function getStripePromise(_mode: RuntimeMode, runtimePublishableKey?: string | null): Promise<Stripe | null> | null;
4
+ export declare function getStripePromise(_mode: RuntimeMode, runtimePublishableKey?: string | null, locale?: StripeConstructorOptions['locale']): Promise<Stripe | null> | null;
@@ -5,15 +5,16 @@ export function isStripeConfigured(mode) {
5
5
  void mode;
6
6
  return FUNNEL_SDK_PUBLISHABLE_KEY.length > 0;
7
7
  }
8
- export function getStripePromise(_mode, runtimePublishableKey) {
8
+ export function getStripePromise(_mode, runtimePublishableKey, locale) {
9
9
  const resolvedPublishableKey = (runtimePublishableKey === null || runtimePublishableKey === void 0 ? void 0 : runtimePublishableKey.trim()) || '';
10
10
  if (!resolvedPublishableKey) {
11
11
  return null;
12
12
  }
13
- let stripePromise = stripePromiseByKey.get(resolvedPublishableKey) || null;
13
+ const cacheKey = JSON.stringify([resolvedPublishableKey, locale !== null && locale !== void 0 ? locale : 'auto']);
14
+ let stripePromise = stripePromiseByKey.get(cacheKey) || null;
14
15
  if (!stripePromise) {
15
- stripePromise = loadStripe(resolvedPublishableKey);
16
- stripePromiseByKey.set(resolvedPublishableKey, stripePromise);
16
+ stripePromise = locale ? loadStripe(resolvedPublishableKey, { locale }) : loadStripe(resolvedPublishableKey);
17
+ stripePromiseByKey.set(cacheKey, stripePromise);
17
18
  }
18
19
  return stripePromise;
19
20
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.15.11",
3
+ "version": "0.21.0",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
@@ -32,8 +32,8 @@
32
32
  "test:run": "vitest run --passWithNoTests"
33
33
  },
34
34
  "dependencies": {
35
- "@funnelsgrove/analytics": "0.1.106",
36
- "@funnelsgrove/runtime": "0.14.4",
35
+ "@funnelsgrove/analytics": "0.1.110",
36
+ "@funnelsgrove/runtime": "0.19.0",
37
37
  "@solidgate/react-sdk": "1.34.0",
38
38
  "@stripe/react-stripe-js": "^5.6.0",
39
39
  "@stripe/stripe-js": "^8.7.0",