@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.
- package/README.md +19 -0
- package/dist/components/ManageSubscriptionScreen.d.ts +30 -4
- package/dist/components/ManageSubscriptionScreen.js +45 -30
- package/dist/index.d.ts +1 -1
- package/dist/providers/solidgate/components/SolidgateCheckoutDialog.js +14 -7
- package/dist/providers/solidgate/services/solidgate.service.js +7 -5
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Dialog.d.ts +3 -0
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Form.internal.d.ts +1 -1
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Form.internal.js +65 -46
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Summary.internal.d.ts +2 -1
- package/dist/providers/stripe/components/SharedStripeCheckoutV2Summary.internal.js +2 -2
- package/dist/providers/stripe/components/StripeCheckoutExpressCheckoutButton.d.ts +3 -1
- package/dist/providers/stripe/components/StripeCheckoutExpressCheckoutButton.js +44 -4
- package/dist/providers/stripe/components/checkout-v2.content.d.ts +14 -0
- package/dist/providers/stripe/components/checkout-v2.content.js +13 -0
- package/dist/providers/stripe/hooks/useStripeOneTimeCheckoutSession.d.ts +3 -2
- package/dist/providers/stripe/hooks/useStripeOneTimeCheckoutSession.js +3 -3
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.d.ts +3 -2
- package/dist/providers/stripe/hooks/useStripeSubscriptionCheckoutSession.js +2 -2
- package/dist/providers/stripe/services/stripeClient.d.ts +2 -2
- package/dist/providers/stripe/services/stripeClient.js +5 -4
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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) =>
|
|
227
|
-
|
|
228
|
-
|
|
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 === '
|
|
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
|
-
:
|
|
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 (
|
|
295
|
-
if (!controller.signal.aborted
|
|
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.
|
|
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
|
-
|
|
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
|
|
18
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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, "
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
36
|
-
"@funnelsgrove/runtime": "0.
|
|
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",
|