@funnelsgrove/payments 0.20.0 → 0.21.1

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,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,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 {};
@@ -13,18 +13,16 @@ import { trackPaidStripeSubscriptionCheckoutCompleted, trackStripeSubscriptionPa
13
13
  import { trackCheckoutFailureShown } from '../../../services/checkoutObservability.service.js';
14
14
  import { SharedStripeCheckoutV2Summary } from './SharedStripeCheckoutV2Summary.internal.js';
15
15
  import { useCheckoutEmail } from './useCheckoutEmail.internal.js';
16
+ import { defaultCheckoutV2Copy } from './checkout-v2.content.js';
16
17
  const defaultPaymentMethodLabels = ['Visa', 'Mastercard', 'Maestro', 'Discover'];
17
18
  export const getFixedSupportedBillingCountry = (supportedCountries) => supportedCountries.length === 1 ? supportedCountries[0] : null;
18
- const checkoutCountryLabels = {
19
- US: 'United States',
20
- };
21
- const getCheckoutCountryLabel = (countryCode) => { var _a; return (_a = checkoutCountryLabels[countryCode]) !== null && _a !== void 0 ? _a : countryCode; };
22
- 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) {
23
21
  return error instanceof Error && error.message
24
22
  ? error.message
25
- : 'Payment failed. Please review your card details and try again.';
23
+ : fallback;
26
24
  }
27
- 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', }) {
28
26
  const checkoutState = useCheckout();
29
27
  const walletPaymentMethods = usePlatformWalletPaymentMethods();
30
28
  const [selectedMethod, setSelectedMethod] = useState('wallet');
@@ -47,7 +45,7 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
47
45
  const normalizedSupportedCountries = useMemo(() => normalizeSupportedCheckoutCountries(supportedCountries), [supportedCountries]);
48
46
  const fixedBillingCountry = getFixedSupportedBillingCountry(normalizedSupportedCountries);
49
47
  const fixedBillingCountryLabel = fixedBillingCountry
50
- ? getCheckoutCountryLabel(fixedBillingCountry)
48
+ ? getCheckoutCountryLabel(fixedBillingCountry, copy)
51
49
  : null;
52
50
  const cardBrandLabels = useMemo(() => paymentMethodLabels.length > 0 ? paymentMethodLabels : defaultPaymentMethodLabels, [paymentMethodLabels]);
53
51
  const normalizedPromoCode = promoCode.trim();
@@ -203,11 +201,11 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
203
201
  if (checkoutState.type !== 'success') {
204
202
  setSharedError(checkoutState.type === 'error'
205
203
  ? checkoutState.error.message
206
- : 'Payment form is not ready yet.');
204
+ : copy.formNotReady);
207
205
  return;
208
206
  }
209
207
  if (!paymentElementReady) {
210
- setSharedError('Payment form is not ready yet.', true);
208
+ setSharedError(copy.formNotReady, true);
211
209
  return;
212
210
  }
213
211
  }
@@ -233,7 +231,7 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
233
231
  redirect: 'if_required',
234
232
  });
235
233
  if (result.type === 'error') {
236
- const message = result.error.message || 'Payment failed.';
234
+ const message = result.error.message || copy.paymentFailed;
237
235
  setSharedError(message, false, result.error);
238
236
  finishSubmitting();
239
237
  return;
@@ -255,16 +253,16 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
255
253
  finishSubmitting();
256
254
  }
257
255
  catch (submitError) {
258
- setSharedError(getFriendlyCardErrorMessage(submitError), confirmed);
256
+ setSharedError(getFriendlyCardErrorMessage(submitError, copy.cardError), confirmed);
259
257
  finishSubmitting();
260
258
  }
261
259
  };
262
260
  const visibleError = error;
263
261
  if (providerConfirmed && !completed)
264
- return _jsxs("form", { onSubmit: handleSubmit, className: 'bg-background text-foreground p-6', children: [_jsx("p", { role: error ? 'alert' : 'status', children: error || 'Verifying payment…' }), _jsx("button", { type: 'submit', disabled: submitting, className: 'bg-accent text-foreground border-border rounded border p-3', children: "Check payment status" })] });
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 })] });
265
263
  if (completed)
266
- return _jsx("div", { role: 'status', className: 'bg-background text-foreground p-6', children: "Payment confirmed. You can return to the app." });
267
- 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: [
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: [
268
266
  'shared-checkout-v2-method',
269
267
  effectiveSelectedMethod === 'wallet' ? 'is-selected' : '',
270
268
  ]
@@ -274,7 +272,7 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
274
272
  effectiveSelectedMethod === 'card' ? 'is-selected' : '',
275
273
  ]
276
274
  .filter(Boolean)
277
- .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: [
278
276
  'shared-checkout-v2-wallet-panel',
279
277
  effectiveSelectedMethod === 'wallet' ? 'is-visible' : '',
280
278
  ]
@@ -289,5 +287,5 @@ export function SharedStripeCheckoutV2CheckoutSessionForm({ amountCents, buttonC
289
287
  return;
290
288
  }
291
289
  setEmailDraft(event.target.value);
292
- } }) })] }), _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] })] })] })] }));
293
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
  ]
@@ -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.20.0",
3
+ "version": "0.21.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/The-Solid-Grove/funnelsgrove.git",
@@ -32,8 +32,6 @@
32
32
  "test:run": "vitest run --passWithNoTests"
33
33
  },
34
34
  "dependencies": {
35
- "@funnelsgrove/analytics": "0.1.110",
36
- "@funnelsgrove/runtime": "0.19.0",
37
35
  "@solidgate/react-sdk": "1.34.0",
38
36
  "@stripe/react-stripe-js": "^5.6.0",
39
37
  "@stripe/stripe-js": "^8.7.0",
@@ -47,6 +45,12 @@
47
45
  "@types/react-dom": "^19",
48
46
  "jsdom": "20.0.3",
49
47
  "typescript": "^5",
50
- "vitest": "^3.2.4"
48
+ "vitest": "^3.2.4",
49
+ "@funnelsgrove/runtime": "0.19.1",
50
+ "@funnelsgrove/analytics": "0.2.0"
51
+ },
52
+ "peerDependencies": {
53
+ "@funnelsgrove/runtime": "^0.19.1",
54
+ "@funnelsgrove/analytics": "^0.1.111 || ^0.2.0"
51
55
  }
52
56
  }