@blocklet/payment-react-headless 1.26.1 → 1.26.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/es/checkout/context/CheckoutProvider.js +26 -10
  2. package/es/checkout/context/SessionContext.d.ts +3 -1
  3. package/es/checkout/core/crossSell.d.ts +3 -2
  4. package/es/checkout/core/crossSell.js +24 -8
  5. package/es/checkout/core/customerForm.js +0 -6
  6. package/es/checkout/core/lineItems.d.ts +6 -5
  7. package/es/checkout/core/lineItems.js +48 -17
  8. package/es/checkout/core/paymentMethod.js +15 -7
  9. package/es/checkout/core/pricing.js +1 -2
  10. package/es/checkout/core/promotion.d.ts +1 -1
  11. package/es/checkout/core/promotion.js +2 -1
  12. package/es/checkout/hooks/useBillingInterval.js +8 -5
  13. package/es/checkout/hooks/useCheckout.js +17 -11
  14. package/es/checkout/hooks/useCheckoutSession.d.ts +2 -2
  15. package/es/checkout/hooks/useCheckoutSession.js +7 -0
  16. package/es/checkout/hooks/useCheckoutStatus.d.ts +1 -1
  17. package/es/checkout/hooks/useCrossSell.js +11 -3
  18. package/es/checkout/hooks/useCustomerForm.d.ts +4 -0
  19. package/es/checkout/hooks/useCustomerForm.js +6 -0
  20. package/es/checkout/hooks/useLineItems.js +36 -9
  21. package/es/checkout/hooks/usePaymentMethod.d.ts +1 -1
  22. package/es/checkout/hooks/usePaymentMethod.js +8 -4
  23. package/es/checkout/hooks/usePricing.d.ts +1 -1
  24. package/es/checkout/hooks/usePricing.js +4 -4
  25. package/es/checkout/hooks/useSubmit.js +6 -1
  26. package/es/checkout/hooks/useUpsell.js +3 -3
  27. package/es/checkout/types.d.ts +5 -3
  28. package/lib/checkout/context/CheckoutProvider.js +21 -12
  29. package/lib/checkout/context/SessionContext.d.ts +3 -1
  30. package/lib/checkout/core/crossSell.d.ts +3 -2
  31. package/lib/checkout/core/crossSell.js +36 -8
  32. package/lib/checkout/core/customerForm.js +0 -5
  33. package/lib/checkout/core/lineItems.d.ts +6 -5
  34. package/lib/checkout/core/lineItems.js +72 -18
  35. package/lib/checkout/core/paymentMethod.js +14 -8
  36. package/lib/checkout/core/pricing.js +1 -2
  37. package/lib/checkout/core/promotion.d.ts +1 -1
  38. package/lib/checkout/core/promotion.js +4 -1
  39. package/lib/checkout/hooks/useBillingInterval.js +10 -5
  40. package/lib/checkout/hooks/useCheckout.js +16 -11
  41. package/lib/checkout/hooks/useCheckoutSession.d.ts +2 -2
  42. package/lib/checkout/hooks/useCheckoutSession.js +7 -0
  43. package/lib/checkout/hooks/useCheckoutStatus.d.ts +1 -1
  44. package/lib/checkout/hooks/useCrossSell.js +5 -3
  45. package/lib/checkout/hooks/useCustomerForm.d.ts +4 -0
  46. package/lib/checkout/hooks/useCustomerForm.js +6 -0
  47. package/lib/checkout/hooks/useLineItems.js +10 -8
  48. package/lib/checkout/hooks/usePaymentMethod.d.ts +1 -1
  49. package/lib/checkout/hooks/usePaymentMethod.js +14 -4
  50. package/lib/checkout/hooks/usePricing.d.ts +1 -1
  51. package/lib/checkout/hooks/usePricing.js +4 -4
  52. package/lib/checkout/hooks/useSubmit.js +8 -1
  53. package/lib/checkout/hooks/useUpsell.js +5 -3
  54. package/lib/checkout/types.d.ts +5 -3
  55. package/package.json +3 -3
  56. package/src/checkout/context/CheckoutProvider.tsx +38 -17
  57. package/src/checkout/context/SessionContext.ts +3 -1
  58. package/src/checkout/core/crossSell.ts +29 -8
  59. package/src/checkout/core/customerForm.ts +0 -6
  60. package/src/checkout/core/lineItems.ts +62 -18
  61. package/src/checkout/core/paymentMethod.ts +24 -7
  62. package/src/checkout/core/pricing.ts +1 -2
  63. package/src/checkout/core/promotion.ts +6 -2
  64. package/src/checkout/hooks/useBillingInterval.ts +8 -5
  65. package/src/checkout/hooks/useCheckout.ts +20 -12
  66. package/src/checkout/hooks/useCheckoutSession.ts +12 -3
  67. package/src/checkout/hooks/useCheckoutStatus.ts +1 -1
  68. package/src/checkout/hooks/useCrossSell.ts +11 -3
  69. package/src/checkout/hooks/useCustomerForm.ts +12 -0
  70. package/src/checkout/hooks/useLineItems.ts +42 -9
  71. package/src/checkout/hooks/usePaymentMethod.ts +13 -5
  72. package/src/checkout/hooks/usePricing.ts +5 -4
  73. package/src/checkout/hooks/useSubmit.ts +13 -1
  74. package/src/checkout/hooks/useUpsell.ts +3 -3
  75. package/src/checkout/types.ts +4 -2
@@ -28,11 +28,12 @@ export function CheckoutProvider({ sessionId, children }) {
28
28
  } = useCheckoutSession(sessionId);
29
29
  const session = sessionData?.checkoutSession;
30
30
  const effectiveSessionId = resolvedSessionId || sessionId;
31
- const items = session?.line_items || [];
31
+ const items = useMemo(() => session?.line_items || [], [session?.line_items]);
32
32
  const isDonation = session?.submit_type === "donate";
33
33
  const sessionValue = useMemo(
34
34
  () => ({
35
35
  sessionData,
36
+ setSessionData,
36
37
  sessionId,
37
38
  effectiveSessionId,
38
39
  isLoading,
@@ -49,6 +50,7 @@ export function CheckoutProvider({ sessionId, children }) {
49
50
  }),
50
51
  [
51
52
  sessionData,
53
+ setSessionData,
52
54
  sessionId,
53
55
  effectiveSessionId,
54
56
  isLoading,
@@ -64,14 +66,18 @@ export function CheckoutProvider({ sessionId, children }) {
64
66
  pageInfo
65
67
  ]
66
68
  );
67
- const paymentMethodHook = usePaymentMethodHook(sessionData, effectiveSessionId, refresh);
69
+ const paymentMethodHook = usePaymentMethodHook(sessionData, effectiveSessionId, refresh, setSessionData);
68
70
  const prevCurrencyRef = useRef(null);
69
71
  useEffect(() => {
70
72
  const currId = paymentMethodHook.currency?.id || null;
71
73
  if (!currId || !session || session.status === "complete") return;
72
74
  if (prevCurrencyRef.current === null || currId !== prevCurrencyRef.current) {
73
75
  prevCurrencyRef.current = currId;
74
- recalculatePromotionIfNeeded(session, effectiveSessionId, currId).then(() => refresh(true));
76
+ recalculatePromotionIfNeeded(session, effectiveSessionId, currId).then((recalculated) => {
77
+ if (recalculated) {
78
+ refresh(true);
79
+ }
80
+ });
75
81
  }
76
82
  }, [paymentMethodHook.currency?.id, session?.id]);
77
83
  const paymentMethodValue = useMemo(
@@ -89,6 +95,7 @@ export function CheckoutProvider({ sessionId, children }) {
89
95
  setCurrency: paymentMethodHook.setCurrency,
90
96
  stripe: paymentMethodHook.stripe
91
97
  }),
98
+ // eslint-disable-next-line react-hooks/exhaustive-deps
92
99
  [
93
100
  paymentMethodHook.current,
94
101
  paymentMethodHook.currency,
@@ -114,7 +121,7 @@ export function CheckoutProvider({ sessionId, children }) {
114
121
  const mountedRef = useRef(true);
115
122
  const hasDynamicPricing = useMemo(() => checkHasDynamicPricing(items), [items]);
116
123
  const fetchRate = useMemoizedFn(async () => {
117
- if (!effectiveSessionId || !hasDynamicPricing || paymentMethodHook.isStripe) {
124
+ if (!effectiveSessionId || !hasDynamicPricing || paymentMethodHook.isStripe || paymentMethodHook.switching) {
118
125
  setRateStatus(hasDynamicPricing ? "unavailable" : "available");
119
126
  if (paymentMethodHook.isStripe) {
120
127
  setExchangeRate(null);
@@ -148,17 +155,19 @@ export function CheckoutProvider({ sessionId, children }) {
148
155
  });
149
156
  useEffect(() => {
150
157
  mountedRef.current = true;
151
- if (!hasDynamicPricing || paymentMethodHook.isStripe || !effectiveSessionId || session?.status === "complete") {
158
+ if (!hasDynamicPricing || paymentMethodHook.isStripe || paymentMethodHook.switching || !effectiveSessionId || session?.status === "complete") {
152
159
  if (paymentMethodHook.isStripe) {
153
160
  setExchangeRate(null);
154
161
  setRateProvider(null);
155
162
  setRateProviderDisplay(null);
156
163
  setRateFetchedAt(null);
157
164
  }
158
- if (session?.status === "complete") {
159
- setRateStatus("available");
160
- } else {
161
- setRateStatus(hasDynamicPricing ? "unavailable" : "available");
165
+ if (!paymentMethodHook.switching) {
166
+ if (session?.status === "complete") {
167
+ setRateStatus("available");
168
+ } else {
169
+ setRateStatus(hasDynamicPricing ? "unavailable" : "available");
170
+ }
162
171
  }
163
172
  return void 0;
164
173
  }
@@ -175,7 +184,14 @@ export function CheckoutProvider({ sessionId, children }) {
175
184
  if (intervalRef.current) clearInterval(intervalRef.current);
176
185
  document.removeEventListener("visibilitychange", handleVisibility);
177
186
  };
178
- }, [hasDynamicPricing, paymentMethodHook.isStripe, effectiveSessionId, paymentMethodHook.currency?.id, session?.status]);
187
+ }, [
188
+ hasDynamicPricing,
189
+ paymentMethodHook.isStripe,
190
+ paymentMethodHook.switching,
191
+ effectiveSessionId,
192
+ paymentMethodHook.currency?.id,
193
+ session?.status
194
+ ]);
179
195
  const exchangeRateValue = useMemo(
180
196
  () => ({
181
197
  rate: exchangeRate,
@@ -2,11 +2,13 @@ import type { TLineItemExpanded, TCheckoutSessionExpanded } from '@blocklet/paym
2
2
  import type { SessionData } from '../hooks/useCheckoutSession';
3
3
  export interface SessionContextValue {
4
4
  sessionData: SessionData | null;
5
+ /** Directly replace session data (e.g. using PUT response to skip redundant GET) */
6
+ setSessionData: (data: SessionData) => void;
5
7
  sessionId: string;
6
8
  effectiveSessionId: string;
7
9
  isLoading: boolean;
8
10
  error: string | null;
9
- errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
11
+ errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | 'STOP_ACCEPTING_ORDERS' | null;
10
12
  refresh: (forceRefresh?: boolean) => Promise<void>;
11
13
  items: TLineItemExpanded[];
12
14
  session: TCheckoutSessionExpanded | null | undefined;
@@ -1,4 +1,5 @@
1
1
  import type { TCheckoutSessionExpanded, TPrice } from '@blocklet/payment-types';
2
- export declare function addCrossSellItem(sessionId: string, crossSellItemId: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>): Promise<void>;
3
- export declare function removeCrossSellItem(sessionId: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>): Promise<void>;
2
+ import type { SessionData } from '../hooks/useCheckoutSession';
3
+ export declare function addCrossSellItem(sessionId: string, crossSellItemId: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>, sessionData?: SessionData | null, setSessionData?: (data: SessionData) => void): Promise<void>;
4
+ export declare function removeCrossSellItem(sessionId: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>, sessionData?: SessionData | null, setSessionData?: (data: SessionData) => void): Promise<void>;
4
5
  export declare function fetchCrossSellItem(sessionId: string): Promise<TPrice | null>;
@@ -1,14 +1,30 @@
1
1
  import api, { API } from "../../shared/api.js";
2
2
  import { recalculatePromotionIfNeeded } from "./lineItems.js";
3
- export async function addCrossSellItem(sessionId, crossSellItemId, session, currencyId, refresh) {
4
- await api.put(API.CROSS_SELL(sessionId), { to: crossSellItemId });
5
- await recalculatePromotionIfNeeded(session, sessionId, currencyId);
6
- await refresh(true);
3
+ export async function addCrossSellItem(sessionId, crossSellItemId, session, currencyId, refresh, sessionData, setSessionData) {
4
+ const { data } = await api.put(API.CROSS_SELL(sessionId), { to: crossSellItemId });
5
+ let finalSession = data;
6
+ if (data.discounts?.length) {
7
+ const recalculated = await recalculatePromotionIfNeeded(session, sessionId, currencyId);
8
+ if (recalculated) finalSession = recalculated;
9
+ }
10
+ if (sessionData && setSessionData) {
11
+ setSessionData({ ...sessionData, checkoutSession: finalSession, quotes: void 0 });
12
+ } else {
13
+ await refresh(true);
14
+ }
7
15
  }
8
- export async function removeCrossSellItem(sessionId, session, currencyId, refresh) {
9
- await api.delete(API.CROSS_SELL(sessionId));
10
- await recalculatePromotionIfNeeded(session, sessionId, currencyId);
11
- await refresh(true);
16
+ export async function removeCrossSellItem(sessionId, session, currencyId, refresh, sessionData, setSessionData) {
17
+ const { data } = await api.delete(API.CROSS_SELL(sessionId));
18
+ let finalSession = data;
19
+ if (data.discounts?.length) {
20
+ const recalculated = await recalculatePromotionIfNeeded(session, sessionId, currencyId);
21
+ if (recalculated) finalSession = recalculated;
22
+ }
23
+ if (sessionData && setSessionData) {
24
+ setSessionData({ ...sessionData, checkoutSession: finalSession, quotes: void 0 });
25
+ } else {
26
+ await refresh(true);
27
+ }
12
28
  }
13
29
  const pendingFetches = /* @__PURE__ */ new Map();
14
30
  export function fetchCrossSellItem(sessionId) {
@@ -32,12 +32,6 @@ export function buildFields(session) {
32
32
  required: true,
33
33
  group: "address"
34
34
  },
35
- {
36
- name: "billing_address.line2",
37
- type: "text",
38
- required: false,
39
- group: "address"
40
- },
41
35
  {
42
36
  name: "billing_address.city",
43
37
  type: "text",
@@ -1,7 +1,8 @@
1
1
  import type { TCheckoutSessionExpanded, TLineItemExpanded, TPrice } from '@blocklet/payment-types';
2
- export declare function recalculatePromotionIfNeeded(session: TCheckoutSessionExpanded | undefined | null, sessionId: string, currencyId: string | null | undefined): Promise<void>;
3
- export declare function adjustQuantity(sessionId: string, itemId: string, qty: number, currencyId: string | null | undefined, session: TCheckoutSessionExpanded | undefined | null, refresh: (force?: boolean) => Promise<void>): Promise<void>;
4
- export declare function performUpsell(sessionId: string, fromId: string, toId: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>): Promise<void>;
5
- export declare function performDownsell(sessionId: string, priceId: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>): Promise<void>;
6
- export declare function changeDonationAmount(sessionId: string, priceId: string, amount: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>): Promise<void>;
2
+ import type { SessionData } from '../hooks/useCheckoutSession';
3
+ export declare function recalculatePromotionIfNeeded(session: TCheckoutSessionExpanded | undefined | null, sessionId: string, currencyId: string | null | undefined): Promise<TCheckoutSessionExpanded | null>;
4
+ export declare function adjustQuantity(sessionId: string, itemId: string, qty: number, currencyId: string | null | undefined, session: TCheckoutSessionExpanded | undefined | null, refresh: (force?: boolean) => Promise<void>, sessionData?: SessionData | null, setSessionData?: (data: SessionData) => void): Promise<void>;
5
+ export declare function performUpsell(sessionId: string, fromId: string, toId: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>, sessionData?: SessionData | null, setSessionData?: (data: SessionData) => void): Promise<void>;
6
+ export declare function performDownsell(sessionId: string, priceId: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>, sessionData?: SessionData | null, setSessionData?: (data: SessionData) => void): Promise<void>;
7
+ export declare function changeDonationAmount(sessionId: string, priceId: string, amount: string, session: TCheckoutSessionExpanded | undefined | null, currencyId: string | null | undefined, refresh: (force?: boolean) => Promise<void>, sessionData?: SessionData | null, setSessionData?: (data: SessionData) => void): Promise<void>;
7
8
  export declare function getCrossSellItem(items: TLineItemExpanded[]): TPrice | null;
@@ -1,52 +1,83 @@
1
1
  import api, { API } from "../../shared/api.js";
2
2
  import { recalculatePromotion, hasAppliedDiscounts } from "./promotion.js";
3
3
  export async function recalculatePromotionIfNeeded(session, sessionId, currencyId) {
4
- if (!hasAppliedDiscounts(session)) return;
4
+ if (!hasAppliedDiscounts(session)) return null;
5
5
  try {
6
- await recalculatePromotion(sessionId, currencyId);
6
+ return await recalculatePromotion(sessionId, currencyId);
7
7
  } catch {
8
+ return null;
8
9
  }
9
10
  }
10
- export async function adjustQuantity(sessionId, itemId, qty, currencyId, session, refresh) {
11
- await api.put(API.ADJUST_QUANTITY(sessionId), {
11
+ export async function adjustQuantity(sessionId, itemId, qty, currencyId, session, refresh, sessionData, setSessionData) {
12
+ const { data } = await api.put(API.ADJUST_QUANTITY(sessionId), {
12
13
  itemId,
13
14
  quantity: qty,
14
15
  currency_id: currencyId
15
16
  });
16
- await recalculatePromotionIfNeeded(session, sessionId, currencyId);
17
- await refresh(true);
17
+ let finalSession = data;
18
+ if (data.discounts?.length || hasAppliedDiscounts(session)) {
19
+ const recalculated = await recalculatePromotionIfNeeded(session, sessionId, currencyId);
20
+ if (recalculated) finalSession = recalculated;
21
+ }
22
+ if (sessionData && setSessionData) {
23
+ setSessionData({ ...sessionData, checkoutSession: finalSession, quotes: void 0 });
24
+ } else {
25
+ await refresh(true);
26
+ }
18
27
  }
19
- export async function performUpsell(sessionId, fromId, toId, session, currencyId, refresh) {
28
+ export async function performUpsell(sessionId, fromId, toId, session, currencyId, refresh, sessionData, setSessionData) {
20
29
  if ((session?.line_items?.length || 0) > 1) {
21
30
  try {
22
31
  await api.delete(API.CROSS_SELL(sessionId));
23
32
  } catch {
24
33
  }
25
34
  }
26
- await api.put(API.UPSELL(sessionId), { from: fromId, to: toId });
27
- await recalculatePromotionIfNeeded(session, sessionId, currencyId);
28
- await refresh(true);
35
+ const { data } = await api.put(API.UPSELL(sessionId), { from: fromId, to: toId });
36
+ let finalSession = data;
37
+ if (data.discounts?.length || hasAppliedDiscounts(session)) {
38
+ const recalculated = await recalculatePromotionIfNeeded(session, sessionId, currencyId);
39
+ if (recalculated) finalSession = recalculated;
40
+ }
41
+ if (sessionData && setSessionData) {
42
+ setSessionData({ ...sessionData, checkoutSession: finalSession, quotes: void 0 });
43
+ } else {
44
+ await refresh(true);
45
+ }
29
46
  }
30
- export async function performDownsell(sessionId, priceId, session, currencyId, refresh) {
47
+ export async function performDownsell(sessionId, priceId, session, currencyId, refresh, sessionData, setSessionData) {
31
48
  if ((session?.line_items?.length || 0) > 1) {
32
49
  try {
33
50
  await api.delete(API.CROSS_SELL(sessionId));
34
51
  } catch {
35
52
  }
36
53
  }
37
- await api.put(API.DOWNSELL(sessionId), { from: priceId });
38
- await recalculatePromotionIfNeeded(session, sessionId, currencyId);
39
- await refresh(true);
54
+ const { data } = await api.put(API.DOWNSELL(sessionId), { from: priceId });
55
+ let finalSession = data;
56
+ if (data.discounts?.length || hasAppliedDiscounts(session)) {
57
+ const recalculated = await recalculatePromotionIfNeeded(session, sessionId, currencyId);
58
+ if (recalculated) finalSession = recalculated;
59
+ }
60
+ if (sessionData && setSessionData) {
61
+ setSessionData({ ...sessionData, checkoutSession: finalSession, quotes: void 0 });
62
+ } else {
63
+ await refresh(true);
64
+ }
40
65
  }
41
- export async function changeDonationAmount(sessionId, priceId, amount, session, currencyId, refresh) {
66
+ export async function changeDonationAmount(sessionId, priceId, amount, session, currencyId, refresh, sessionData, setSessionData) {
42
67
  const { data } = await api.put(API.CHANGE_AMOUNT(sessionId), {
43
68
  priceId,
44
69
  amount
45
70
  });
71
+ let finalSession = data;
46
72
  if (data?.discounts?.length) {
47
- await recalculatePromotionIfNeeded(session, sessionId, currencyId);
73
+ const recalculated = await recalculatePromotionIfNeeded(session, sessionId, currencyId);
74
+ if (recalculated) finalSession = recalculated;
75
+ }
76
+ if (sessionData && setSessionData) {
77
+ setSessionData({ ...sessionData, checkoutSession: finalSession, quotes: void 0 });
78
+ } else {
79
+ await refresh(true);
48
80
  }
49
- await refresh(true);
50
81
  }
51
82
  export function getCrossSellItem(items) {
52
83
  for (const item of items) {
@@ -21,33 +21,41 @@ export function getCurrencyStorageKey(did) {
21
21
  return did ? `${CURRENCY_PREFERENCE_KEY}:${did}` : CURRENCY_PREFERENCE_KEY;
22
22
  }
23
23
  export function getInitialCurrencyId(session, methods) {
24
+ const availableCurrencyIds = new Set(
25
+ methods.flatMap((m) => (m.payment_currencies || []).map((c) => c.id)).filter(Boolean)
26
+ );
27
+ const isAvailable = (currencyId) => !!currencyId && availableCurrencyIds.has(currencyId);
28
+ if (session?.discounts?.length && isAvailable(session.currency_id)) {
29
+ return session.currency_id;
30
+ }
24
31
  if (typeof window !== "undefined") {
25
32
  try {
26
33
  const params = new URLSearchParams(window.location.search);
27
34
  const urlCurrency = params.get("currencyId") || params.get("currency_id");
28
- if (urlCurrency) return urlCurrency;
35
+ if (isAvailable(urlCurrency)) return urlCurrency;
29
36
  } catch {
30
37
  }
31
38
  const user = session?.user;
32
39
  if (user && !hasDidWallet(user)) {
33
40
  const stripeMethod = methods.find((m) => m.type === "stripe");
34
41
  const stripeCurrency = stripeMethod?.payment_currencies?.[0];
35
- if (stripeCurrency) return stripeCurrency.id;
42
+ if (isAvailable(stripeCurrency?.id)) return stripeCurrency?.id;
36
43
  }
37
44
  try {
38
45
  const did = session?.user?.did;
39
46
  const stored = localStorage.getItem(getCurrencyStorageKey(did));
40
- if (stored) return stored;
47
+ if (isAvailable(stored)) return stored;
41
48
  } catch {
42
49
  }
43
50
  }
44
- return session?.currency_id || null;
51
+ if (isAvailable(session?.currency_id)) {
52
+ return session?.currency_id;
53
+ }
54
+ return methods[0]?.payment_currencies?.[0]?.id || null;
45
55
  }
46
56
  export function findMethodAndCurrency(methods, currencyId) {
47
57
  if (!currencyId) {
48
- const first2 = methods[0];
49
- const firstCurrency = first2?.payment_currencies?.[0] || null;
50
- return { method: first2 || null, currency: firstCurrency };
58
+ return { method: null, currency: null };
51
59
  }
52
60
  for (const method of methods) {
53
61
  for (const currency of method.payment_currencies || []) {
@@ -77,7 +77,6 @@ export function calculateAmounts(items, currency, session, exchangeRate, hasDyna
77
77
  exchangeRate: hasDynamicPricing ? exchangeRate : null
78
78
  });
79
79
  const subtotalBN = new BN(result.total);
80
- const subtotalFormatted = formatDynamicPrice(fromUnitToToken(subtotalBN, currency.decimal), hasDynamicPricing);
81
80
  const discountBN = calculateCouponDiscount(items, currency, session, hasDynamicPricing, exchangeRate, trialing, false);
82
81
  const discount = discountBN.gt(new BN(0)) ? formatDynamicPrice(fromUnitToToken(discountBN.toString(), currency.decimal), hasDynamicPricing) : null;
83
82
  const taxAmount = session?.total_details?.amount_tax;
@@ -125,7 +124,7 @@ export function calculateAmounts(items, currency, session, exchangeRate, hasDyna
125
124
  const stakingFormatted = stakingBN.gt(new BN(0)) ? `${formatDynamicPrice(fromUnitToToken(stakingBN.toString(), currency.decimal), hasDynamicPricing)} ${currency.symbol}` : null;
126
125
  return {
127
126
  subtotal: `${displaySubtotalFormatted} ${currency.symbol}`,
128
- paymentAmount: `${subtotalFormatted} ${currency.symbol}`,
127
+ paymentAmount: `${totalFormatted} ${currency.symbol}`,
129
128
  total: `${totalFormatted} ${currency.symbol}`,
130
129
  discount: discount ? `${discount} ${currency.symbol}` : null,
131
130
  tax,
@@ -5,6 +5,6 @@ export declare function applyPromotionCode(sessionId: string, code: string, curr
5
5
  error?: string;
6
6
  }>;
7
7
  export declare function removePromotionCode(sessionId: string): Promise<void>;
8
- export declare function recalculatePromotion(sessionId: string, currencyId: string | null | undefined): Promise<void>;
8
+ export declare function recalculatePromotion(sessionId: string, currencyId: string | null | undefined): Promise<TCheckoutSessionExpanded>;
9
9
  export declare function isPromotionActive(session: TCheckoutSessionExpanded | undefined | null): boolean;
10
10
  export declare function hasAppliedDiscounts(session: TCheckoutSessionExpanded | undefined | null): boolean;
@@ -26,9 +26,10 @@ export async function removePromotionCode(sessionId) {
26
26
  await api.delete(API.REMOVE_PROMOTION(sessionId));
27
27
  }
28
28
  export async function recalculatePromotion(sessionId, currencyId) {
29
- await api.post(API.RECALCULATE_PROMOTION_SESSION(sessionId), {
29
+ const { data } = await api.post(API.RECALCULATE_PROMOTION_SESSION(sessionId), {
30
30
  currency_id: currencyId
31
31
  });
32
+ return data;
32
33
  }
33
34
  export function isPromotionActive(session) {
34
35
  return session?.allow_promotion_codes !== false;
@@ -6,20 +6,21 @@ import { usePaymentMethodContext } from "../context/PaymentMethodContext.js";
6
6
  import { parseBillingInterval } from "../core/billingInterval.js";
7
7
  import { performUpsell, performDownsell } from "../core/lineItems.js";
8
8
  export function useBillingInterval() {
9
- const { items, session, effectiveSessionId, refresh } = useSessionContext();
9
+ const { items, session, effectiveSessionId, refresh, sessionData, setSessionData } = useSessionContext();
10
10
  const { currency } = usePaymentMethodContext();
11
11
  const currencyId = currency?.id || null;
12
12
  const [switching, setSwitching] = useState(false);
13
+ const [pendingInterval, setPendingInterval] = useState(null);
13
14
  const upsell = useMemoizedFn(async (fromId, toId) => {
14
15
  try {
15
- await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh);
16
+ await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh, sessionData, setSessionData);
16
17
  } catch (err) {
17
18
  console.error("Failed to upsell:", getErrorMessage(err));
18
19
  }
19
20
  });
20
21
  const downsell = useMemoizedFn(async (priceId) => {
21
22
  try {
22
- await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh);
23
+ await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh, sessionData, setSessionData);
23
24
  } catch (err) {
24
25
  console.error("Failed to downsell:", getErrorMessage(err));
25
26
  }
@@ -28,12 +29,13 @@ export function useBillingInterval() {
28
29
  const parsed = parseBillingInterval(items);
29
30
  if (!parsed) return null;
30
31
  return {
31
- current: parsed.current,
32
+ current: pendingInterval || parsed.current,
32
33
  available: parsed.available,
33
34
  switching,
34
35
  switch: async (interval) => {
35
36
  const target = parsed.available.find((a) => a.interval === interval);
36
37
  if (!target || switching) return;
38
+ setPendingInterval(interval);
37
39
  setSwitching(true);
38
40
  try {
39
41
  if (!parsed.firstItem.upsell_price_id && target.priceId) {
@@ -43,8 +45,9 @@ export function useBillingInterval() {
43
45
  }
44
46
  } finally {
45
47
  setSwitching(false);
48
+ setPendingInterval(null);
46
49
  }
47
50
  }
48
51
  };
49
- }, [items, effectiveSessionId, switching]);
52
+ }, [items, effectiveSessionId, switching, pendingInterval]);
50
53
  }
@@ -22,6 +22,7 @@ export function useCheckout(sessionId) {
22
22
  error,
23
23
  errorCode,
24
24
  refresh,
25
+ setSessionData,
25
26
  sessionData,
26
27
  resolvedSessionId,
27
28
  vendorCount,
@@ -31,14 +32,15 @@ export function useCheckout(sessionId) {
31
32
  } = useCheckoutSession(sessionId);
32
33
  const session = sessionData?.checkoutSession;
33
34
  const effectiveSessionId = resolvedSessionId || sessionId;
34
- const paymentMethodHook = usePaymentMethod(sessionData, effectiveSessionId, refresh);
35
+ const paymentMethodHook = usePaymentMethod(sessionData, effectiveSessionId, refresh, setSessionData);
35
36
  const pricingHook = usePricing(
36
37
  sessionData,
37
38
  effectiveSessionId,
38
39
  paymentMethodHook.currency,
39
40
  paymentMethodHook.isStripe,
40
41
  refresh,
41
- paymentMethodHook.current?.type || null
42
+ paymentMethodHook.current?.type || null,
43
+ paymentMethodHook.switching
42
44
  );
43
45
  const formHook = useCustomerForm(
44
46
  sessionData,
@@ -57,34 +59,38 @@ export function useCheckout(sessionId) {
57
59
  formHook.validate,
58
60
  refresh
59
61
  );
60
- const items = session?.line_items || [];
62
+ const items = useMemo(() => session?.line_items || [], [session?.line_items]);
61
63
  const currencyId = paymentMethodHook.currency?.id || null;
62
64
  const prevCurrencyRef = useRef(null);
63
65
  useEffect(() => {
64
66
  const currId = paymentMethodHook.currency?.id || null;
65
- if (!currId || !session) return;
67
+ if (!currId || !session || session.status === "complete") return;
66
68
  if (prevCurrencyRef.current === null || currId !== prevCurrencyRef.current) {
67
69
  prevCurrencyRef.current = currId;
68
- recalculatePromotionIfNeeded(session, effectiveSessionId, currId).then(() => refresh(true));
70
+ recalculatePromotionIfNeeded(session, effectiveSessionId, currId).then((recalculated) => {
71
+ if (recalculated) {
72
+ refresh(true);
73
+ }
74
+ });
69
75
  }
70
76
  }, [paymentMethodHook.currency?.id, session?.id]);
71
77
  const updateQuantity = useMemoizedFn(async (itemId, qty) => {
72
78
  try {
73
- await adjustQuantity(effectiveSessionId, itemId, qty, currencyId, session, refresh);
79
+ await adjustQuantity(effectiveSessionId, itemId, qty, currencyId, session, refresh, sessionData, setSessionData);
74
80
  } catch (err) {
75
81
  console.error("Failed to update quantity:", getErrorMessage(err));
76
82
  }
77
83
  });
78
84
  const upsell = useMemoizedFn(async (fromId, toId) => {
79
85
  try {
80
- await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh);
86
+ await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh, sessionData, setSessionData);
81
87
  } catch (err) {
82
88
  console.error("Failed to upsell:", getErrorMessage(err));
83
89
  }
84
90
  });
85
91
  const downsell = useMemoizedFn(async (priceId) => {
86
92
  try {
87
- await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh);
93
+ await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh, sessionData, setSessionData);
88
94
  } catch (err) {
89
95
  console.error("Failed to downsell:", getErrorMessage(err));
90
96
  }
@@ -109,14 +115,14 @@ export function useCheckout(sessionId) {
109
115
  const addCrossSell = useMemoizedFn(async () => {
110
116
  if (!crossSellItem) return;
111
117
  try {
112
- await addCrossSellItem(effectiveSessionId, crossSellItem.id, session, currencyId, refresh);
118
+ await addCrossSellItem(effectiveSessionId, crossSellItem.id, session, currencyId, refresh, sessionData, setSessionData);
113
119
  } catch (err) {
114
120
  console.error("Failed to add cross-sell:", getErrorMessage(err));
115
121
  }
116
122
  });
117
123
  const removeCrossSell = useMemoizedFn(async () => {
118
124
  try {
119
- await removeCrossSellItem(effectiveSessionId, session, currencyId, refresh);
125
+ await removeCrossSellItem(effectiveSessionId, session, currencyId, refresh, sessionData, setSessionData);
120
126
  } catch (err) {
121
127
  console.error("Failed to remove cross-sell:", getErrorMessage(err));
122
128
  }
@@ -141,7 +147,7 @@ export function useCheckout(sessionId) {
141
147
  const setDonationAmount = useMemoizedFn(async (priceId, amount) => {
142
148
  if (!isDonation) return;
143
149
  try {
144
- await changeDonationAmount(effectiveSessionId, priceId, amount, session, currencyId, refresh);
150
+ await changeDonationAmount(effectiveSessionId, priceId, amount, session, currencyId, refresh, sessionData, setSessionData);
145
151
  } catch (err) {
146
152
  console.error("Failed to change amount:", getErrorMessage(err));
147
153
  }
@@ -18,8 +18,8 @@ export interface SessionData {
18
18
  export interface UseCheckoutSessionReturn {
19
19
  isLoading: boolean;
20
20
  error: string | null;
21
- /** Error code for structured error handling: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null */
22
- errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
21
+ /** Error code for structured error handling */
22
+ errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | 'STOP_ACCEPTING_ORDERS' | null;
23
23
  refresh: (forceRefresh?: boolean) => Promise<void>;
24
24
  /** Directly update session data (e.g. after completion polling returns fresh data) */
25
25
  setSessionData: (data: SessionData) => void;
@@ -54,6 +54,11 @@ export function useCheckoutSession(sessionId) {
54
54
  return;
55
55
  }
56
56
  }
57
+ if (data?.stopAcceptingOrders && cs?.status !== "complete") {
58
+ setError("STOP_ACCEPTING_ORDERS");
59
+ setIsLoading(false);
60
+ return;
61
+ }
57
62
  setSessionData(data);
58
63
  setIsLoading(false);
59
64
  } catch (err) {
@@ -87,6 +92,8 @@ export function useCheckoutSession(sessionId) {
87
92
  errorCode = "SESSION_EXPIRED";
88
93
  } else if (error === "EMPTY_LINE_ITEMS") {
89
94
  errorCode = "EMPTY_LINE_ITEMS";
95
+ } else if (error === "STOP_ACCEPTING_ORDERS") {
96
+ errorCode = "STOP_ACCEPTING_ORDERS";
90
97
  }
91
98
  const vendorCount = session?.line_items?.reduce((count, item) => {
92
99
  return count + (item?.price?.product?.vendor_config?.length || 0);
@@ -1,7 +1,7 @@
1
1
  export interface UseCheckoutStatusReturn {
2
2
  isLoading: boolean;
3
3
  error: string | null;
4
- errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
4
+ errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | 'STOP_ACCEPTING_ORDERS' | null;
5
5
  canSubmit: boolean;
6
6
  isCompleted: boolean;
7
7
  isDonation: boolean;
@@ -6,7 +6,7 @@ import { usePaymentMethodContext } from "../context/PaymentMethodContext.js";
6
6
  import { getCrossSellItem } from "../core/lineItems.js";
7
7
  import { addCrossSellItem, removeCrossSellItem, fetchCrossSellItem } from "../core/crossSell.js";
8
8
  export function useCrossSell() {
9
- const { items, session, effectiveSessionId, refresh } = useSessionContext();
9
+ const { items, session, effectiveSessionId, refresh, sessionData, setSessionData } = useSessionContext();
10
10
  const { currency } = usePaymentMethodContext();
11
11
  const currencyId = currency?.id || null;
12
12
  const embeddedItem = useMemo(() => getCrossSellItem(items), [items]);
@@ -40,7 +40,15 @@ export function useCrossSell() {
40
40
  const crossSellItemPrice = getCrossSellItem(items);
41
41
  if (!crossSellItemPrice) return;
42
42
  try {
43
- await addCrossSellItem(effectiveSessionId, crossSellItemPrice.id, session, currencyId, refresh);
43
+ await addCrossSellItem(
44
+ effectiveSessionId,
45
+ crossSellItemPrice.id,
46
+ session,
47
+ currencyId,
48
+ refresh,
49
+ sessionData,
50
+ setSessionData
51
+ );
44
52
  } catch (err) {
45
53
  console.error("Failed to add cross-sell:", getErrorMessage(err));
46
54
  }
@@ -48,7 +56,7 @@ export function useCrossSell() {
48
56
  const remove = useMemoizedFn(async () => {
49
57
  if (session?.status === "complete") return;
50
58
  try {
51
- await removeCrossSellItem(effectiveSessionId, session, currencyId, refresh);
59
+ await removeCrossSellItem(effectiveSessionId, session, currencyId, refresh, sessionData, setSessionData);
52
60
  } catch (err) {
53
61
  console.error("Failed to remove cross-sell:", getErrorMessage(err));
54
62
  }
@@ -7,7 +7,11 @@ export interface UseCustomerFormReturn {
7
7
  errors: Partial<Record<string, string>>;
8
8
  touched: Record<string, boolean>;
9
9
  validate: () => Promise<boolean>;
10
+ /** Silent validation — returns valid/invalid without setting error messages on the form */
11
+ checkValid: () => Promise<boolean>;
10
12
  validateField: (field: string) => Promise<void>;
13
+ /** Whether customer info has been prefetched from backend */
14
+ prefetched: boolean;
11
15
  /** Re-fetch customer info from backend and update form values (e.g. after login) */
12
16
  refetchCustomer: () => Promise<void>;
13
17
  }
@@ -67,6 +67,10 @@ export function useCustomerForm(sessionData, currencyId, methodId) {
67
67
  setErrors(result.errors);
68
68
  return result.valid;
69
69
  });
70
+ const checkValid = useMemoizedFn(async () => {
71
+ const result = await validateForm(values, getValidateOptions());
72
+ return result.valid;
73
+ });
70
74
  const validateField = useMemoizedFn(async (field) => {
71
75
  const result = await validateForm(values, getValidateOptions());
72
76
  setErrors((prev) => {
@@ -110,7 +114,9 @@ export function useCustomerForm(sessionData, currencyId, methodId) {
110
114
  errors,
111
115
  touched,
112
116
  validate,
117
+ checkValid,
113
118
  validateField,
119
+ prefetched,
114
120
  refetchCustomer
115
121
  };
116
122
  }