@tagadapay/plugin-sdk 3.0.3 → 3.0.12
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/build-cdn.js +113 -0
- package/dist/config/basisTheory.d.ts +26 -0
- package/dist/config/basisTheory.js +29 -0
- package/dist/external-tracker.js +5072 -0
- package/dist/external-tracker.min.js +11 -0
- package/dist/external-tracker.min.js.map +7 -0
- package/dist/react/config/payment.d.ts +8 -8
- package/dist/react/config/payment.js +17 -21
- package/dist/react/hooks/useApplePay.js +1 -1
- package/dist/react/hooks/usePayment.js +1 -3
- package/dist/react/hooks/useThreeds.js +2 -2
- package/dist/v2/core/client.d.ts +30 -3
- package/dist/v2/core/client.js +326 -8
- package/dist/v2/core/config/environment.d.ts +16 -3
- package/dist/v2/core/config/environment.js +72 -3
- package/dist/v2/core/funnelClient.d.ts +4 -0
- package/dist/v2/core/funnelClient.js +106 -4
- package/dist/v2/core/resources/funnel.d.ts +22 -0
- package/dist/v2/core/resources/offers.d.ts +64 -3
- package/dist/v2/core/resources/offers.js +112 -10
- package/dist/v2/core/resources/postPurchases.js +4 -1
- package/dist/v2/core/utils/configHotReload.d.ts +39 -0
- package/dist/v2/core/utils/configHotReload.js +75 -0
- package/dist/v2/core/utils/eventBus.d.ts +11 -0
- package/dist/v2/core/utils/eventBus.js +34 -0
- package/dist/v2/core/utils/pluginConfig.d.ts +14 -5
- package/dist/v2/core/utils/pluginConfig.js +74 -59
- package/dist/v2/core/utils/previewMode.d.ts +114 -0
- package/dist/v2/core/utils/previewMode.js +379 -0
- package/dist/v2/core/utils/sessionStorage.d.ts +5 -0
- package/dist/v2/core/utils/sessionStorage.js +22 -0
- package/dist/v2/index.d.ts +4 -1
- package/dist/v2/index.js +3 -1
- package/dist/v2/react/components/DebugDrawer.js +68 -46
- package/dist/v2/react/hooks/useOfferQuery.js +50 -17
- package/dist/v2/react/hooks/usePaymentQuery.js +1 -3
- package/dist/v2/react/hooks/usePreviewOffer.d.ts +84 -0
- package/dist/v2/react/hooks/usePreviewOffer.js +290 -0
- package/dist/v2/react/hooks/useThreeds.js +2 -2
- package/dist/v2/react/index.d.ts +2 -0
- package/dist/v2/react/index.js +1 -0
- package/dist/v2/react/providers/TagadaProvider.js +49 -32
- package/dist/v2/standalone/external-tracker.d.ts +119 -0
- package/dist/v2/standalone/external-tracker.js +260 -0
- package/dist/v2/standalone/index.d.ts +2 -0
- package/dist/v2/standalone/index.js +6 -0
- package/package.json +11 -3
- package/dist/v2/react/hooks/useOffersQuery.d.ts +0 -12
- package/dist/v2/react/hooks/useOffersQuery.js +0 -404
|
@@ -15,9 +15,12 @@ import { useTagadaContext } from '../providers/TagadaProvider';
|
|
|
15
15
|
import { getGlobalApiClient } from './useApiQuery';
|
|
16
16
|
import { usePluginConfig } from './usePluginConfig';
|
|
17
17
|
export function useOfferQuery(options) {
|
|
18
|
-
const { offerId, enabled = true, mainOrderId } = options;
|
|
18
|
+
const { offerId, enabled = true, mainOrderId: rawMainOrderId } = options;
|
|
19
19
|
const { storeId } = usePluginConfig();
|
|
20
20
|
const { isSessionInitialized, session } = useTagadaContext();
|
|
21
|
+
// 🎯 Normalize mainOrderId: treat undefined and empty string as the same
|
|
22
|
+
// This prevents effect re-runs when value changes from undefined to ''
|
|
23
|
+
const mainOrderId = rawMainOrderId || undefined;
|
|
21
24
|
// ─────────────────────────────────────────────────────────────────────────
|
|
22
25
|
// State
|
|
23
26
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -140,17 +143,28 @@ export function useOfferQuery(options) {
|
|
|
140
143
|
});
|
|
141
144
|
}, [initCheckoutSessionAsync, session?.customerId, isSessionInitialized]);
|
|
142
145
|
// ─────────────────────────────────────────────────────────────────────────
|
|
143
|
-
// 2. Auto-initialize checkout session when
|
|
146
|
+
// 2. Auto-initialize checkout session when offer is loaded
|
|
147
|
+
// (mainOrderId is optional - used for post-purchase upsells, not needed for standalone offers)
|
|
144
148
|
// ─────────────────────────────────────────────────────────────────────────
|
|
145
149
|
useEffect(() => {
|
|
146
|
-
|
|
150
|
+
// Only require offer and session to be ready (mainOrderId is optional)
|
|
151
|
+
if (!offer || !isSessionInitialized) {
|
|
147
152
|
return;
|
|
148
|
-
|
|
153
|
+
}
|
|
154
|
+
if (checkoutSession.checkoutSessionId || isInitializing) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
// 🎯 CRITICAL: Check and set initialization flag atomically
|
|
158
|
+
// This ensures only ONE effect can proceed with initialization
|
|
159
|
+
// Once set to true, it stays true (never reset) to prevent duplicate inits
|
|
160
|
+
if (hasInitializedRef.current) {
|
|
149
161
|
return;
|
|
162
|
+
}
|
|
163
|
+
hasInitializedRef.current = true;
|
|
150
164
|
const initSession = async () => {
|
|
151
|
-
hasInitializedRef.current = true;
|
|
152
165
|
setIsInitializing(true);
|
|
153
166
|
try {
|
|
167
|
+
// mainOrderId is optional - pass undefined if not provided
|
|
154
168
|
const result = await initCheckoutSession(offerId, mainOrderId);
|
|
155
169
|
setCheckoutSession(prev => ({
|
|
156
170
|
...prev,
|
|
@@ -160,7 +174,7 @@ export function useOfferQuery(options) {
|
|
|
160
174
|
await fetchOrderSummary(result.checkoutSessionId);
|
|
161
175
|
}
|
|
162
176
|
catch (err) {
|
|
163
|
-
console.error('Failed to init checkout session:', err);
|
|
177
|
+
console.error('[useOfferQuery] Failed to init checkout session:', err);
|
|
164
178
|
hasInitializedRef.current = false; // Allow retry on error
|
|
165
179
|
}
|
|
166
180
|
finally {
|
|
@@ -198,7 +212,8 @@ export function useOfferQuery(options) {
|
|
|
198
212
|
const product = variant?.product;
|
|
199
213
|
if (!variant || !product)
|
|
200
214
|
continue;
|
|
201
|
-
|
|
215
|
+
// Currency is determined from the checkout session, not from the offer
|
|
216
|
+
const currency = checkoutSession.orderSummary?.currency || 'USD';
|
|
202
217
|
const currencyOption = price?.currencyOptions?.[currency];
|
|
203
218
|
const unitAmount = currencyOption?.amount ?? variant.price ?? 0;
|
|
204
219
|
items.push({
|
|
@@ -282,21 +297,39 @@ export function useOfferQuery(options) {
|
|
|
282
297
|
const orderSummary = checkoutSession.orderSummary;
|
|
283
298
|
if (orderSummary?.options?.[productId]) {
|
|
284
299
|
const currency = orderSummary.currency || 'USD';
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
unitAmount
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
300
|
+
// Find the active line item for this product to get the correct priceId
|
|
301
|
+
const activeLineItem = orderSummary.items?.find((item) => item.productId === productId);
|
|
302
|
+
const activePriceId = activeLineItem?.priceId;
|
|
303
|
+
return orderSummary.options[productId].map((variant) => {
|
|
304
|
+
// Try to find the price that matches the active priceId, otherwise use first price
|
|
305
|
+
let unitAmount = 0;
|
|
306
|
+
if (activePriceId && variant.prices) {
|
|
307
|
+
const matchingPrice = variant.prices.find((p) => p.id === activePriceId);
|
|
308
|
+
if (matchingPrice?.currencyOptions?.[currency]?.amount) {
|
|
309
|
+
unitAmount = matchingPrice.currencyOptions[currency].amount;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Fallback to first price if no match found
|
|
313
|
+
if (unitAmount === 0 && variant.prices?.[0]?.currencyOptions?.[currency]?.amount) {
|
|
314
|
+
unitAmount = variant.prices[0].currencyOptions[currency].amount;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
variantId: variant.id,
|
|
318
|
+
variantName: variant.name,
|
|
319
|
+
sku: variant.sku || null,
|
|
320
|
+
imageUrl: variant.imageUrl || null,
|
|
321
|
+
unitAmount,
|
|
322
|
+
currency,
|
|
323
|
+
isDefault: variant.default ?? false,
|
|
324
|
+
};
|
|
325
|
+
});
|
|
294
326
|
}
|
|
295
327
|
// Fallback to static offer data
|
|
296
328
|
if (!offer?.offerLineItems)
|
|
297
329
|
return [];
|
|
298
330
|
const variants = [];
|
|
299
|
-
|
|
331
|
+
// Currency is determined from the checkout session, not from the offer
|
|
332
|
+
const currency = checkoutSession.orderSummary?.currency || 'USD';
|
|
300
333
|
for (const item of offer.offerLineItems) {
|
|
301
334
|
const price = item.price;
|
|
302
335
|
const variant = price?.variant;
|
|
@@ -28,10 +28,8 @@ export function usePaymentQuery() {
|
|
|
28
28
|
const { createSession, startChallenge } = useThreeds();
|
|
29
29
|
// Track challenge in progress to prevent multiple challenges
|
|
30
30
|
const challengeInProgressRef = useRef(false);
|
|
31
|
-
// Stabilize environment value to prevent re-renders
|
|
32
|
-
const currentEnvironment = useMemo(() => environment?.environment || 'local', [environment?.environment]);
|
|
33
31
|
// Get API key from embedded configuration with proper environment detection
|
|
34
|
-
const apiKey = useMemo(() => getBasisTheoryApiKey(
|
|
32
|
+
const apiKey = useMemo(() => getBasisTheoryApiKey(), []); // Auto-detects environment
|
|
35
33
|
// Initialize BasisTheory using React wrapper
|
|
36
34
|
const { bt: basisTheory, error: btError } = useBasisTheory(apiKey, {
|
|
37
35
|
elements: false,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePreviewOffer - Lightweight offer preview without checkout sessions
|
|
3
|
+
*
|
|
4
|
+
* This hook provides a fast, client-side offer preview by:
|
|
5
|
+
* 1. Fetching offer data with pre-computed summaries
|
|
6
|
+
* 2. Allowing variant/quantity selection
|
|
7
|
+
* 3. Recalculating totals on-the-fly without backend calls
|
|
8
|
+
* 4. NO checkout session creation - perfect for browsing/previewing
|
|
9
|
+
*/
|
|
10
|
+
import { Offer } from '../../core/resources/offers';
|
|
11
|
+
export interface PreviewLineItemSelection {
|
|
12
|
+
lineItemId: string;
|
|
13
|
+
variantId: string;
|
|
14
|
+
quantity: number;
|
|
15
|
+
priceId?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface PreviewOfferSummary {
|
|
18
|
+
items: Array<{
|
|
19
|
+
id: string;
|
|
20
|
+
productId: string;
|
|
21
|
+
variantId: string;
|
|
22
|
+
priceId: string;
|
|
23
|
+
productName: string;
|
|
24
|
+
variantName: string;
|
|
25
|
+
imageUrl: string | null;
|
|
26
|
+
quantity: number;
|
|
27
|
+
unitAmount: number;
|
|
28
|
+
amount: number;
|
|
29
|
+
adjustedAmount: number;
|
|
30
|
+
currency: string;
|
|
31
|
+
}>;
|
|
32
|
+
totalAmount: number;
|
|
33
|
+
totalAdjustedAmount: number;
|
|
34
|
+
totalPromotionAmount: number;
|
|
35
|
+
currency: string;
|
|
36
|
+
options: Record<string, Array<{
|
|
37
|
+
variantId: string;
|
|
38
|
+
variantName: string;
|
|
39
|
+
sku: string | null;
|
|
40
|
+
imageUrl: string | null;
|
|
41
|
+
prices: Array<{
|
|
42
|
+
id: string;
|
|
43
|
+
currencyOptions: Record<string, {
|
|
44
|
+
amount: number;
|
|
45
|
+
currency: string;
|
|
46
|
+
}>;
|
|
47
|
+
}>;
|
|
48
|
+
default: boolean;
|
|
49
|
+
}>>;
|
|
50
|
+
}
|
|
51
|
+
export interface UsePreviewOfferOptions {
|
|
52
|
+
offerId: string;
|
|
53
|
+
currency?: string;
|
|
54
|
+
initialSelections?: Record<string, PreviewLineItemSelection>;
|
|
55
|
+
}
|
|
56
|
+
export interface UsePreviewOfferResult {
|
|
57
|
+
offer: Offer | null;
|
|
58
|
+
isLoading: boolean;
|
|
59
|
+
isFetching: boolean;
|
|
60
|
+
isPaying: boolean;
|
|
61
|
+
error: Error | null;
|
|
62
|
+
summary: PreviewOfferSummary | null;
|
|
63
|
+
selections: Record<string, PreviewLineItemSelection>;
|
|
64
|
+
selectVariant: (lineItemId: string, variantId: string) => void;
|
|
65
|
+
updateQuantity: (lineItemId: string, quantity: number) => void;
|
|
66
|
+
selectVariantByProduct: (productId: string, variantId: string) => void;
|
|
67
|
+
updateQuantityByProduct: (productId: string, quantity: number) => void;
|
|
68
|
+
getAvailableVariants: (productId: string) => Array<{
|
|
69
|
+
variantId: string;
|
|
70
|
+
variantName: string;
|
|
71
|
+
imageUrl: string | null;
|
|
72
|
+
unitAmount: number;
|
|
73
|
+
currency: string;
|
|
74
|
+
}>;
|
|
75
|
+
pay: (mainOrderId?: string) => Promise<{
|
|
76
|
+
checkoutUrl: string;
|
|
77
|
+
}>;
|
|
78
|
+
toCheckout: (mainOrderId?: string) => Promise<{
|
|
79
|
+
checkoutSessionId?: string;
|
|
80
|
+
checkoutToken?: string;
|
|
81
|
+
checkoutUrl: string;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
export declare function usePreviewOffer(options: UsePreviewOfferOptions): UsePreviewOfferResult;
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePreviewOffer - Lightweight offer preview without checkout sessions
|
|
3
|
+
*
|
|
4
|
+
* This hook provides a fast, client-side offer preview by:
|
|
5
|
+
* 1. Fetching offer data with pre-computed summaries
|
|
6
|
+
* 2. Allowing variant/quantity selection
|
|
7
|
+
* 3. Recalculating totals on-the-fly without backend calls
|
|
8
|
+
* 4. NO checkout session creation - perfect for browsing/previewing
|
|
9
|
+
*/
|
|
10
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
|
+
import { useQuery } from '@tanstack/react-query';
|
|
12
|
+
import { OffersResource } from '../../core/resources/offers';
|
|
13
|
+
import { getGlobalApiClient } from './useApiQuery';
|
|
14
|
+
export function usePreviewOffer(options) {
|
|
15
|
+
const { offerId, currency: requestedCurrency = 'USD', initialSelections = {} } = options;
|
|
16
|
+
const offersResource = useMemo(() => new OffersResource(getGlobalApiClient()), []);
|
|
17
|
+
// Track user selections (variant + quantity per product)
|
|
18
|
+
const [selections, setSelections] = useState(initialSelections);
|
|
19
|
+
const [isPaying, setIsPaying] = useState(false);
|
|
20
|
+
// Build lineItems array for API call
|
|
21
|
+
const lineItemsForPreview = useMemo(() => {
|
|
22
|
+
if (Object.keys(selections).length === 0)
|
|
23
|
+
return undefined;
|
|
24
|
+
return Object.values(selections).map((selection) => {
|
|
25
|
+
const item = {
|
|
26
|
+
variantId: selection.variantId,
|
|
27
|
+
quantity: selection.quantity,
|
|
28
|
+
};
|
|
29
|
+
// Include lineItemId for precise matching
|
|
30
|
+
if (selection.lineItemId) {
|
|
31
|
+
item.lineItemId = selection.lineItemId;
|
|
32
|
+
}
|
|
33
|
+
return item;
|
|
34
|
+
});
|
|
35
|
+
}, [selections]);
|
|
36
|
+
// 🎯 ONE API CALL - Fetch offer + preview in one request
|
|
37
|
+
const { data, isLoading, isFetching, error } = useQuery({
|
|
38
|
+
queryKey: ['offer-preview', offerId, requestedCurrency, lineItemsForPreview],
|
|
39
|
+
queryFn: async () => {
|
|
40
|
+
console.log('🔍 [usePreviewOffer] Fetching preview:', {
|
|
41
|
+
offerId,
|
|
42
|
+
currency: requestedCurrency,
|
|
43
|
+
lineItems: lineItemsForPreview,
|
|
44
|
+
});
|
|
45
|
+
const result = await offersResource.previewOffer(offerId, requestedCurrency, lineItemsForPreview);
|
|
46
|
+
console.log('✅ [usePreviewOffer] Preview result:', result);
|
|
47
|
+
// Extract offer and preview from unified response
|
|
48
|
+
const offer = result.offer || null;
|
|
49
|
+
const preview = result.preview || result; // Backwards compatibility
|
|
50
|
+
// Transform preview to expected format
|
|
51
|
+
const summary = {
|
|
52
|
+
items: (preview.items || []).map((item) => ({
|
|
53
|
+
id: item.id,
|
|
54
|
+
productId: item.productId,
|
|
55
|
+
variantId: item.variantId,
|
|
56
|
+
priceId: item.priceId,
|
|
57
|
+
productName: item.product?.name || item.orderLineItemProduct?.name || '',
|
|
58
|
+
variantName: item.variant?.name || item.orderLineItemVariant?.name || '',
|
|
59
|
+
imageUrl: item.variant?.imageUrl || item.orderLineItemVariant?.imageUrl || null,
|
|
60
|
+
quantity: item.quantity,
|
|
61
|
+
unitAmount: item.unitAmount,
|
|
62
|
+
amount: item.amount,
|
|
63
|
+
adjustedAmount: item.adjustedAmount,
|
|
64
|
+
currency: item.currency,
|
|
65
|
+
})),
|
|
66
|
+
totalAmount: preview.totalAmount || 0,
|
|
67
|
+
totalAdjustedAmount: preview.totalAdjustedAmount || 0,
|
|
68
|
+
totalPromotionAmount: preview.totalPromotionAmount || 0,
|
|
69
|
+
currency: preview.currency || requestedCurrency,
|
|
70
|
+
options: preview.options || {},
|
|
71
|
+
};
|
|
72
|
+
return { offer, summary };
|
|
73
|
+
},
|
|
74
|
+
enabled: !!offerId,
|
|
75
|
+
staleTime: 0, // Always refetch, no cache
|
|
76
|
+
gcTime: 5000, // Keep in cache for 5s during transitions
|
|
77
|
+
placeholderData: (previousData) => previousData, // Keep previous data during refetch
|
|
78
|
+
refetchOnMount: 'always', // Always refetch on mount
|
|
79
|
+
retry: false, // Don't retry on error
|
|
80
|
+
retryOnMount: false,
|
|
81
|
+
});
|
|
82
|
+
const offer = data?.offer || null;
|
|
83
|
+
const summary = data?.summary || null;
|
|
84
|
+
// Initialize selections from preview response on first load
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
if (summary && Object.keys(selections).length === 0) {
|
|
87
|
+
const initialSelections = {};
|
|
88
|
+
summary.items.forEach((item) => {
|
|
89
|
+
// Use lineItemId as key to support same product with different variants
|
|
90
|
+
initialSelections[item.id] = {
|
|
91
|
+
lineItemId: item.id,
|
|
92
|
+
variantId: item.variantId,
|
|
93
|
+
quantity: item.quantity,
|
|
94
|
+
priceId: item.priceId,
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
console.log('🎬 [usePreviewOffer] Initialized selections:', initialSelections);
|
|
98
|
+
setSelections(initialSelections);
|
|
99
|
+
}
|
|
100
|
+
}, [summary, selections]);
|
|
101
|
+
console.log('🎁 [usePreviewOffer] State:', {
|
|
102
|
+
offerId,
|
|
103
|
+
isLoading,
|
|
104
|
+
isFetching,
|
|
105
|
+
hasData: !!data,
|
|
106
|
+
hasOffer: !!offer,
|
|
107
|
+
hasSummary: !!summary,
|
|
108
|
+
selectionsCount: Object.keys(selections).length,
|
|
109
|
+
error: error?.message,
|
|
110
|
+
});
|
|
111
|
+
// ===== Precise methods (by lineItemId) =====
|
|
112
|
+
// Select a variant for a specific line item
|
|
113
|
+
const selectVariant = useCallback((lineItemId, variantId) => {
|
|
114
|
+
const item = summary?.items.find((i) => i.id === lineItemId);
|
|
115
|
+
if (!item)
|
|
116
|
+
return;
|
|
117
|
+
const variants = summary?.options?.[item.productId];
|
|
118
|
+
if (!variants)
|
|
119
|
+
return;
|
|
120
|
+
const selectedVariant = variants.find((v) => (v.id || v.variantId) === variantId);
|
|
121
|
+
if (!selectedVariant)
|
|
122
|
+
return;
|
|
123
|
+
const priceId = selectedVariant.prices?.[0]?.id;
|
|
124
|
+
setSelections(prev => ({
|
|
125
|
+
...prev,
|
|
126
|
+
[lineItemId]: {
|
|
127
|
+
lineItemId,
|
|
128
|
+
variantId,
|
|
129
|
+
quantity: prev[lineItemId]?.quantity || item.quantity,
|
|
130
|
+
priceId,
|
|
131
|
+
},
|
|
132
|
+
}));
|
|
133
|
+
}, [summary]);
|
|
134
|
+
// Update quantity for a specific line item
|
|
135
|
+
const updateQuantity = useCallback((lineItemId, quantity) => {
|
|
136
|
+
if (quantity < 1)
|
|
137
|
+
return;
|
|
138
|
+
const item = summary?.items.find((i) => i.id === lineItemId);
|
|
139
|
+
if (!item)
|
|
140
|
+
return;
|
|
141
|
+
setSelections(prev => ({
|
|
142
|
+
...prev,
|
|
143
|
+
[lineItemId]: {
|
|
144
|
+
lineItemId,
|
|
145
|
+
variantId: prev[lineItemId]?.variantId || item.variantId,
|
|
146
|
+
quantity,
|
|
147
|
+
priceId: prev[lineItemId]?.priceId || item.priceId,
|
|
148
|
+
},
|
|
149
|
+
}));
|
|
150
|
+
}, [summary]);
|
|
151
|
+
// ===== Simple methods (by productId - affects all items with this product) =====
|
|
152
|
+
// Select a variant for all line items with this product
|
|
153
|
+
const selectVariantByProduct = useCallback((productId, variantId) => {
|
|
154
|
+
if (!summary?.options?.[productId])
|
|
155
|
+
return;
|
|
156
|
+
const variants = summary.options[productId];
|
|
157
|
+
const selectedVariant = variants.find((v) => (v.id || v.variantId) === variantId);
|
|
158
|
+
if (!selectedVariant)
|
|
159
|
+
return;
|
|
160
|
+
const priceId = selectedVariant.prices?.[0]?.id;
|
|
161
|
+
setSelections(prev => {
|
|
162
|
+
const updated = { ...prev };
|
|
163
|
+
// Update all line items with this productId
|
|
164
|
+
summary.items.forEach((item) => {
|
|
165
|
+
if (item.productId === productId) {
|
|
166
|
+
updated[item.id] = {
|
|
167
|
+
lineItemId: item.id,
|
|
168
|
+
variantId,
|
|
169
|
+
quantity: prev[item.id]?.quantity || item.quantity,
|
|
170
|
+
priceId,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
return updated;
|
|
175
|
+
});
|
|
176
|
+
}, [summary]);
|
|
177
|
+
// Update quantity for all line items with this product
|
|
178
|
+
const updateQuantityByProduct = useCallback((productId, quantity) => {
|
|
179
|
+
if (quantity < 1)
|
|
180
|
+
return;
|
|
181
|
+
setSelections(prev => {
|
|
182
|
+
const updated = { ...prev };
|
|
183
|
+
// Update all line items with this productId
|
|
184
|
+
summary?.items.forEach((item) => {
|
|
185
|
+
if (item.productId === productId) {
|
|
186
|
+
updated[item.id] = {
|
|
187
|
+
lineItemId: item.id,
|
|
188
|
+
variantId: prev[item.id]?.variantId || item.variantId,
|
|
189
|
+
quantity,
|
|
190
|
+
priceId: prev[item.id]?.priceId || item.priceId,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
return updated;
|
|
195
|
+
});
|
|
196
|
+
}, [summary]);
|
|
197
|
+
// Get available variants for a product
|
|
198
|
+
const getAvailableVariants = useCallback((productId) => {
|
|
199
|
+
if (!summary?.options?.[productId])
|
|
200
|
+
return [];
|
|
201
|
+
const currentItem = summary.items.find((i) => i.productId === productId);
|
|
202
|
+
const activePriceId = currentItem?.priceId;
|
|
203
|
+
const currency = summary.currency || requestedCurrency;
|
|
204
|
+
return summary.options[productId].map((variant) => {
|
|
205
|
+
// Find matching price or use first
|
|
206
|
+
let unitAmount = 0;
|
|
207
|
+
if (activePriceId && variant.prices) {
|
|
208
|
+
const matchingPrice = variant.prices.find((p) => p.id === activePriceId);
|
|
209
|
+
if (matchingPrice?.currencyOptions?.[currency]) {
|
|
210
|
+
unitAmount = matchingPrice.currencyOptions[currency].amount;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Fallback to first price
|
|
214
|
+
if (unitAmount === 0 && variant.prices?.[0]?.currencyOptions?.[currency]) {
|
|
215
|
+
unitAmount = variant.prices[0].currencyOptions[currency].amount;
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
variantId: variant.id || variant.variantId,
|
|
219
|
+
variantName: variant.name || variant.variantName,
|
|
220
|
+
imageUrl: variant.imageUrl,
|
|
221
|
+
unitAmount,
|
|
222
|
+
currency,
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
}, [summary, requestedCurrency]);
|
|
226
|
+
// Pay for the offer with current selections
|
|
227
|
+
const pay = useCallback(async (mainOrderId) => {
|
|
228
|
+
if (isPaying) {
|
|
229
|
+
throw new Error('Payment already in progress');
|
|
230
|
+
}
|
|
231
|
+
setIsPaying(true);
|
|
232
|
+
try {
|
|
233
|
+
const result = await offersResource.payPreviewedOffer(offerId, requestedCurrency, lineItemsForPreview, typeof window !== 'undefined' ? window.location.href : undefined, mainOrderId);
|
|
234
|
+
console.log('[usePreviewOffer] Payment initiated:', result);
|
|
235
|
+
return {
|
|
236
|
+
checkoutUrl: result.checkout.checkoutUrl,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
console.error('[usePreviewOffer] Payment failed:', error);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
setIsPaying(false);
|
|
245
|
+
}
|
|
246
|
+
}, [offerId, requestedCurrency, lineItemsForPreview, offersResource, isPaying]);
|
|
247
|
+
// Create checkout session without paying (for landing pages)
|
|
248
|
+
const toCheckout = useCallback(async (mainOrderId) => {
|
|
249
|
+
if (isPaying) {
|
|
250
|
+
throw new Error('Operation already in progress');
|
|
251
|
+
}
|
|
252
|
+
setIsPaying(true);
|
|
253
|
+
try {
|
|
254
|
+
const result = await offersResource.toCheckout(offerId, requestedCurrency, lineItemsForPreview, typeof window !== 'undefined' ? window.location.href : undefined, mainOrderId);
|
|
255
|
+
console.log('[usePreviewOffer] Checkout session created:', result);
|
|
256
|
+
return {
|
|
257
|
+
checkoutSessionId: result.checkoutSessionId,
|
|
258
|
+
checkoutToken: result.checkoutToken,
|
|
259
|
+
checkoutUrl: result.checkoutUrl,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
catch (error) {
|
|
263
|
+
console.error('[usePreviewOffer] Checkout creation failed:', error);
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
setIsPaying(false);
|
|
268
|
+
}
|
|
269
|
+
}, [offerId, requestedCurrency, lineItemsForPreview, offersResource, isPaying]);
|
|
270
|
+
return {
|
|
271
|
+
offer,
|
|
272
|
+
isLoading,
|
|
273
|
+
isFetching,
|
|
274
|
+
isPaying,
|
|
275
|
+
error: error,
|
|
276
|
+
summary,
|
|
277
|
+
selections,
|
|
278
|
+
// Precise methods
|
|
279
|
+
selectVariant,
|
|
280
|
+
updateQuantity,
|
|
281
|
+
// Simple methods
|
|
282
|
+
selectVariantByProduct,
|
|
283
|
+
updateQuantityByProduct,
|
|
284
|
+
// Helpers
|
|
285
|
+
getAvailableVariants,
|
|
286
|
+
// Payment
|
|
287
|
+
pay,
|
|
288
|
+
toCheckout,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
@@ -47,7 +47,7 @@ export function useThreeds(options = {}) {
|
|
|
47
47
|
throw new Error('BasisTheory3ds not loaded yet');
|
|
48
48
|
}
|
|
49
49
|
// Use the same API key approach as the working CMS version
|
|
50
|
-
const apiKey = getBasisTheoryApiKey(
|
|
50
|
+
const apiKey = getBasisTheoryApiKey(); // Auto-detects environment
|
|
51
51
|
const bt3ds = basisTheory3ds(apiKey);
|
|
52
52
|
const session = await bt3ds.createSession({
|
|
53
53
|
tokenId: paymentInstrument.token,
|
|
@@ -91,7 +91,7 @@ export function useThreeds(options = {}) {
|
|
|
91
91
|
throw new Error('BasisTheory3ds not loaded yet');
|
|
92
92
|
}
|
|
93
93
|
// Use the same API key approach as the working CMS version
|
|
94
|
-
const apiKey = getBasisTheoryApiKey(
|
|
94
|
+
const apiKey = getBasisTheoryApiKey(); // Auto-detects environment
|
|
95
95
|
if (!apiKey) {
|
|
96
96
|
throw new Error('BasisTheory API key is not set');
|
|
97
97
|
}
|
package/dist/v2/react/index.d.ts
CHANGED
|
@@ -27,6 +27,7 @@ export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
|
|
|
27
27
|
export { useCurrency } from './hooks/useCurrency';
|
|
28
28
|
export { useDiscountsQuery as useDiscounts } from './hooks/useDiscountsQuery';
|
|
29
29
|
export { useOfferQuery as useOffer } from './hooks/useOfferQuery';
|
|
30
|
+
export { usePreviewOffer } from './hooks/usePreviewOffer';
|
|
30
31
|
export { useOrderBumpQuery as useOrderBump } from './hooks/useOrderBumpQuery';
|
|
31
32
|
export { useOrderQuery as useOrder } from './hooks/useOrderQuery';
|
|
32
33
|
export { usePaymentQuery as usePayment } from './hooks/usePaymentQuery';
|
|
@@ -64,6 +65,7 @@ export type { FunnelAction, FunnelNavigationAction, FunnelNavigationResult, Simp
|
|
|
64
65
|
export type { FunnelContextValue } from './hooks/useFunnel';
|
|
65
66
|
export type { UseFunnelOptions as UseFunnelLegacyOptions, UseFunnelResult as UseFunnelLegacyResult } from './hooks/useFunnelLegacy';
|
|
66
67
|
export type { AvailableVariant, LineItemSelection, OfferLineItem, OfferPreviewSummary, UseOfferQueryOptions as UseOfferOptions, UseOfferQueryResult as UseOfferResult } from './hooks/useOfferQuery';
|
|
68
|
+
export type { PreviewOfferSummary, UsePreviewOfferOptions, UsePreviewOfferResult } from './hooks/usePreviewOffer';
|
|
67
69
|
export type { UseOrderBumpQueryOptions as UseOrderBumpOptions, UseOrderBumpQueryResult as UseOrderBumpResult } from './hooks/useOrderBumpQuery';
|
|
68
70
|
export type { UseOrderQueryOptions as UseOrderOptions, UseOrderQueryResult as UseOrderResult } from './hooks/useOrderQuery';
|
|
69
71
|
export type { PaymentHook as UsePaymentResult } from './hooks/usePaymentQuery';
|
package/dist/v2/react/index.js
CHANGED
|
@@ -30,6 +30,7 @@ export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
|
|
|
30
30
|
export { useCurrency } from './hooks/useCurrency';
|
|
31
31
|
export { useDiscountsQuery as useDiscounts } from './hooks/useDiscountsQuery';
|
|
32
32
|
export { useOfferQuery as useOffer } from './hooks/useOfferQuery';
|
|
33
|
+
export { usePreviewOffer } from './hooks/usePreviewOffer';
|
|
33
34
|
export { useOrderBumpQuery as useOrderBump } from './hooks/useOrderBumpQuery';
|
|
34
35
|
export { useOrderQuery as useOrder } from './hooks/useOrderQuery';
|
|
35
36
|
export { usePaymentQuery as usePayment } from './hooks/usePaymentQuery';
|