@funnelsgrove/payments 0.1.17 → 0.1.19

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.
@@ -22,6 +22,8 @@ type ManageSubscription = RuntimeSubscription & {
22
22
  export declare function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId, plans, }: ManageSubscriptionScreenProps): import("react/jsx-runtime").JSX.Element;
23
23
  export declare const __manageSubscriptionScreenTestables: {
24
24
  resolveBillingDateLabel: (subscription: Pick<ManageSubscription, "cancelAtPeriodEnd" | "currentPeriodEnd" | "status">, options?: {
25
+ cancelled?: boolean;
25
26
  past?: boolean;
26
27
  }) => string;
28
+ resolveSubscriptionStatusLabel: (subscription: ManageSubscription) => string;
27
29
  };
@@ -4,9 +4,13 @@ import { useEffect, useMemo, useState } from 'react';
4
4
  import { apiService, buildPreviewManageSubscriptionsFallback, isPreviewFrameRuntime, runtimePublicConfig, useFunnel, } from '@funnelsgrove/runtime';
5
5
  import { getFallbackBillingPlans } from '../services/planCatalog.service.js';
6
6
  const pastSubscriptionsNote = 'Past subscriptions are already canceled or expired. Dates below are historical, not upcoming charges.';
7
+ const cancelledSubscriptionsNote = 'These subscriptions are cancelled and remain active until the date shown below.';
8
+ const getNormalizedStatus = (subscription) => subscription.status.trim().toLowerCase();
9
+ const isCancelledActiveSubscription = (subscription) => {
10
+ return subscription.cancelAtPeriodEnd && getNormalizedStatus(subscription) !== 'canceled';
11
+ };
7
12
  const isCancellableSubscription = (subscription) => {
8
- const normalizedStatus = subscription.status.trim().toLowerCase();
9
- return normalizedStatus !== 'canceled' && !subscription.cancelAtPeriodEnd;
13
+ return getNormalizedStatus(subscription) !== 'canceled' && !subscription.cancelAtPeriodEnd;
10
14
  };
11
15
  const formatDate = (value) => {
12
16
  if (!value) {
@@ -51,8 +55,8 @@ const resolveCreatedDate = (subscription) => {
51
55
  return formatDate(subscription.createdAt || subscription.currentPeriodStart);
52
56
  };
53
57
  const resolveBillingDateLabel = (subscription, options = {}) => {
54
- const normalizedStatus = subscription.status.trim().toLowerCase();
55
- if (subscription.cancelAtPeriodEnd) {
58
+ const normalizedStatus = getNormalizedStatus(subscription);
59
+ if (options.cancelled || subscription.cancelAtPeriodEnd) {
56
60
  return `Active until: ${formatDate(subscription.currentPeriodEnd)}`;
57
61
  }
58
62
  if (options.past && normalizedStatus === 'canceled') {
@@ -64,6 +68,16 @@ const resolveBillingDateLabel = (subscription, options = {}) => {
64
68
  return `Next charge: ${formatDate(subscription.currentPeriodEnd)}`;
65
69
  };
66
70
  const toManageSubscriptions = (subscriptions) => subscriptions;
71
+ const resolveSubscriptionStatusLabel = (subscription) => {
72
+ if (isCancelledActiveSubscription(subscription)) {
73
+ return 'Cancelled';
74
+ }
75
+ const normalizedStatus = getNormalizedStatus(subscription);
76
+ if (!normalizedStatus) {
77
+ return 'Active';
78
+ }
79
+ return `${normalizedStatus.charAt(0).toUpperCase()}${normalizedStatus.slice(1)}`;
80
+ };
67
81
  export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId, plans, }) {
68
82
  var _a, _b;
69
83
  const { attributes, completeStep, goToStep, setAnswer, setUser, user } = useFunnel();
@@ -114,7 +128,8 @@ export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId,
114
128
  }, [content.errorMessages.load]);
115
129
  const subscriptions = useMemo(() => { var _a; return toManageSubscriptions((_a = data === null || data === void 0 ? void 0 : data.subscriptions) !== null && _a !== void 0 ? _a : []); }, [data]);
116
130
  const activeSubscriptions = useMemo(() => subscriptions.filter(isCancellableSubscription), [subscriptions]);
117
- const pastSubscriptions = useMemo(() => subscriptions.filter((subscription) => !isCancellableSubscription(subscription)), [subscriptions]);
131
+ const cancelledSubscriptions = useMemo(() => subscriptions.filter(isCancelledActiveSubscription), [subscriptions]);
132
+ const pastSubscriptions = useMemo(() => subscriptions.filter((subscription) => !isCancellableSubscription(subscription) && !isCancelledActiveSubscription(subscription)), [subscriptions]);
118
133
  const selectedSubscription = (_a = activeSubscriptions.find((subscription) => subscription.id === selectedSubscriptionId)) !== null && _a !== void 0 ? _a : null;
119
134
  const selectedReason = (_b = content.whyStage.reasons.find((reason) => reason.id === selectedReasonId)) !== null && _b !== void 0 ? _b : null;
120
135
  const handleSubscriptionSelected = (subscription) => {
@@ -192,11 +207,11 @@ export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId,
192
207
  setCancelInFlight(false);
193
208
  }
194
209
  };
195
- 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 && 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: () => {
210
+ 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: () => {
196
211
  void handleCancelSubscription();
197
212
  }, children: cancelInFlight
198
213
  ? content.confirmStage.cancellingLabel
199
- : 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-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: () => {
214
+ : 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: () => {
200
215
  if (homeStepId) {
201
216
  goToStep(homeStepId);
202
217
  return;
@@ -204,8 +219,16 @@ export function ManageSubscriptionScreen({ stepId, content, homeStepId, nodeId,
204
219
  setStage('subscriptions');
205
220
  }, children: content.doneStage.returnHomeLabel })] })) : null] }), _jsx("style", { children: manageSubscriptionStyles })] }));
206
221
  }
207
- function SubscriptionSection({ title, subscriptions, plans, onSelect, description, past = false, }) {
208
- 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: past ? 'manage-subscription-row is-past' : 'manage-subscription-row', disabled: past, onClick: () => onSelect(subscription), children: [_jsx("span", { className: 'manage-subscription-radio', "aria-hidden": true }), _jsxs("span", { className: 'manage-subscription-row-title', children: ["Subscription #", formatSubscriptionNumber(subscription)] }), _jsx("span", { className: 'manage-subscription-row-plan', children: resolvePlanLine(subscription, plans) }), _jsxs("span", { className: 'manage-subscription-row-meta', children: [_jsx("span", { children: resolveBillingDateLabel(subscription, { past }) }), _jsxs("span", { children: ["Created at: ", resolveCreatedDate(subscription)] })] })] }) }, subscription.id))) })] }));
222
+ function SubscriptionSection({ title, subscriptions, plans, onSelect, description, cancelled = false, past = false, }) {
223
+ const isDisabled = cancelled || past;
224
+ const rowClassName = cancelled
225
+ ? 'manage-subscription-row is-cancelled'
226
+ : past
227
+ ? 'manage-subscription-row is-past'
228
+ : 'manage-subscription-row';
229
+ 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
230
+ ? resolveBillingDateLabel(subscription, { cancelled })
231
+ : resolveBillingDateLabel(subscription, { past }) }), _jsxs("span", { children: ["Created at: ", resolveCreatedDate(subscription)] })] })] }) }, subscription.id))) })] }));
209
232
  }
210
233
  const manageSubscriptionStyles = `
211
234
  .manage-subscription {
@@ -295,6 +318,11 @@ const manageSubscriptionStyles = `
295
318
  opacity: 0.82;
296
319
  }
297
320
 
321
+ .manage-subscription-row.is-cancelled {
322
+ border-left-color: var(--color-warning, #f28100);
323
+ cursor: default;
324
+ }
325
+
298
326
  .manage-subscription-radio {
299
327
  width: 14px;
300
328
  height: 14px;
@@ -303,12 +331,31 @@ const manageSubscriptionStyles = `
303
331
  display: block;
304
332
  }
305
333
 
334
+ .manage-subscription-row-heading {
335
+ width: 100%;
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: space-between;
339
+ gap: 8px;
340
+ }
341
+
306
342
  .manage-subscription-row-title {
307
343
  font-size: 15px;
308
344
  line-height: 1.25;
309
345
  font-weight: 800;
310
346
  }
311
347
 
348
+ .manage-subscription-row-status {
349
+ border-radius: 999px;
350
+ background: color-mix(in srgb, var(--color-warning, #f28100) 16%, var(--color-surface, #fff) 84%);
351
+ color: var(--color-warning, #f28100);
352
+ padding: 3px 8px;
353
+ font-size: 10px;
354
+ line-height: 1.2;
355
+ font-weight: 800;
356
+ text-transform: uppercase;
357
+ }
358
+
312
359
  .manage-subscription-row-plan {
313
360
  font-size: 13px;
314
361
  line-height: 1.2;
@@ -326,6 +373,11 @@ const manageSubscriptionStyles = `
326
373
  line-height: 1.3;
327
374
  }
328
375
 
376
+ .manage-subscription-row-meta .is-active-until {
377
+ color: var(--color-text, #262729);
378
+ font-weight: 800;
379
+ }
380
+
329
381
  .manage-subscription-support {
330
382
  margin: 30px 0 0;
331
383
  color: var(--color-text, #262729);
@@ -371,6 +423,19 @@ const manageSubscriptionStyles = `
371
423
  font-weight: 800;
372
424
  }
373
425
 
426
+ .manage-subscription-done-status {
427
+ width: fit-content;
428
+ margin: 14px auto 0;
429
+ border-radius: 999px;
430
+ background: color-mix(in srgb, var(--color-warning, #f28100) 16%, var(--color-surface, #fff) 84%);
431
+ color: var(--color-warning, #f28100);
432
+ padding: 4px 10px;
433
+ font-size: 11px;
434
+ line-height: 1.2;
435
+ font-weight: 800;
436
+ text-transform: uppercase;
437
+ }
438
+
374
439
  .manage-subscription-reasons {
375
440
  margin: 18px 0 0;
376
441
  padding: 0;
@@ -446,4 +511,5 @@ const manageSubscriptionStyles = `
446
511
  `;
447
512
  export const __manageSubscriptionScreenTestables = {
448
513
  resolveBillingDateLabel,
514
+ resolveSubscriptionStatusLabel,
449
515
  };
@@ -11,8 +11,10 @@ export type StripeCheckoutExpressCheckoutButtonProps = {
11
11
  availabilityPaymentMethods?: readonly (keyof AvailablePaymentMethods)[];
12
12
  initialAvailable?: boolean | null;
13
13
  keepInitialAvailableOnReady?: boolean;
14
+ serverUpdateKey?: string | null;
15
+ onServerUpdate?: () => Promise<boolean>;
14
16
  onAvailabilityChange?: (available: boolean) => void;
15
17
  onError?: (message: string | null) => void;
16
18
  onSuccess?: () => void;
17
19
  };
18
- export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, onAvailabilityChange, onError, onSuccess, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl, customerEmail, className, options, availabilityPaymentMethods, initialAvailable, keepInitialAvailableOnReady, serverUpdateKey, onServerUpdate, onAvailabilityChange, onError, onSuccess, }: StripeCheckoutExpressCheckoutButtonProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useMemo, useState } from 'react';
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
4
  import { ExpressCheckoutElement, useCheckout, } from '@stripe/react-stripe-js/checkout';
5
5
  const buildDefaultOptions = () => ({
6
6
  buttonHeight: 55,
@@ -25,9 +25,12 @@ const buildDefaultOptions = () => ({
25
25
  paypal: 'never',
26
26
  },
27
27
  });
28
- export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, onAvailabilityChange, onError, onSuccess, }) {
28
+ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl, customerEmail, className, options, availabilityPaymentMethods = ['applePay'], initialAvailable, keepInitialAvailableOnReady = false, serverUpdateKey, onServerUpdate, onAvailabilityChange, onError, onSuccess, }) {
29
29
  const checkoutState = useCheckout();
30
+ const appliedServerUpdateKeyRef = useRef('');
31
+ const serverUpdatePromiseRef = useRef(null);
30
32
  const [walletAvailable, setWalletAvailable] = useState(initialAvailable !== null && initialAvailable !== void 0 ? initialAvailable : null);
33
+ const normalizedServerUpdateKey = (serverUpdateKey === null || serverUpdateKey === void 0 ? void 0 : serverUpdateKey.trim()) || '';
31
34
  const effectiveWalletAvailable = initialAvailable === true && walletAvailable !== false
32
35
  ? true
33
36
  : walletAvailable;
@@ -36,6 +39,52 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl,
36
39
  var _a, _b, _c, _d, _e, _f, _g, _h, _j;
37
40
  return Object.assign(Object.assign(Object.assign({}, baseOptions), (options !== null && options !== void 0 ? options : {})), { buttonTheme: Object.assign(Object.assign({}, ((_a = baseOptions.buttonTheme) !== null && _a !== void 0 ? _a : {})), ((_b = options === null || options === void 0 ? void 0 : options.buttonTheme) !== null && _b !== void 0 ? _b : {})), buttonType: Object.assign(Object.assign({}, ((_c = baseOptions.buttonType) !== null && _c !== void 0 ? _c : {})), ((_d = options === null || options === void 0 ? void 0 : options.buttonType) !== null && _d !== void 0 ? _d : {})), layout: Object.assign(Object.assign({}, ((_e = baseOptions.layout) !== null && _e !== void 0 ? _e : {})), ((_f = options === null || options === void 0 ? void 0 : options.layout) !== null && _f !== void 0 ? _f : {})), paymentMethods: Object.assign(Object.assign({}, ((_g = baseOptions.paymentMethods) !== null && _g !== void 0 ? _g : {})), ((_h = options === null || options === void 0 ? void 0 : options.paymentMethods) !== null && _h !== void 0 ? _h : {})), paymentMethodOrder: (_j = options === null || options === void 0 ? void 0 : options.paymentMethodOrder) !== null && _j !== void 0 ? _j : baseOptions.paymentMethodOrder });
38
41
  }, [baseOptions, options]);
42
+ const ensureServerUpdated = useCallback(async () => {
43
+ if (!normalizedServerUpdateKey) {
44
+ return true;
45
+ }
46
+ if (appliedServerUpdateKeyRef.current === normalizedServerUpdateKey) {
47
+ return true;
48
+ }
49
+ if (!onServerUpdate || checkoutState.type !== 'success') {
50
+ return false;
51
+ }
52
+ if (serverUpdatePromiseRef.current) {
53
+ return serverUpdatePromiseRef.current;
54
+ }
55
+ const updatePromise = (async () => {
56
+ const updateResult = await checkoutState.checkout.runServerUpdate(async () => {
57
+ const updated = await onServerUpdate();
58
+ if (!updated) {
59
+ throw new Error('Unable to update checkout session.');
60
+ }
61
+ });
62
+ if (updateResult.type === 'error') {
63
+ onError === null || onError === void 0 ? void 0 : onError(updateResult.error.message || 'Unable to update checkout session.');
64
+ return false;
65
+ }
66
+ appliedServerUpdateKeyRef.current = normalizedServerUpdateKey;
67
+ onError === null || onError === void 0 ? void 0 : onError(null);
68
+ return true;
69
+ })();
70
+ serverUpdatePromiseRef.current = updatePromise;
71
+ try {
72
+ return await updatePromise;
73
+ }
74
+ finally {
75
+ serverUpdatePromiseRef.current = null;
76
+ }
77
+ }, [checkoutState, normalizedServerUpdateKey, onError, onServerUpdate]);
78
+ useEffect(() => {
79
+ if (!normalizedServerUpdateKey) {
80
+ appliedServerUpdateKeyRef.current = '';
81
+ return;
82
+ }
83
+ if (checkoutState.type !== 'success' || !onServerUpdate) {
84
+ return;
85
+ }
86
+ void ensureServerUpdated();
87
+ }, [checkoutState.type, ensureServerUpdated, normalizedServerUpdateKey, onServerUpdate]);
39
88
  const handleConfirm = async (event) => {
40
89
  if (checkoutState.type !== 'success') {
41
90
  const message = checkoutState.type === 'error'
@@ -46,6 +95,13 @@ export function StripeCheckoutExpressCheckoutButton({ beforeConfirm, returnUrl,
46
95
  return;
47
96
  }
48
97
  onError === null || onError === void 0 ? void 0 : onError(null);
98
+ const updated = await ensureServerUpdated();
99
+ if (!updated) {
100
+ const message = 'Unable to update checkout session.';
101
+ onError === null || onError === void 0 ? void 0 : onError(message);
102
+ event.paymentFailed({ reason: 'fail', message });
103
+ return;
104
+ }
49
105
  let preparedCustomerEmail = (customerEmail === null || customerEmail === void 0 ? void 0 : customerEmail.trim()) || null;
50
106
  if (beforeConfirm) {
51
107
  try {
@@ -31,6 +31,11 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
31
31
  expressCheckoutAllowed &&
32
32
  walletAvailable === null;
33
33
  const placeholderIsPreparing = shouldRenderPlaceholder;
34
+ const serverUpdateKey = session.activeClientSecret &&
35
+ session.checkoutSessionId &&
36
+ session.activePlanKey !== session.planKey
37
+ ? session.planKey
38
+ : null;
34
39
  const markWalletUnavailable = useCallback((intentKey) => {
35
40
  setWalletAvailability({
36
41
  available: false,
@@ -105,7 +110,7 @@ export function WalletSubscriptionCheckoutSlot({ amountCents, availabilityPaymen
105
110
  .filter(Boolean)
106
111
  .join(' '), "aria-busy": placeholderIsPreparing || undefined, children: [shouldRenderExpressCheckout &&
107
112
  session.activeClientSecret &&
108
- session.stripePromise ? (_jsx(StripeExpressCheckoutElement, { amountCents: amountCents, availabilityPaymentMethods: [availabilityPaymentMethod], beforeConfirm: beforeConfirm, className: className, clientSecret: session.activeClientSecret, customerEmail: customerEmail, customerName: customerName, onAvailabilityChange: handleAvailabilityChange, onError: handleError, onSuccess: onSuccess, options: options, returnUrl: returnUrl, stripePromise: session.stripePromise, summaryLabel: summaryLabel }, session.activeClientSecret)) : null, shouldRenderPlaceholder ? (_jsx("div", { className: 'wallet-subscription-checkout-slot__placeholder', children: (renderPreparingPlaceholder !== null && renderPreparingPlaceholder !== void 0 ? renderPreparingPlaceholder : renderPlaceholder)({
113
+ session.stripePromise ? (_jsx(StripeExpressCheckoutElement, { amountCents: amountCents, availabilityPaymentMethods: [availabilityPaymentMethod], beforeConfirm: beforeConfirm, className: className, clientSecret: session.activeClientSecret, customerEmail: customerEmail, customerName: customerName, onAvailabilityChange: handleAvailabilityChange, onError: handleError, onSuccess: onSuccess, options: options, returnUrl: returnUrl, serverUpdateKey: serverUpdateKey, stripePromise: session.stripePromise, summaryLabel: summaryLabel, onServerUpdate: session.updateCheckoutSessionPlan })) : null, shouldRenderPlaceholder ? (_jsx("div", { className: 'wallet-subscription-checkout-slot__placeholder', children: (renderPreparingPlaceholder !== null && renderPreparingPlaceholder !== void 0 ? renderPreparingPlaceholder : renderPlaceholder)({
109
114
  className,
110
115
  disabled: slotDisabled,
111
116
  label: placeholderLabel,
@@ -17,6 +17,8 @@ export type StripeSubscriptionCheckoutSessionInput = {
17
17
  };
18
18
  export type StripeSubscriptionCheckoutSession = {
19
19
  activeClientSecret: string | null;
20
+ activePlanKey: string | null;
21
+ checkoutSessionId: string | null;
20
22
  configured: boolean;
21
23
  error: string | null;
22
24
  intentKey: string;
@@ -27,8 +29,10 @@ export type StripeSubscriptionCheckoutSession = {
27
29
  reset: () => void;
28
30
  restartCardCheckout: () => Promise<string | null>;
29
31
  restartWalletCheckout: () => Promise<string | null>;
32
+ planKey: string;
30
33
  setError: (message: string | null) => void;
31
34
  stripePromise: Promise<Stripe | null> | null;
35
+ updateCheckoutSessionPlan: () => Promise<boolean>;
32
36
  };
33
37
  export declare function isInactiveCheckoutSessionError(message?: string | null): boolean;
34
38
  export declare function useStripeSubscriptionCheckoutSession({ checkoutMode, couponId, customerEmail, displayPlan, enabled, onError, plan, returnUrl, runtimeConfig, userId, }: StripeSubscriptionCheckoutSessionInput): StripeSubscriptionCheckoutSession;
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import { useCallback, useEffect, useMemo, useRef, useState, } from 'react';
3
3
  import { createEmptyCheckoutPreparationLoading, } from '../paymentProvider.types.js';
4
- import { createStripeSubscriptionCheckout, getStripePromise, isStripeConfigured, } from '../../services/stripe.service.js';
4
+ import { createStripeSubscriptionCheckout, getStripePromise, isStripeConfigured, updateStripeSubscriptionCheckoutPlan, } from '../../services/stripe.service.js';
5
5
  const getFriendlyStripeCheckoutError = (error) => {
6
6
  const message = error instanceof Error ? error.message : 'Unable to initialize checkout';
7
7
  return message === 'Internal server error'
@@ -14,9 +14,11 @@ export function isInactiveCheckoutSessionError(message) {
14
14
  return /\bcheckout session\b.*\bno longer active\b/i.test((_a = message === null || message === void 0 ? void 0 : message.trim()) !== null && _a !== void 0 ? _a : '');
15
15
  }
16
16
  export function useStripeSubscriptionCheckoutSession({ checkoutMode, couponId, customerEmail, displayPlan, enabled = true, onError, plan, returnUrl, runtimeConfig, userId, }) {
17
- var _a, _b, _c, _d;
17
+ var _a, _b, _c, _d, _e;
18
18
  const [clientSecret, setClientSecret] = useState(null);
19
19
  const [clientSecretIntentKey, setClientSecretIntentKey] = useState(null);
20
+ const [checkoutSessionId, setCheckoutSessionId] = useState(null);
21
+ const [clientSecretPlanKey, setClientSecretPlanKey] = useState(null);
20
22
  const [error, setErrorState] = useState(null);
21
23
  const [loading, setLoading] = useState(createEmptyCheckoutPreparationLoading);
22
24
  const [runtimeStripePublishableKey, setRuntimeStripePublishableKey] = useState(null);
@@ -35,20 +37,27 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, couponId, c
35
37
  'stripe',
36
38
  checkoutMode,
37
39
  runtimeConfigSignature,
38
- (_a = plan === null || plan === void 0 ? void 0 : plan.id) !== null && _a !== void 0 ? _a : '',
39
- (_b = plan === null || plan === void 0 ? void 0 : plan.amountCents) !== null && _b !== void 0 ? _b : '',
40
- (_c = displayPlan === null || displayPlan === void 0 ? void 0 : displayPlan.id) !== null && _c !== void 0 ? _c : '',
41
- (_d = displayPlan === null || displayPlan === void 0 ? void 0 : displayPlan.amountCents) !== null && _d !== void 0 ? _d : '',
42
40
  (couponId === null || couponId === void 0 ? void 0 : couponId.trim()) || '',
43
41
  returnUrl.trim(),
44
42
  ].join('|');
43
+ const planKey = [
44
+ (_a = plan === null || plan === void 0 ? void 0 : plan.id) !== null && _a !== void 0 ? _a : '',
45
+ (_b = plan === null || plan === void 0 ? void 0 : plan.providerPlanId) !== null && _b !== void 0 ? _b : '',
46
+ (_c = plan === null || plan === void 0 ? void 0 : plan.amountCents) !== null && _c !== void 0 ? _c : '',
47
+ (_d = displayPlan === null || displayPlan === void 0 ? void 0 : displayPlan.id) !== null && _d !== void 0 ? _d : '',
48
+ (_e = displayPlan === null || displayPlan === void 0 ? void 0 : displayPlan.amountCents) !== null && _e !== void 0 ? _e : '',
49
+ ].join('|');
45
50
  const activeClientSecret = clientSecretIntentKey === intentKey ? clientSecret : null;
51
+ const activeCheckoutSessionId = clientSecretIntentKey === intentKey ? checkoutSessionId : null;
52
+ const activePlanKey = clientSecretIntentKey === intentKey ? clientSecretPlanKey : null;
46
53
  const stripePromise = useMemo(() => getStripePromise(checkoutMode, runtimeStripePublishableKey), [checkoutMode, runtimeStripePublishableKey]);
47
54
  const clearActiveCheckoutSession = useCallback(() => {
48
55
  requestIdRef.current += 1;
49
56
  creatingIntentRef.current = null;
50
57
  setClientSecret(null);
51
58
  setClientSecretIntentKey(null);
59
+ setCheckoutSessionId(null);
60
+ setClientSecretPlanKey(null);
52
61
  }, []);
53
62
  const setError = useCallback((message) => {
54
63
  if (isInactiveCheckoutSessionError(message)) {
@@ -89,10 +98,11 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, couponId, c
89
98
  clearActiveCheckoutSession();
90
99
  }
91
100
  const requestKey = intentKey;
101
+ const requestPlanKey = planKey;
92
102
  const requestId = requestIdRef.current + 1;
93
103
  requestIdRef.current = requestId;
94
104
  const subscriptionRequest = (async () => {
95
- var _a, _b, _c, _d, _e, _f, _g;
105
+ var _a, _b, _c, _d, _e, _f, _g, _h;
96
106
  try {
97
107
  const response = await createStripeSubscriptionCheckout({
98
108
  plan,
@@ -113,18 +123,20 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, couponId, c
113
123
  }
114
124
  setClientSecret(response.clientSecret);
115
125
  setClientSecretIntentKey(requestKey);
126
+ setCheckoutSessionId(((_d = response.checkoutSessionId) === null || _d === void 0 ? void 0 : _d.trim()) || null);
127
+ setClientSecretPlanKey(requestPlanKey);
116
128
  return response.clientSecret;
117
129
  }
118
130
  catch (subscriptionError) {
119
- if (((_d = creatingIntentRef.current) === null || _d === void 0 ? void 0 : _d.key) === requestKey &&
120
- ((_e = creatingIntentRef.current) === null || _e === void 0 ? void 0 : _e.id) === requestId) {
131
+ if (((_e = creatingIntentRef.current) === null || _e === void 0 ? void 0 : _e.key) === requestKey &&
132
+ ((_f = creatingIntentRef.current) === null || _f === void 0 ? void 0 : _f.id) === requestId) {
121
133
  setError(getFriendlyStripeCheckoutError(subscriptionError));
122
134
  }
123
135
  return null;
124
136
  }
125
137
  finally {
126
- if (((_f = creatingIntentRef.current) === null || _f === void 0 ? void 0 : _f.key) === requestKey &&
127
- ((_g = creatingIntentRef.current) === null || _g === void 0 ? void 0 : _g.id) === requestId) {
138
+ if (((_g = creatingIntentRef.current) === null || _g === void 0 ? void 0 : _g.key) === requestKey &&
139
+ ((_h = creatingIntentRef.current) === null || _h === void 0 ? void 0 : _h.id) === requestId) {
128
140
  creatingIntentRef.current = null;
129
141
  }
130
142
  }
@@ -145,6 +157,7 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, couponId, c
145
157
  displayPlan,
146
158
  intentKey,
147
159
  plan,
160
+ planKey,
148
161
  returnUrl,
149
162
  runtimeConfig,
150
163
  setError,
@@ -178,8 +191,49 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, couponId, c
178
191
  const prepareWalletCheckout = useCallback(() => prepareCheckout('wallet'), [prepareCheckout]);
179
192
  const restartCardCheckout = useCallback(() => prepareCheckout('card', { forceNew: true }), [prepareCheckout]);
180
193
  const restartWalletCheckout = useCallback(() => prepareCheckout('wallet', { forceNew: true }), [prepareCheckout]);
194
+ const updateCheckoutSessionPlan = useCallback(async () => {
195
+ if (!configured || !plan || !displayPlan || !activeCheckoutSessionId) {
196
+ return false;
197
+ }
198
+ if (activePlanKey === planKey) {
199
+ return true;
200
+ }
201
+ setError(null);
202
+ try {
203
+ await updateStripeSubscriptionCheckoutPlan({
204
+ checkoutSessionId: activeCheckoutSessionId,
205
+ plan,
206
+ couponId,
207
+ customerEmail,
208
+ user_id: userId,
209
+ environment: checkoutMode,
210
+ runtimeConfig,
211
+ });
212
+ setClientSecretPlanKey(planKey);
213
+ return true;
214
+ }
215
+ catch (subscriptionError) {
216
+ setError(getFriendlyStripeCheckoutError(subscriptionError));
217
+ return false;
218
+ }
219
+ }, [
220
+ activeCheckoutSessionId,
221
+ activePlanKey,
222
+ checkoutMode,
223
+ configured,
224
+ couponId,
225
+ customerEmail,
226
+ displayPlan,
227
+ plan,
228
+ planKey,
229
+ runtimeConfig,
230
+ setError,
231
+ userId,
232
+ ]);
181
233
  return {
182
234
  activeClientSecret,
235
+ activePlanKey,
236
+ checkoutSessionId: activeCheckoutSessionId,
183
237
  configured,
184
238
  error,
185
239
  intentKey,
@@ -190,7 +244,9 @@ export function useStripeSubscriptionCheckoutSession({ checkoutMode, couponId, c
190
244
  reset,
191
245
  restartCardCheckout,
192
246
  restartWalletCheckout,
247
+ planKey,
193
248
  setError,
194
249
  stripePromise,
250
+ updateCheckoutSessionPlan,
195
251
  };
196
252
  }
@@ -80,7 +80,21 @@ type CreateSubscriptionCheckoutResponse = {
80
80
  stripePublishableKey?: string;
81
81
  environment?: 'test' | 'live';
82
82
  };
83
+ type UpdateSubscriptionCheckoutPlanInput = {
84
+ checkoutSessionId: string;
85
+ plan: CheckoutSessionPlanInput;
86
+ couponId?: string | null;
87
+ customerEmail?: string | null;
88
+ user_id?: string | null;
89
+ environment?: RuntimeMode;
90
+ runtimeConfig?: StripeRuntimeConfigOverrides;
91
+ };
92
+ type UpdateSubscriptionCheckoutPlanResponse = {
93
+ checkoutSessionId: string;
94
+ environment?: 'test' | 'live';
95
+ };
83
96
  export declare function createStripeSubscriptionCheckout(input: CreateSubscriptionCheckoutInput): Promise<CreateSubscriptionCheckoutResponse>;
97
+ export declare function updateStripeSubscriptionCheckoutPlan(input: UpdateSubscriptionCheckoutPlanInput): Promise<UpdateSubscriptionCheckoutPlanResponse>;
84
98
  type CreateCheckoutSessionInput = {
85
99
  plan: CheckoutSessionPlanInput;
86
100
  successUrl: string;
@@ -588,6 +588,42 @@ export async function createStripeSubscriptionCheckout(input) {
588
588
  }
589
589
  return (await response.json());
590
590
  }
591
+ export async function updateStripeSubscriptionCheckoutPlan(input) {
592
+ var _a, _b, _c, _d, _e;
593
+ const recurring = resolveCheckoutPlanRecurringConfig(input.plan);
594
+ if (!recurring) {
595
+ throw new Error('Unable to resolve the recurring interval for the selected plan');
596
+ }
597
+ const runtimeConfig = resolveStripeRuntimeConfig(input.runtimeConfig);
598
+ const response = await fetch(buildStripeRuntimeApiUrl('/sdk/public/payments/subscriptions/update-plan', runtimeConfig), {
599
+ method: 'POST',
600
+ headers: buildStripeRuntimeHeaders(runtimeConfig, {
601
+ 'Content-Type': 'application/json',
602
+ }),
603
+ body: JSON.stringify({
604
+ publishableKey: runtimeConfig.funnelSdkPublishableKey || undefined,
605
+ funnelId: runtimeConfig.funnelId || undefined,
606
+ funnelVersionId: runtimeConfig.funnelVersionId || undefined,
607
+ checkoutSessionId: input.checkoutSessionId.trim(),
608
+ planId: getPaywallPlanSelectionValue(input.plan),
609
+ providerPlanId: ((_a = input.plan.providerPlanId) === null || _a === void 0 ? void 0 : _a.trim()) || undefined,
610
+ title: input.plan.title,
611
+ description: ((_b = input.plan.description) === null || _b === void 0 ? void 0 : _b.trim()) || undefined,
612
+ amountCents: input.plan.amountCents,
613
+ billingInterval: recurring.interval,
614
+ billingIntervalCount: recurring.intervalCount,
615
+ couponId: ((_c = input.couponId) === null || _c === void 0 ? void 0 : _c.trim()) || undefined,
616
+ customerEmail: ((_d = input.customerEmail) === null || _d === void 0 ? void 0 : _d.trim()) || undefined,
617
+ user_id: ((_e = input.user_id) === null || _e === void 0 ? void 0 : _e.trim()) || undefined,
618
+ environment: input.environment || undefined,
619
+ }),
620
+ });
621
+ if (!response.ok) {
622
+ const errorMessage = await parseErrorMessage(response, 'Unable to update subscription checkout');
623
+ throw new Error(errorMessage);
624
+ }
625
+ return (await response.json());
626
+ }
591
627
  export async function createStripeCheckoutSession(input) {
592
628
  var _a, _b, _c, _d, _e;
593
629
  const recurring = resolveCheckoutPlanRecurringConfig(input.plan);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@funnelsgrove/payments",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "main": "./dist/index.js",