@blocklet/payment-react-headless 1.26.0 → 1.26.2

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 (42) hide show
  1. package/es/checkout/context/CheckoutProvider.js +16 -4
  2. package/es/checkout/context/SessionContext.d.ts +2 -0
  3. package/es/checkout/core/crossSell.d.ts +3 -2
  4. package/es/checkout/core/crossSell.js +24 -8
  5. package/es/checkout/core/lineItems.d.ts +6 -5
  6. package/es/checkout/core/lineItems.js +48 -17
  7. package/es/checkout/core/promotion.d.ts +1 -1
  8. package/es/checkout/core/promotion.js +2 -1
  9. package/es/checkout/hooks/useBillingInterval.js +8 -5
  10. package/es/checkout/hooks/useCheckout.js +14 -9
  11. package/es/checkout/hooks/useCrossSell.js +11 -3
  12. package/es/checkout/hooks/useLineItems.js +36 -9
  13. package/es/checkout/hooks/usePaymentMethod.d.ts +1 -1
  14. package/es/checkout/hooks/usePaymentMethod.js +14 -4
  15. package/es/checkout/hooks/useUpsell.js +3 -3
  16. package/lib/checkout/context/CheckoutProvider.js +13 -4
  17. package/lib/checkout/context/SessionContext.d.ts +2 -0
  18. package/lib/checkout/core/crossSell.d.ts +3 -2
  19. package/lib/checkout/core/crossSell.js +36 -8
  20. package/lib/checkout/core/lineItems.d.ts +6 -5
  21. package/lib/checkout/core/lineItems.js +72 -18
  22. package/lib/checkout/core/promotion.d.ts +1 -1
  23. package/lib/checkout/core/promotion.js +4 -1
  24. package/lib/checkout/hooks/useBillingInterval.js +10 -5
  25. package/lib/checkout/hooks/useCheckout.js +18 -9
  26. package/lib/checkout/hooks/useCrossSell.js +5 -3
  27. package/lib/checkout/hooks/useLineItems.js +10 -8
  28. package/lib/checkout/hooks/usePaymentMethod.d.ts +1 -1
  29. package/lib/checkout/hooks/usePaymentMethod.js +20 -4
  30. package/lib/checkout/hooks/useUpsell.js +5 -3
  31. package/package.json +3 -3
  32. package/src/checkout/context/CheckoutProvider.tsx +18 -5
  33. package/src/checkout/context/SessionContext.ts +2 -0
  34. package/src/checkout/core/crossSell.ts +29 -8
  35. package/src/checkout/core/lineItems.ts +62 -18
  36. package/src/checkout/core/promotion.ts +6 -2
  37. package/src/checkout/hooks/useBillingInterval.ts +8 -5
  38. package/src/checkout/hooks/useCheckout.ts +15 -10
  39. package/src/checkout/hooks/useCrossSell.ts +11 -3
  40. package/src/checkout/hooks/useLineItems.ts +42 -9
  41. package/src/checkout/hooks/usePaymentMethod.ts +17 -5
  42. package/src/checkout/hooks/useUpsell.ts +3 -3
@@ -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 && sessionData) {
78
+ setSessionData({ ...sessionData, checkoutSession: recalculated, quotes: void 0 });
79
+ }
80
+ });
75
81
  }
76
82
  }, [paymentMethodHook.currency?.id, session?.id]);
77
83
  const paymentMethodValue = useMemo(
@@ -175,7 +181,13 @@ export function CheckoutProvider({ sessionId, children }) {
175
181
  if (intervalRef.current) clearInterval(intervalRef.current);
176
182
  document.removeEventListener("visibilitychange", handleVisibility);
177
183
  };
178
- }, [hasDynamicPricing, paymentMethodHook.isStripe, effectiveSessionId, paymentMethodHook.currency?.id, session?.status]);
184
+ }, [
185
+ hasDynamicPricing,
186
+ paymentMethodHook.isStripe,
187
+ effectiveSessionId,
188
+ paymentMethodHook.currency?.id,
189
+ session?.status
190
+ ]);
179
191
  const exchangeRateValue = useMemo(
180
192
  () => ({
181
193
  rate: exchangeRate,
@@ -2,6 +2,8 @@ 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;
@@ -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) {
@@ -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) {
@@ -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,7 +32,7 @@ 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,
@@ -57,7 +58,7 @@ export function useCheckout(sessionId) {
57
58
  formHook.validate,
58
59
  refresh
59
60
  );
60
- const items = session?.line_items || [];
61
+ const items = useMemo(() => session?.line_items || [], [session?.line_items]);
61
62
  const currencyId = paymentMethodHook.currency?.id || null;
62
63
  const prevCurrencyRef = useRef(null);
63
64
  useEffect(() => {
@@ -65,26 +66,30 @@ export function useCheckout(sessionId) {
65
66
  if (!currId || !session) return;
66
67
  if (prevCurrencyRef.current === null || currId !== prevCurrencyRef.current) {
67
68
  prevCurrencyRef.current = currId;
68
- recalculatePromotionIfNeeded(session, effectiveSessionId, currId).then(() => refresh(true));
69
+ recalculatePromotionIfNeeded(session, effectiveSessionId, currId).then((recalculated) => {
70
+ if (recalculated && sessionData) {
71
+ setSessionData({ ...sessionData, checkoutSession: recalculated, quotes: void 0 });
72
+ }
73
+ });
69
74
  }
70
75
  }, [paymentMethodHook.currency?.id, session?.id]);
71
76
  const updateQuantity = useMemoizedFn(async (itemId, qty) => {
72
77
  try {
73
- await adjustQuantity(effectiveSessionId, itemId, qty, currencyId, session, refresh);
78
+ await adjustQuantity(effectiveSessionId, itemId, qty, currencyId, session, refresh, sessionData, setSessionData);
74
79
  } catch (err) {
75
80
  console.error("Failed to update quantity:", getErrorMessage(err));
76
81
  }
77
82
  });
78
83
  const upsell = useMemoizedFn(async (fromId, toId) => {
79
84
  try {
80
- await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh);
85
+ await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh, sessionData, setSessionData);
81
86
  } catch (err) {
82
87
  console.error("Failed to upsell:", getErrorMessage(err));
83
88
  }
84
89
  });
85
90
  const downsell = useMemoizedFn(async (priceId) => {
86
91
  try {
87
- await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh);
92
+ await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh, sessionData, setSessionData);
88
93
  } catch (err) {
89
94
  console.error("Failed to downsell:", getErrorMessage(err));
90
95
  }
@@ -109,14 +114,14 @@ export function useCheckout(sessionId) {
109
114
  const addCrossSell = useMemoizedFn(async () => {
110
115
  if (!crossSellItem) return;
111
116
  try {
112
- await addCrossSellItem(effectiveSessionId, crossSellItem.id, session, currencyId, refresh);
117
+ await addCrossSellItem(effectiveSessionId, crossSellItem.id, session, currencyId, refresh, sessionData, setSessionData);
113
118
  } catch (err) {
114
119
  console.error("Failed to add cross-sell:", getErrorMessage(err));
115
120
  }
116
121
  });
117
122
  const removeCrossSell = useMemoizedFn(async () => {
118
123
  try {
119
- await removeCrossSellItem(effectiveSessionId, session, currencyId, refresh);
124
+ await removeCrossSellItem(effectiveSessionId, session, currencyId, refresh, sessionData, setSessionData);
120
125
  } catch (err) {
121
126
  console.error("Failed to remove cross-sell:", getErrorMessage(err));
122
127
  }
@@ -141,7 +146,7 @@ export function useCheckout(sessionId) {
141
146
  const setDonationAmount = useMemoizedFn(async (priceId, amount) => {
142
147
  if (!isDonation) return;
143
148
  try {
144
- await changeDonationAmount(effectiveSessionId, priceId, amount, session, currencyId, refresh);
149
+ await changeDonationAmount(effectiveSessionId, priceId, amount, session, currencyId, refresh, sessionData, setSessionData);
145
150
  } catch (err) {
146
151
  console.error("Failed to change amount:", getErrorMessage(err));
147
152
  }
@@ -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
  }
@@ -12,12 +12,13 @@ import {
12
12
  } from "../core/lineItems.js";
13
13
  import { addCrossSellItem, removeCrossSellItem, fetchCrossSellItem } from "../core/crossSell.js";
14
14
  export function useLineItems() {
15
- const { items, session, effectiveSessionId, isDonation, refresh } = useSessionContext();
15
+ const { items, session, effectiveSessionId, isDonation, refresh, sessionData, setSessionData } = useSessionContext();
16
16
  const { currency } = usePaymentMethodContext();
17
17
  const currencyId = currency?.id || null;
18
18
  const defaultQtyApplied = useRef(false);
19
19
  useEffect(() => {
20
- if (defaultQtyApplied.current || !effectiveSessionId || !items.length || !currencyId || session?.status === "complete") return;
20
+ if (defaultQtyApplied.current || !effectiveSessionId || !items.length || !currencyId || session?.status === "complete")
21
+ return;
21
22
  try {
22
23
  const params = new URLSearchParams(window.location.search);
23
24
  for (const item of items) {
@@ -26,7 +27,16 @@ export function useLineItems() {
26
27
  const qty = Math.max(1, parseInt(qtyStr, 10));
27
28
  if (Number.isFinite(qty) && qty !== item.quantity) {
28
29
  defaultQtyApplied.current = true;
29
- adjustQuantity(effectiveSessionId, item.price_id, qty, currencyId, session, refresh);
30
+ adjustQuantity(
31
+ effectiveSessionId,
32
+ item.price_id,
33
+ qty,
34
+ currencyId,
35
+ session,
36
+ refresh,
37
+ sessionData,
38
+ setSessionData
39
+ );
30
40
  break;
31
41
  }
32
42
  }
@@ -37,18 +47,18 @@ export function useLineItems() {
37
47
  const updateQuantity = useMemoizedFn(async (itemId, qty) => {
38
48
  if (session?.status === "complete") return;
39
49
  try {
40
- await adjustQuantity(effectiveSessionId, itemId, qty, currencyId, session, refresh);
50
+ await adjustQuantity(effectiveSessionId, itemId, qty, currencyId, session, refresh, sessionData, setSessionData);
41
51
  } catch (err) {
42
52
  console.error("Failed to update quantity:", getErrorMessage(err));
43
53
  }
44
54
  });
45
55
  const upsell = useMemoizedFn(async (fromId, toId) => {
46
56
  if (session?.status === "complete") return;
47
- await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh);
57
+ await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh, sessionData, setSessionData);
48
58
  });
49
59
  const downsell = useMemoizedFn(async (priceId) => {
50
60
  if (session?.status === "complete") return;
51
- await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh);
61
+ await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh, sessionData, setSessionData);
52
62
  });
53
63
  const embeddedCrossSellItem = useMemo(() => getCrossSellItem(items), [items]);
54
64
  const [fetchedCrossSellItem, setFetchedCrossSellItem] = useState(null);
@@ -80,7 +90,15 @@ export function useLineItems() {
80
90
  if (session?.status === "complete") return;
81
91
  if (!crossSellItem) return;
82
92
  try {
83
- await addCrossSellItem(effectiveSessionId, crossSellItem.id, session, currencyId, refresh);
93
+ await addCrossSellItem(
94
+ effectiveSessionId,
95
+ crossSellItem.id,
96
+ session,
97
+ currencyId,
98
+ refresh,
99
+ sessionData,
100
+ setSessionData
101
+ );
84
102
  } catch (err) {
85
103
  console.error("Failed to add cross-sell:", getErrorMessage(err));
86
104
  }
@@ -88,7 +106,7 @@ export function useLineItems() {
88
106
  const removeCrossSell = useMemoizedFn(async () => {
89
107
  if (session?.status === "complete") return;
90
108
  try {
91
- await removeCrossSellItem(effectiveSessionId, session, currencyId, refresh);
109
+ await removeCrossSellItem(effectiveSessionId, session, currencyId, refresh, sessionData, setSessionData);
92
110
  } catch (err) {
93
111
  console.error("Failed to remove cross-sell:", getErrorMessage(err));
94
112
  }
@@ -97,7 +115,16 @@ export function useLineItems() {
97
115
  if (session?.status === "complete") return;
98
116
  if (!isDonation) return;
99
117
  try {
100
- await changeDonationAmount(effectiveSessionId, priceId, amount, session, currencyId, refresh);
118
+ await changeDonationAmount(
119
+ effectiveSessionId,
120
+ priceId,
121
+ amount,
122
+ session,
123
+ currencyId,
124
+ refresh,
125
+ sessionData,
126
+ setSessionData
127
+ );
101
128
  } catch (err) {
102
129
  console.error("Failed to change amount:", getErrorMessage(err));
103
130
  }
@@ -23,4 +23,4 @@ export interface UsePaymentMethodReturn {
23
23
  status: 'idle' | 'ready' | 'processing' | 'succeeded' | 'failed';
24
24
  } | null;
25
25
  }
26
- export declare function usePaymentMethod(sessionData: SessionData | null, sessionId: string, refreshSession: (force?: boolean) => Promise<void>): UsePaymentMethodReturn;
26
+ export declare function usePaymentMethod(sessionData: SessionData | null, sessionId: string, refreshSession: (force?: boolean) => Promise<void>, setSessionData?: (data: SessionData) => void): UsePaymentMethodReturn;
@@ -9,8 +9,9 @@ import {
9
9
  findMethodAndCurrency,
10
10
  buildPaymentTypes
11
11
  } from "../core/paymentMethod.js";
12
- export function usePaymentMethod(sessionData, sessionId, refreshSession) {
13
- const methods = sessionData?.paymentMethods || [];
12
+ import { recalculatePromotionIfNeeded } from "../core/lineItems.js";
13
+ export function usePaymentMethod(sessionData, sessionId, refreshSession, setSessionData) {
14
+ const methods = useMemo(() => sessionData?.paymentMethods || [], [sessionData?.paymentMethods]);
14
15
  const session = sessionData?.checkoutSession;
15
16
  const [currencyId, setCurrencyId] = useState(() => getInitialCurrencyId(session, methods));
16
17
  const [switching, setSwitching] = useState(false);
@@ -43,7 +44,7 @@ export function usePaymentMethod(sessionData, sessionId, refreshSession) {
43
44
  if (!method || !sessionId) return;
44
45
  setSwitching(true);
45
46
  try {
46
- await api.put(API.SWITCH_CURRENCY(sessionId), {
47
+ const { data } = await api.put(API.SWITCH_CURRENCY(sessionId), {
47
48
  currency_id: newCurrencyId,
48
49
  payment_method_id: method.id
49
50
  });
@@ -55,7 +56,16 @@ export function usePaymentMethod(sessionData, sessionId, refreshSession) {
55
56
  );
56
57
  } catch {
57
58
  }
58
- await refreshSession(true);
59
+ let finalSession = data;
60
+ if (data.discounts?.length) {
61
+ const recalculated = await recalculatePromotionIfNeeded(session, sessionId, newCurrencyId);
62
+ if (recalculated) finalSession = recalculated;
63
+ }
64
+ if (sessionData && setSessionData) {
65
+ setSessionData({ ...sessionData, checkoutSession: finalSession, quotes: void 0 });
66
+ } else {
67
+ await refreshSession(true);
68
+ }
59
69
  } catch (err) {
60
70
  console.error("Failed to switch currency:", getErrorMessage(err));
61
71
  if (session?.currency_id) {
@@ -4,19 +4,19 @@ import { useSessionContext } from "../context/SessionContext.js";
4
4
  import { usePaymentMethodContext } from "../context/PaymentMethodContext.js";
5
5
  import { performUpsell, performDownsell } from "../core/lineItems.js";
6
6
  export function useUpsell() {
7
- const { session, effectiveSessionId, refresh } = useSessionContext();
7
+ const { session, effectiveSessionId, refresh, sessionData, setSessionData } = useSessionContext();
8
8
  const { currency } = usePaymentMethodContext();
9
9
  const currencyId = currency?.id || null;
10
10
  const upsell = useMemoizedFn(async (fromId, toId) => {
11
11
  try {
12
- await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh);
12
+ await performUpsell(effectiveSessionId, fromId, toId, session, currencyId, refresh, sessionData, setSessionData);
13
13
  } catch (err) {
14
14
  console.error("Failed to upsell:", getErrorMessage(err));
15
15
  }
16
16
  });
17
17
  const downsell = useMemoizedFn(async (priceId) => {
18
18
  try {
19
- await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh);
19
+ await performDownsell(effectiveSessionId, priceId, session, currencyId, refresh, sessionData, setSessionData);
20
20
  } catch (err) {
21
21
  console.error("Failed to downsell:", getErrorMessage(err));
22
22
  }
@@ -37,10 +37,11 @@ function CheckoutProvider({
37
37
  } = (0, _useCheckoutSession.useCheckoutSession)(sessionId);
38
38
  const session = sessionData?.checkoutSession;
39
39
  const effectiveSessionId = resolvedSessionId || sessionId;
40
- const items = session?.line_items || [];
40
+ const items = (0, _react.useMemo)(() => session?.line_items || [], [session?.line_items]);
41
41
  const isDonation = session?.submit_type === "donate";
42
42
  const sessionValue = (0, _react.useMemo)(() => ({
43
43
  sessionData,
44
+ setSessionData,
44
45
  sessionId,
45
46
  effectiveSessionId,
46
47
  isLoading,
@@ -54,15 +55,23 @@ function CheckoutProvider({
54
55
  product,
55
56
  subscription,
56
57
  pageInfo
57
- }), [sessionData, sessionId, effectiveSessionId, isLoading, error, errorCode, refresh, items, session, isDonation, vendorCount, product, subscription, pageInfo]);
58
- const paymentMethodHook = (0, _usePaymentMethod.usePaymentMethod)(sessionData, effectiveSessionId, refresh);
58
+ }), [sessionData, setSessionData, sessionId, effectiveSessionId, isLoading, error, errorCode, refresh, items, session, isDonation, vendorCount, product, subscription, pageInfo]);
59
+ const paymentMethodHook = (0, _usePaymentMethod.usePaymentMethod)(sessionData, effectiveSessionId, refresh, setSessionData);
59
60
  const prevCurrencyRef = (0, _react.useRef)(null);
60
61
  (0, _react.useEffect)(() => {
61
62
  const currId = paymentMethodHook.currency?.id || null;
62
63
  if (!currId || !session || session.status === "complete") return;
63
64
  if (prevCurrencyRef.current === null || currId !== prevCurrencyRef.current) {
64
65
  prevCurrencyRef.current = currId;
65
- (0, _lineItems.recalculatePromotionIfNeeded)(session, effectiveSessionId, currId).then(() => refresh(true));
66
+ (0, _lineItems.recalculatePromotionIfNeeded)(session, effectiveSessionId, currId).then(recalculated => {
67
+ if (recalculated && sessionData) {
68
+ setSessionData({
69
+ ...sessionData,
70
+ checkoutSession: recalculated,
71
+ quotes: void 0
72
+ });
73
+ }
74
+ });
66
75
  }
67
76
  }, [paymentMethodHook.currency?.id, session?.id]);
68
77
  const paymentMethodValue = (0, _react.useMemo)(() => ({
@@ -2,6 +2,8 @@ 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;