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