@blocklet/payment-react-headless 1.26.2 → 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.
- package/es/checkout/context/CheckoutProvider.js +12 -8
- package/es/checkout/context/SessionContext.d.ts +1 -1
- package/es/checkout/core/customerForm.js +0 -6
- package/es/checkout/core/paymentMethod.js +15 -7
- package/es/checkout/core/pricing.js +1 -2
- package/es/checkout/hooks/useCheckout.js +5 -4
- package/es/checkout/hooks/useCheckoutSession.d.ts +2 -2
- package/es/checkout/hooks/useCheckoutSession.js +7 -0
- package/es/checkout/hooks/useCheckoutStatus.d.ts +1 -1
- package/es/checkout/hooks/useCustomerForm.d.ts +4 -0
- package/es/checkout/hooks/useCustomerForm.js +6 -0
- package/es/checkout/hooks/usePaymentMethod.js +1 -7
- package/es/checkout/hooks/usePricing.d.ts +1 -1
- package/es/checkout/hooks/usePricing.js +4 -4
- package/es/checkout/hooks/useSubmit.js +6 -1
- package/es/checkout/types.d.ts +5 -3
- package/lib/checkout/context/CheckoutProvider.js +14 -14
- package/lib/checkout/context/SessionContext.d.ts +1 -1
- package/lib/checkout/core/customerForm.js +0 -5
- package/lib/checkout/core/paymentMethod.js +14 -8
- package/lib/checkout/core/pricing.js +1 -2
- package/lib/checkout/hooks/useCheckout.js +4 -8
- package/lib/checkout/hooks/useCheckoutSession.d.ts +2 -2
- package/lib/checkout/hooks/useCheckoutSession.js +7 -0
- package/lib/checkout/hooks/useCheckoutStatus.d.ts +1 -1
- package/lib/checkout/hooks/useCustomerForm.d.ts +4 -0
- package/lib/checkout/hooks/useCustomerForm.js +6 -0
- package/lib/checkout/hooks/usePaymentMethod.js +1 -7
- package/lib/checkout/hooks/usePricing.d.ts +1 -1
- package/lib/checkout/hooks/usePricing.js +4 -4
- package/lib/checkout/hooks/useSubmit.js +8 -1
- package/lib/checkout/types.d.ts +5 -3
- package/package.json +3 -3
- package/src/checkout/context/CheckoutProvider.tsx +23 -15
- package/src/checkout/context/SessionContext.ts +1 -1
- package/src/checkout/core/customerForm.ts +0 -6
- package/src/checkout/core/paymentMethod.ts +24 -7
- package/src/checkout/core/pricing.ts +1 -2
- package/src/checkout/hooks/useCheckout.ts +8 -5
- package/src/checkout/hooks/useCheckoutSession.ts +12 -3
- package/src/checkout/hooks/useCheckoutStatus.ts +1 -1
- package/src/checkout/hooks/useCustomerForm.ts +12 -0
- package/src/checkout/hooks/usePaymentMethod.ts +5 -9
- package/src/checkout/hooks/usePricing.ts +5 -4
- package/src/checkout/hooks/useSubmit.ts +13 -1
- 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
|
|
78
|
-
|
|
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 (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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;
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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: `${
|
|
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
|
|
71
|
-
|
|
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
|
|
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:
|
|
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();
|
package/es/checkout/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
68
|
-
|
|
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
|
-
}),
|
|
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 (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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;
|
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
65
|
-
currency:
|
|
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: `${
|
|
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
|
|
49
|
-
|
|
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
|
|
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
|
}
|
|
@@ -83,6 +83,10 @@ function useCustomerForm(sessionData, currencyId, methodId) {
|
|
|
83
83
|
setErrors(result.errors);
|
|
84
84
|
return result.valid;
|
|
85
85
|
});
|
|
86
|
+
const checkValid = (0, _ahooks.useMemoizedFn)(async () => {
|
|
87
|
+
const result = await (0, _validation.validateForm)(values, getValidateOptions());
|
|
88
|
+
return result.valid;
|
|
89
|
+
});
|
|
86
90
|
const validateField = (0, _ahooks.useMemoizedFn)(async field => {
|
|
87
91
|
const result = await (0, _validation.validateForm)(values, getValidateOptions());
|
|
88
92
|
setErrors(prev => {
|
|
@@ -129,7 +133,9 @@ function useCustomerForm(sessionData, currencyId, methodId) {
|
|
|
129
133
|
errors,
|
|
130
134
|
touched,
|
|
131
135
|
validate,
|
|
136
|
+
checkValid,
|
|
132
137
|
validateField,
|
|
138
|
+
prefetched,
|
|
133
139
|
refetchCustomer
|
|
134
140
|
};
|
|
135
141
|
}
|