@blocklet/payment-react-headless 1.26.2 → 1.26.4

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 (46) hide show
  1. package/es/checkout/context/CheckoutProvider.js +12 -8
  2. package/es/checkout/context/SessionContext.d.ts +1 -1
  3. package/es/checkout/core/customerForm.js +1 -7
  4. package/es/checkout/core/paymentMethod.js +15 -7
  5. package/es/checkout/core/pricing.js +1 -2
  6. package/es/checkout/hooks/useCheckout.js +5 -4
  7. package/es/checkout/hooks/useCheckoutSession.d.ts +2 -2
  8. package/es/checkout/hooks/useCheckoutSession.js +7 -0
  9. package/es/checkout/hooks/useCheckoutStatus.d.ts +1 -1
  10. package/es/checkout/hooks/useCustomerForm.d.ts +4 -0
  11. package/es/checkout/hooks/useCustomerForm.js +6 -0
  12. package/es/checkout/hooks/usePaymentMethod.js +1 -7
  13. package/es/checkout/hooks/usePricing.d.ts +1 -1
  14. package/es/checkout/hooks/usePricing.js +4 -4
  15. package/es/checkout/hooks/useSubmit.js +6 -1
  16. package/es/checkout/types.d.ts +5 -3
  17. package/lib/checkout/context/CheckoutProvider.js +14 -14
  18. package/lib/checkout/context/SessionContext.d.ts +1 -1
  19. package/lib/checkout/core/customerForm.js +1 -6
  20. package/lib/checkout/core/paymentMethod.js +14 -8
  21. package/lib/checkout/core/pricing.js +1 -2
  22. package/lib/checkout/hooks/useCheckout.js +4 -8
  23. package/lib/checkout/hooks/useCheckoutSession.d.ts +2 -2
  24. package/lib/checkout/hooks/useCheckoutSession.js +7 -0
  25. package/lib/checkout/hooks/useCheckoutStatus.d.ts +1 -1
  26. package/lib/checkout/hooks/useCustomerForm.d.ts +4 -0
  27. package/lib/checkout/hooks/useCustomerForm.js +6 -0
  28. package/lib/checkout/hooks/usePaymentMethod.js +1 -7
  29. package/lib/checkout/hooks/usePricing.d.ts +1 -1
  30. package/lib/checkout/hooks/usePricing.js +4 -4
  31. package/lib/checkout/hooks/useSubmit.js +8 -1
  32. package/lib/checkout/types.d.ts +5 -3
  33. package/package.json +3 -3
  34. package/src/checkout/context/CheckoutProvider.tsx +23 -15
  35. package/src/checkout/context/SessionContext.ts +1 -1
  36. package/src/checkout/core/customerForm.ts +1 -7
  37. package/src/checkout/core/paymentMethod.ts +24 -7
  38. package/src/checkout/core/pricing.ts +1 -2
  39. package/src/checkout/hooks/useCheckout.ts +8 -5
  40. package/src/checkout/hooks/useCheckoutSession.ts +12 -3
  41. package/src/checkout/hooks/useCheckoutStatus.ts +1 -1
  42. package/src/checkout/hooks/useCustomerForm.ts +12 -0
  43. package/src/checkout/hooks/usePaymentMethod.ts +5 -9
  44. package/src/checkout/hooks/usePricing.ts +5 -4
  45. package/src/checkout/hooks/useSubmit.ts +13 -1
  46. package/src/checkout/types.ts +4 -2
@@ -74,8 +74,8 @@ export function CheckoutProvider({ sessionId, children }) {
74
74
  if (prevCurrencyRef.current === null || currId !== prevCurrencyRef.current) {
75
75
  prevCurrencyRef.current = currId;
76
76
  recalculatePromotionIfNeeded(session, effectiveSessionId, currId).then((recalculated) => {
77
- if (recalculated && sessionData) {
78
- setSessionData({ ...sessionData, checkoutSession: recalculated, quotes: void 0 });
77
+ if (recalculated) {
78
+ refresh(true);
79
79
  }
80
80
  });
81
81
  }
@@ -95,6 +95,7 @@ export function CheckoutProvider({ sessionId, children }) {
95
95
  setCurrency: paymentMethodHook.setCurrency,
96
96
  stripe: paymentMethodHook.stripe
97
97
  }),
98
+ // eslint-disable-next-line react-hooks/exhaustive-deps
98
99
  [
99
100
  paymentMethodHook.current,
100
101
  paymentMethodHook.currency,
@@ -120,7 +121,7 @@ export function CheckoutProvider({ sessionId, children }) {
120
121
  const mountedRef = useRef(true);
121
122
  const hasDynamicPricing = useMemo(() => checkHasDynamicPricing(items), [items]);
122
123
  const fetchRate = useMemoizedFn(async () => {
123
- if (!effectiveSessionId || !hasDynamicPricing || paymentMethodHook.isStripe) {
124
+ if (!effectiveSessionId || !hasDynamicPricing || paymentMethodHook.isStripe || paymentMethodHook.switching) {
124
125
  setRateStatus(hasDynamicPricing ? "unavailable" : "available");
125
126
  if (paymentMethodHook.isStripe) {
126
127
  setExchangeRate(null);
@@ -154,17 +155,19 @@ export function CheckoutProvider({ sessionId, children }) {
154
155
  });
155
156
  useEffect(() => {
156
157
  mountedRef.current = true;
157
- if (!hasDynamicPricing || paymentMethodHook.isStripe || !effectiveSessionId || session?.status === "complete") {
158
+ if (!hasDynamicPricing || paymentMethodHook.isStripe || paymentMethodHook.switching || !effectiveSessionId || session?.status === "complete") {
158
159
  if (paymentMethodHook.isStripe) {
159
160
  setExchangeRate(null);
160
161
  setRateProvider(null);
161
162
  setRateProviderDisplay(null);
162
163
  setRateFetchedAt(null);
163
164
  }
164
- if (session?.status === "complete") {
165
- setRateStatus("available");
166
- } else {
167
- setRateStatus(hasDynamicPricing ? "unavailable" : "available");
165
+ if (!paymentMethodHook.switching) {
166
+ if (session?.status === "complete") {
167
+ setRateStatus("available");
168
+ } else {
169
+ setRateStatus(hasDynamicPricing ? "unavailable" : "available");
170
+ }
168
171
  }
169
172
  return void 0;
170
173
  }
@@ -184,6 +187,7 @@ export function CheckoutProvider({ sessionId, children }) {
184
187
  }, [
185
188
  hasDynamicPricing,
186
189
  paymentMethodHook.isStripe,
190
+ paymentMethodHook.switching,
187
191
  effectiveSessionId,
188
192
  paymentMethodHook.currency?.id,
189
193
  session?.status
@@ -8,7 +8,7 @@ export interface SessionContextValue {
8
8
  effectiveSessionId: string;
9
9
  isLoading: boolean;
10
10
  error: string | null;
11
- errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
11
+ errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | 'STOP_ACCEPTING_ORDERS' | null;
12
12
  refresh: (forceRefresh?: boolean) => Promise<void>;
13
13
  items: TLineItemExpanded[];
14
14
  session: TCheckoutSessionExpanded | null | undefined;
@@ -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",
@@ -83,7 +77,7 @@ export function createInitialValues(session, customer) {
83
77
  payment_method: "",
84
78
  payment_currency: session?.currency_id || "",
85
79
  billing_address: {
86
- country: customer?.address?.country || "",
80
+ country: customer?.address?.country || "us",
87
81
  state: customer?.address?.state || "",
88
82
  city: customer?.address?.city || "",
89
83
  line1: customer?.address?.line1 || "",
@@ -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,
@@ -39,7 +39,8 @@ export function useCheckout(sessionId) {
39
39
  paymentMethodHook.currency,
40
40
  paymentMethodHook.isStripe,
41
41
  refresh,
42
- paymentMethodHook.current?.type || null
42
+ paymentMethodHook.current?.type || null,
43
+ paymentMethodHook.switching
43
44
  );
44
45
  const formHook = useCustomerForm(
45
46
  sessionData,
@@ -63,12 +64,12 @@ export function useCheckout(sessionId) {
63
64
  const prevCurrencyRef = useRef(null);
64
65
  useEffect(() => {
65
66
  const currId = paymentMethodHook.currency?.id || null;
66
- if (!currId || !session) return;
67
+ if (!currId || !session || session.status === "complete") return;
67
68
  if (prevCurrencyRef.current === null || currId !== prevCurrencyRef.current) {
68
69
  prevCurrencyRef.current = currId;
69
70
  recalculatePromotionIfNeeded(session, effectiveSessionId, currId).then((recalculated) => {
70
- if (recalculated && sessionData) {
71
- setSessionData({ ...sessionData, checkoutSession: recalculated, quotes: void 0 });
71
+ if (recalculated) {
72
+ refresh(true);
72
73
  }
73
74
  });
74
75
  }
@@ -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;
@@ -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
  }
@@ -9,7 +9,6 @@ import {
9
9
  findMethodAndCurrency,
10
10
  buildPaymentTypes
11
11
  } from "../core/paymentMethod.js";
12
- import { recalculatePromotionIfNeeded } from "../core/lineItems.js";
13
12
  export function usePaymentMethod(sessionData, sessionId, refreshSession, setSessionData) {
14
13
  const methods = useMemo(() => sessionData?.paymentMethods || [], [sessionData?.paymentMethods]);
15
14
  const session = sessionData?.checkoutSession;
@@ -56,13 +55,8 @@ export function usePaymentMethod(sessionData, sessionId, refreshSession, setSess
56
55
  );
57
56
  } catch {
58
57
  }
59
- let finalSession = data;
60
- if (data.discounts?.length) {
61
- const recalculated = await recalculatePromotionIfNeeded(session, sessionId, newCurrencyId);
62
- if (recalculated) finalSession = recalculated;
63
- }
64
58
  if (sessionData && setSessionData) {
65
- setSessionData({ ...sessionData, checkoutSession: finalSession, quotes: void 0 });
59
+ setSessionData({ ...sessionData, checkoutSession: data, quotes: void 0 });
66
60
  } else {
67
61
  await refreshSession(true);
68
62
  }
@@ -54,4 +54,4 @@ export interface UsePricingReturn {
54
54
  afterTrialInterval: string | null;
55
55
  };
56
56
  }
57
- export declare function usePricing(sessionData: SessionData | null, sessionId: string, currency: TPaymentCurrency | null, isStripe: boolean, refreshSession: (force?: boolean) => Promise<void>, paymentMethodType?: string | null): UsePricingReturn;
57
+ export declare function usePricing(sessionData: SessionData | null, sessionId: string, currency: TPaymentCurrency | null, isStripe: boolean, refreshSession: (force?: boolean) => Promise<void>, paymentMethodType?: string | null, switching?: boolean): UsePricingReturn;
@@ -11,7 +11,7 @@ import {
11
11
  isPromotionActive
12
12
  } from "../core/promotion.js";
13
13
  import { updateSlippage } from "../core/submit.js";
14
- export function usePricing(sessionData, sessionId, currency, isStripe, refreshSession, paymentMethodType) {
14
+ export function usePricing(sessionData, sessionId, currency, isStripe, refreshSession, paymentMethodType, switching) {
15
15
  const session = sessionData?.checkoutSession;
16
16
  const items = session?.line_items || [];
17
17
  const [exchangeRate, setExchangeRate] = useState(null);
@@ -30,7 +30,7 @@ export function usePricing(sessionData, sessionId, currency, isStripe, refreshSe
30
30
  const mountedRef = useRef(true);
31
31
  const hasDynamicPricing = useMemo(() => checkHasDynamicPricing(items), [items]);
32
32
  const fetchRate = useMemoizedFn(async () => {
33
- if (!sessionId || !hasDynamicPricing || isStripe) {
33
+ if (!sessionId || !hasDynamicPricing || isStripe || switching) {
34
34
  setRateStatus(hasDynamicPricing ? "unavailable" : "available");
35
35
  if (isStripe) {
36
36
  setExchangeRate(null);
@@ -62,7 +62,7 @@ export function usePricing(sessionData, sessionId, currency, isStripe, refreshSe
62
62
  });
63
63
  useEffect(() => {
64
64
  mountedRef.current = true;
65
- if (!hasDynamicPricing || isStripe || !sessionId || session?.status === "complete") {
65
+ if (!hasDynamicPricing || isStripe || switching || !sessionId || session?.status === "complete") {
66
66
  if (isStripe) {
67
67
  setExchangeRate(null);
68
68
  setRateProvider(null);
@@ -95,7 +95,7 @@ export function usePricing(sessionData, sessionId, currency, isStripe, refreshSe
95
95
  if (pollingRef.current) clearTimeout(pollingRef.current);
96
96
  document.removeEventListener("visibilitychange", handleVisibility);
97
97
  };
98
- }, [hasDynamicPricing, isStripe, sessionId, currency?.id, session?.status]);
98
+ }, [hasDynamicPricing, isStripe, switching, sessionId, currency?.id, session?.status]);
99
99
  const amounts = useMemo(
100
100
  () => calculateAmounts(items, currency, session, exchangeRate, hasDynamicPricing, paymentMethodType),
101
101
  [items, currency, exchangeRate, hasDynamicPricing, session, paymentMethodType]
@@ -328,6 +328,11 @@ export function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit
328
328
  });
329
329
  return;
330
330
  }
331
+ if (errorCode === "STOP_ACCEPTING_ORDERS") {
332
+ setStatus("service_suspended");
333
+ setContext({ type: "service_suspended" });
334
+ return;
335
+ }
331
336
  if (errorCode === "UNIFIED_APP_REQUIRED" || errorCode === "CUSTOMER_LIMITED") {
332
337
  setStatus("failed");
333
338
  setContext({
@@ -443,7 +448,7 @@ export function useSubmit(sessionData, sessionId, currencyId, isStripe, isCredit
443
448
  }
444
449
  });
445
450
  const cancel = useMemoizedFn(() => {
446
- if (status === "confirming_price" || status === "confirming_fast_pay" || status === "credit_insufficient") {
451
+ if (status === "confirming_price" || status === "confirming_fast_pay" || status === "credit_insufficient" || status === "service_suspended") {
447
452
  setStatus("idle");
448
453
  setContext(null);
449
454
  unlock();
@@ -1,5 +1,5 @@
1
1
  import type { TLineItemExpanded, TPaymentMethodExpanded, TPaymentCurrency, TPrice, TCheckoutSessionExpanded, TPaymentIntent, TCustomer } from '@blocklet/payment-types';
2
- export type SubmitStatus = 'idle' | 'submitting' | 'confirming_price' | 'confirming_fast_pay' | 'credit_insufficient' | 'waiting_did' | 'waiting_stripe' | 'completed' | 'failed';
2
+ export type SubmitStatus = 'idle' | 'submitting' | 'confirming_price' | 'confirming_fast_pay' | 'credit_insufficient' | 'service_suspended' | 'waiting_did' | 'waiting_stripe' | 'completed' | 'failed';
3
3
  export type SubmitContext = {
4
4
  type: 'price_change';
5
5
  changePercent: number;
@@ -25,6 +25,8 @@ export type SubmitContext = {
25
25
  type: 'error';
26
26
  message: string;
27
27
  code?: string;
28
+ } | {
29
+ type: 'service_suspended';
28
30
  } | null;
29
31
  export interface FieldConfig {
30
32
  name: string;
@@ -69,8 +71,8 @@ export type CheckoutResult = {
69
71
  export interface UseCheckoutReturn {
70
72
  isLoading: boolean;
71
73
  error: string | null;
72
- /** Structured error code: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null */
73
- errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
74
+ /** Structured error code */
75
+ errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | 'STOP_ACCEPTING_ORDERS' | null;
74
76
  refresh: () => Promise<void>;
75
77
  vendorCount: number;
76
78
  product: {
@@ -64,12 +64,8 @@ function CheckoutProvider({
64
64
  if (prevCurrencyRef.current === null || currId !== prevCurrencyRef.current) {
65
65
  prevCurrencyRef.current = currId;
66
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
- });
67
+ if (recalculated) {
68
+ refresh(true);
73
69
  }
74
70
  });
75
71
  }
@@ -87,7 +83,9 @@ function CheckoutProvider({
87
83
  types: paymentMethodHook.types,
88
84
  setCurrency: paymentMethodHook.setCurrency,
89
85
  stripe: paymentMethodHook.stripe
90
- }), [paymentMethodHook.current, paymentMethodHook.currency, paymentMethodHook.available, paymentMethodHook.currencies, paymentMethodHook.isStripe, paymentMethodHook.isCrypto, paymentMethodHook.isCredit, paymentMethodHook.switching, paymentMethodHook.setType, paymentMethodHook.types, paymentMethodHook.setCurrency, paymentMethodHook.stripe]);
86
+ }),
87
+ // eslint-disable-next-line react-hooks/exhaustive-deps
88
+ [paymentMethodHook.current, paymentMethodHook.currency, paymentMethodHook.available, paymentMethodHook.currencies, paymentMethodHook.isStripe, paymentMethodHook.isCrypto, paymentMethodHook.isCredit, paymentMethodHook.switching, paymentMethodHook.setType, paymentMethodHook.types, paymentMethodHook.setCurrency, paymentMethodHook.stripe]);
91
89
  const [exchangeRate, setExchangeRate] = (0, _react.useState)(null);
92
90
  const [rateProvider, setRateProvider] = (0, _react.useState)(null);
93
91
  const [rateProviderDisplay, setRateProviderDisplay] = (0, _react.useState)(null);
@@ -98,7 +96,7 @@ function CheckoutProvider({
98
96
  const mountedRef = (0, _react.useRef)(true);
99
97
  const hasDynamicPricing = (0, _react.useMemo)(() => (0, _exchangeRate.checkHasDynamicPricing)(items), [items]);
100
98
  const fetchRate = (0, _ahooks.useMemoizedFn)(async () => {
101
- if (!effectiveSessionId || !hasDynamicPricing || paymentMethodHook.isStripe) {
99
+ if (!effectiveSessionId || !hasDynamicPricing || paymentMethodHook.isStripe || paymentMethodHook.switching) {
102
100
  setRateStatus(hasDynamicPricing ? "unavailable" : "available");
103
101
  if (paymentMethodHook.isStripe) {
104
102
  setExchangeRate(null);
@@ -132,17 +130,19 @@ function CheckoutProvider({
132
130
  });
133
131
  (0, _react.useEffect)(() => {
134
132
  mountedRef.current = true;
135
- if (!hasDynamicPricing || paymentMethodHook.isStripe || !effectiveSessionId || session?.status === "complete") {
133
+ if (!hasDynamicPricing || paymentMethodHook.isStripe || paymentMethodHook.switching || !effectiveSessionId || session?.status === "complete") {
136
134
  if (paymentMethodHook.isStripe) {
137
135
  setExchangeRate(null);
138
136
  setRateProvider(null);
139
137
  setRateProviderDisplay(null);
140
138
  setRateFetchedAt(null);
141
139
  }
142
- if (session?.status === "complete") {
143
- setRateStatus("available");
144
- } else {
145
- setRateStatus(hasDynamicPricing ? "unavailable" : "available");
140
+ if (!paymentMethodHook.switching) {
141
+ if (session?.status === "complete") {
142
+ setRateStatus("available");
143
+ } else {
144
+ setRateStatus(hasDynamicPricing ? "unavailable" : "available");
145
+ }
146
146
  }
147
147
  return void 0;
148
148
  }
@@ -159,7 +159,7 @@ function CheckoutProvider({
159
159
  if (intervalRef.current) clearInterval(intervalRef.current);
160
160
  document.removeEventListener("visibilitychange", handleVisibility);
161
161
  };
162
- }, [hasDynamicPricing, paymentMethodHook.isStripe, effectiveSessionId, paymentMethodHook.currency?.id, session?.status]);
162
+ }, [hasDynamicPricing, paymentMethodHook.isStripe, paymentMethodHook.switching, effectiveSessionId, paymentMethodHook.currency?.id, session?.status]);
163
163
  const exchangeRateValue = (0, _react.useMemo)(() => ({
164
164
  rate: exchangeRate,
165
165
  provider: rateProvider,
@@ -8,7 +8,7 @@ export interface SessionContextValue {
8
8
  effectiveSessionId: string;
9
9
  isLoading: boolean;
10
10
  error: string | null;
11
- errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | null;
11
+ errorCode: 'SESSION_EXPIRED' | 'EMPTY_LINE_ITEMS' | 'STOP_ACCEPTING_ORDERS' | null;
12
12
  refresh: (forceRefresh?: boolean) => Promise<void>;
13
13
  items: TLineItemExpanded[];
14
14
  session: TCheckoutSessionExpanded | null | undefined;
@@ -37,11 +37,6 @@ function buildFields(session) {
37
37
  type: "text",
38
38
  required: true,
39
39
  group: "address"
40
- }, {
41
- name: "billing_address.line2",
42
- type: "text",
43
- required: false,
44
- group: "address"
45
40
  }, {
46
41
  name: "billing_address.city",
47
42
  type: "text",
@@ -81,7 +76,7 @@ function createInitialValues(session, customer) {
81
76
  payment_method: "",
82
77
  payment_currency: session?.currency_id || "",
83
78
  billing_address: {
84
- country: customer?.address?.country || "",
79
+ country: customer?.address?.country || "us",
85
80
  state: customer?.address?.state || "",
86
81
  city: customer?.address?.city || "",
87
82
  line1: customer?.address?.line1 || "",
@@ -36,33 +36,39 @@ function getCurrencyStorageKey(did) {
36
36
  return did ? `${CURRENCY_PREFERENCE_KEY}:${did}` : CURRENCY_PREFERENCE_KEY;
37
37
  }
38
38
  function getInitialCurrencyId(session, methods) {
39
+ const availableCurrencyIds = new Set(methods.flatMap(m => (m.payment_currencies || []).map(c => c.id)).filter(Boolean));
40
+ const isAvailable = currencyId => !!currencyId && availableCurrencyIds.has(currencyId);
41
+ if (session?.discounts?.length && isAvailable(session.currency_id)) {
42
+ return session.currency_id;
43
+ }
39
44
  if (typeof window !== "undefined") {
40
45
  try {
41
46
  const params = new URLSearchParams(window.location.search);
42
47
  const urlCurrency = params.get("currencyId") || params.get("currency_id");
43
- if (urlCurrency) return urlCurrency;
48
+ if (isAvailable(urlCurrency)) return urlCurrency;
44
49
  } catch {}
45
50
  const user = session?.user;
46
51
  if (user && !hasDidWallet(user)) {
47
52
  const stripeMethod = methods.find(m => m.type === "stripe");
48
53
  const stripeCurrency = stripeMethod?.payment_currencies?.[0];
49
- if (stripeCurrency) return stripeCurrency.id;
54
+ if (isAvailable(stripeCurrency?.id)) return stripeCurrency?.id;
50
55
  }
51
56
  try {
52
57
  const did = session?.user?.did;
53
58
  const stored = localStorage.getItem(getCurrencyStorageKey(did));
54
- if (stored) return stored;
59
+ if (isAvailable(stored)) return stored;
55
60
  } catch {}
56
61
  }
57
- return session?.currency_id || null;
62
+ if (isAvailable(session?.currency_id)) {
63
+ return session?.currency_id;
64
+ }
65
+ return methods[0]?.payment_currencies?.[0]?.id || null;
58
66
  }
59
67
  function findMethodAndCurrency(methods, currencyId) {
60
68
  if (!currencyId) {
61
- const first2 = methods[0];
62
- const firstCurrency = first2?.payment_currencies?.[0] || null;
63
69
  return {
64
- method: first2 || null,
65
- currency: firstCurrency
70
+ method: null,
71
+ currency: null
66
72
  };
67
73
  }
68
74
  for (const method of methods) {
@@ -82,7 +82,6 @@ function calculateAmounts(items, currency, session, exchangeRate, hasDynamicPric
82
82
  exchangeRate: hasDynamicPricing ? exchangeRate : null
83
83
  });
84
84
  const subtotalBN = new _util.BN(result.total);
85
- const subtotalFormatted = (0, _format.formatDynamicPrice)((0, _util.fromUnitToToken)(subtotalBN, currency.decimal), hasDynamicPricing);
86
85
  const discountBN = calculateCouponDiscount(items, currency, session, hasDynamicPricing, exchangeRate, trialing, false);
87
86
  const discount = discountBN.gt(new _util.BN(0)) ? (0, _format.formatDynamicPrice)((0, _util.fromUnitToToken)(discountBN.toString(), currency.decimal), hasDynamicPricing) : null;
88
87
  const taxAmount = session?.total_details?.amount_tax;
@@ -124,7 +123,7 @@ function calculateAmounts(items, currency, session, exchangeRate, hasDynamicPric
124
123
  const stakingFormatted = stakingBN.gt(new _util.BN(0)) ? `${(0, _format.formatDynamicPrice)((0, _util.fromUnitToToken)(stakingBN.toString(), currency.decimal), hasDynamicPricing)} ${currency.symbol}` : null;
125
124
  return {
126
125
  subtotal: `${displaySubtotalFormatted} ${currency.symbol}`,
127
- paymentAmount: `${subtotalFormatted} ${currency.symbol}`,
126
+ paymentAmount: `${totalFormatted} ${currency.symbol}`,
128
127
  total: `${totalFormatted} ${currency.symbol}`,
129
128
  discount: discount ? `${discount} ${currency.symbol}` : null,
130
129
  tax,
@@ -32,7 +32,7 @@ function useCheckout(sessionId) {
32
32
  const session = sessionData?.checkoutSession;
33
33
  const effectiveSessionId = resolvedSessionId || sessionId;
34
34
  const paymentMethodHook = (0, _usePaymentMethod.usePaymentMethod)(sessionData, effectiveSessionId, refresh, setSessionData);
35
- const pricingHook = (0, _usePricing.usePricing)(sessionData, effectiveSessionId, paymentMethodHook.currency, paymentMethodHook.isStripe, refresh, paymentMethodHook.current?.type || null);
35
+ const pricingHook = (0, _usePricing.usePricing)(sessionData, effectiveSessionId, paymentMethodHook.currency, paymentMethodHook.isStripe, refresh, paymentMethodHook.current?.type || null, paymentMethodHook.switching);
36
36
  const formHook = (0, _useCustomerForm.useCustomerForm)(sessionData, paymentMethodHook.currency?.id || null, paymentMethodHook.current?.id || null);
37
37
  const isDonation = session?.submit_type === "donate";
38
38
  const submitHook = (0, _useSubmit.useSubmit)(sessionData, effectiveSessionId, paymentMethodHook.currency?.id || null, paymentMethodHook.isStripe, paymentMethodHook.isCredit, isDonation, formHook.values, formHook.validate, refresh);
@@ -41,16 +41,12 @@ function useCheckout(sessionId) {
41
41
  const prevCurrencyRef = (0, _react.useRef)(null);
42
42
  (0, _react.useEffect)(() => {
43
43
  const currId = paymentMethodHook.currency?.id || null;
44
- if (!currId || !session) return;
44
+ if (!currId || !session || session.status === "complete") return;
45
45
  if (prevCurrencyRef.current === null || currId !== prevCurrencyRef.current) {
46
46
  prevCurrencyRef.current = currId;
47
47
  (0, _lineItems.recalculatePromotionIfNeeded)(session, effectiveSessionId, currId).then(recalculated => {
48
- if (recalculated && sessionData) {
49
- setSessionData({
50
- ...sessionData,
51
- checkoutSession: recalculated,
52
- quotes: void 0
53
- });
48
+ if (recalculated) {
49
+ refresh(true);
54
50
  }
55
51
  });
56
52
  }
@@ -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;
@@ -66,6 +66,11 @@ function useCheckoutSession(sessionId) {
66
66
  return;
67
67
  }
68
68
  }
69
+ if (data?.stopAcceptingOrders && cs?.status !== "complete") {
70
+ setError("STOP_ACCEPTING_ORDERS");
71
+ setIsLoading(false);
72
+ return;
73
+ }
69
74
  setSessionData(data);
70
75
  setIsLoading(false);
71
76
  } catch (err) {
@@ -99,6 +104,8 @@ function useCheckoutSession(sessionId) {
99
104
  errorCode = "SESSION_EXPIRED";
100
105
  } else if (error === "EMPTY_LINE_ITEMS") {
101
106
  errorCode = "EMPTY_LINE_ITEMS";
107
+ } else if (error === "STOP_ACCEPTING_ORDERS") {
108
+ errorCode = "STOP_ACCEPTING_ORDERS";
102
109
  }
103
110
  const vendorCount = session?.line_items?.reduce((count, item) => {
104
111
  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;
@@ -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
  }