@tagadapay/plugin-sdk 3.0.2 → 3.0.9
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 +4947 -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 +31 -3
- package/dist/v2/core/client.js +234 -9
- 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/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.d.ts +1 -2
- package/dist/v2/react/providers/TagadaProvider.js +51 -34
- 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
|
@@ -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';
|
|
@@ -115,7 +115,6 @@ interface TagadaProviderProps {
|
|
|
115
115
|
*/
|
|
116
116
|
debugScripts?: DebugScript[];
|
|
117
117
|
}
|
|
118
|
-
export declare function TagadaProvider({ children, environment, customApiConfig,
|
|
119
|
-
debugMode, localConfig, blockUntilSessionReady, rawPluginConfig, features, funnelId, autoInitializeFunnel, onNavigate, onFunnelError, debugScripts, }: TagadaProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
118
|
+
export declare function TagadaProvider({ children, environment, customApiConfig, debugMode, localConfig, blockUntilSessionReady, rawPluginConfig, features, funnelId, autoInitializeFunnel, onNavigate, onFunnelError, debugScripts, }: TagadaProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
120
119
|
export declare function useTagadaContext(): TagadaContextValue;
|
|
121
120
|
export {};
|
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
* TagadaProvider - Main provider component for the Tagada Pay React SDK
|
|
5
5
|
*/
|
|
6
6
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
7
|
-
import { createContext, useCallback, useContext, useEffect, useMemo,
|
|
7
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react';
|
|
8
8
|
import { ApiService } from '../../../react/services/apiService';
|
|
9
9
|
import { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits, } from '../../../react/utils/money';
|
|
10
10
|
import { TagadaClient } from '../../core/client';
|
|
@@ -43,8 +43,7 @@ const InitializationLoader = () => (_jsxs("div", { style: {
|
|
|
43
43
|
}
|
|
44
44
|
` })] }));
|
|
45
45
|
const TagadaContext = createContext(null);
|
|
46
|
-
export function TagadaProvider({ children, environment, customApiConfig,
|
|
47
|
-
debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, features, funnelId, autoInitializeFunnel = true, onNavigate, onFunnelError, debugScripts = [], }) {
|
|
46
|
+
export function TagadaProvider({ children, environment, customApiConfig, debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, features, funnelId, autoInitializeFunnel = true, onNavigate, onFunnelError, debugScripts = [], }) {
|
|
48
47
|
// Auto-detect debug mode from environment if not explicitly set
|
|
49
48
|
const effectiveDebugMode = debugMode !== undefined
|
|
50
49
|
? debugMode
|
|
@@ -52,15 +51,31 @@ debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, feature
|
|
|
52
51
|
(typeof process !== 'undefined' && process.env?.NODE_ENV === 'development');
|
|
53
52
|
// Initialize client
|
|
54
53
|
const client = useMemo(() => {
|
|
54
|
+
// Merge features with defaults, ensuring autoRedirect is always false in provider
|
|
55
|
+
const mergedFeatures = features ?? { funnel: true };
|
|
56
|
+
const funnelFeature = mergedFeatures.funnel;
|
|
55
57
|
const config = {
|
|
56
58
|
environment,
|
|
59
|
+
customApiConfig,
|
|
57
60
|
debugMode: effectiveDebugMode,
|
|
58
61
|
localConfig,
|
|
59
62
|
rawPluginConfig,
|
|
60
63
|
blockUntilSessionReady,
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
funnel:
|
|
64
|
+
features: {
|
|
65
|
+
...mergedFeatures,
|
|
66
|
+
funnel: typeof funnelFeature === 'object'
|
|
67
|
+
? {
|
|
68
|
+
...funnelFeature,
|
|
69
|
+
// Always disable auto-redirect in core because we handle it in the provider
|
|
70
|
+
// to set pendingRedirect state and prevent UI flashes
|
|
71
|
+
autoRedirect: false,
|
|
72
|
+
}
|
|
73
|
+
: funnelFeature === false
|
|
74
|
+
? false
|
|
75
|
+
: {
|
|
76
|
+
// Default: enable funnel with auto-redirect disabled
|
|
77
|
+
autoRedirect: false,
|
|
78
|
+
},
|
|
64
79
|
},
|
|
65
80
|
};
|
|
66
81
|
return new TagadaClient(config);
|
|
@@ -70,6 +85,17 @@ debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, feature
|
|
|
70
85
|
useEffect(() => {
|
|
71
86
|
return client.subscribe(setState);
|
|
72
87
|
}, [client]);
|
|
88
|
+
// Listen for config hot-reload updates
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const cleanup = client.bus.on('CONFIG_UPDATED', () => {
|
|
91
|
+
// Force re-render when config is updated
|
|
92
|
+
setState(client.getState());
|
|
93
|
+
if (state.debugMode) {
|
|
94
|
+
console.log('[TagadaProvider] Config updated, re-rendering...');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
return cleanup;
|
|
98
|
+
}, [client, state.debugMode]);
|
|
73
99
|
// Cleanup client on unmount
|
|
74
100
|
useEffect(() => {
|
|
75
101
|
return () => {
|
|
@@ -151,39 +177,29 @@ debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, feature
|
|
|
151
177
|
lastUpdated: new Date(),
|
|
152
178
|
});
|
|
153
179
|
}, []);
|
|
154
|
-
// Refresh Coordinator
|
|
155
|
-
const checkoutRefreshRefs = useRef(new Set());
|
|
156
|
-
const orderBumpRefreshRefs = useRef(new Set());
|
|
180
|
+
// Refresh Coordinator (backed by EventBus in core)
|
|
157
181
|
const refreshCoordinator = useMemo(() => ({
|
|
158
182
|
registerCheckoutRefresh: (refreshFn, name) => {
|
|
159
|
-
|
|
183
|
+
client.bus.on('CHECKOUT_REFRESH', refreshFn);
|
|
160
184
|
},
|
|
161
185
|
registerOrderBumpRefresh: (refreshFn) => {
|
|
162
|
-
|
|
186
|
+
client.bus.on('ORDER_BUMP_REFRESH', refreshFn);
|
|
163
187
|
},
|
|
164
188
|
notifyCheckoutChanged: async () => {
|
|
165
|
-
|
|
166
|
-
await Promise.all(Array.from(orderBumpRefreshRefs.current).map((fn) => fn().catch(console.warn)));
|
|
167
|
-
}
|
|
189
|
+
await client.bus.emit('ORDER_BUMP_REFRESH'); // Notify bumps that checkout changed? Wait logic
|
|
168
190
|
},
|
|
169
191
|
notifyOrderBumpChanged: async () => {
|
|
170
|
-
|
|
171
|
-
await Promise.all(Array.from(checkoutRefreshRefs.current).map((fn) => fn().catch(console.warn)));
|
|
172
|
-
}
|
|
192
|
+
await client.bus.emit('CHECKOUT_REFRESH'); // Notify checkout that bumps changed
|
|
173
193
|
},
|
|
174
194
|
unregisterCheckoutRefresh: (refreshFn) => {
|
|
175
195
|
if (refreshFn)
|
|
176
|
-
|
|
177
|
-
else
|
|
178
|
-
checkoutRefreshRefs.current.clear();
|
|
196
|
+
client.bus.off('CHECKOUT_REFRESH', refreshFn);
|
|
179
197
|
},
|
|
180
198
|
unregisterOrderBumpRefresh: (refreshFn) => {
|
|
181
199
|
if (refreshFn)
|
|
182
|
-
|
|
183
|
-
else
|
|
184
|
-
orderBumpRefreshRefs.current.clear();
|
|
200
|
+
client.bus.off('ORDER_BUMP_REFRESH', refreshFn);
|
|
185
201
|
},
|
|
186
|
-
}), []);
|
|
202
|
+
}), [client]);
|
|
187
203
|
// Money Utils
|
|
188
204
|
const moneyUtils = useMemo(() => ({
|
|
189
205
|
formatMoney,
|
|
@@ -212,8 +228,8 @@ debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, feature
|
|
|
212
228
|
refetch: noopAsync,
|
|
213
229
|
};
|
|
214
230
|
}
|
|
215
|
-
|
|
216
|
-
|
|
231
|
+
// Helper to handle navigation result with optional auto-redirect
|
|
232
|
+
const handleNavigationResult = (result) => {
|
|
217
233
|
// Call custom onNavigate handler if provided
|
|
218
234
|
let shouldAutoRedirect = true;
|
|
219
235
|
if (onNavigate) {
|
|
@@ -230,22 +246,23 @@ debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, feature
|
|
|
230
246
|
setPendingRedirect(true);
|
|
231
247
|
window.location.href = result.url;
|
|
232
248
|
}
|
|
249
|
+
};
|
|
250
|
+
const next = async (event) => {
|
|
251
|
+
const result = await client.funnel.navigate(event);
|
|
252
|
+
handleNavigationResult(result);
|
|
233
253
|
return result;
|
|
234
254
|
};
|
|
235
255
|
const goToStep = async (stepId) => {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
});
|
|
256
|
+
const result = await client.funnel.goToStep(stepId);
|
|
257
|
+
handleNavigationResult(result);
|
|
258
|
+
return result;
|
|
240
259
|
};
|
|
241
|
-
// Helper to resolve accountId with fallbacks
|
|
242
|
-
const getAccountId = () => state.store?.accountId || state.pluginConfig?.accountId || state.session?.accountId || '';
|
|
243
260
|
return {
|
|
244
261
|
next,
|
|
245
262
|
goToStep,
|
|
246
263
|
updateContext: (updates) => client.funnel.updateContext(updates),
|
|
247
264
|
initializeSession: async (entryStepId) => {
|
|
248
|
-
const accountId = getAccountId();
|
|
265
|
+
const accountId = client.getAccountId();
|
|
249
266
|
if (!accountId || !state.auth.session || !state.store) {
|
|
250
267
|
throw new Error('Cannot initialize funnel: missing required data (accountId, session, or store)');
|
|
251
268
|
}
|
|
@@ -253,7 +270,7 @@ debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, feature
|
|
|
253
270
|
},
|
|
254
271
|
endSession: () => client.funnel.endSession(),
|
|
255
272
|
retryInitialization: () => {
|
|
256
|
-
const accountId = getAccountId();
|
|
273
|
+
const accountId = client.getAccountId();
|
|
257
274
|
if (!accountId || !state.auth.session || !state.store) {
|
|
258
275
|
return Promise.reject(new Error('Cannot retry initialization: missing required data'));
|
|
259
276
|
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TagadaPay External Page Tracker
|
|
3
|
+
*
|
|
4
|
+
* A lightweight standalone script for tracking users on external pages
|
|
5
|
+
* that are part of a Tagadapay funnel but not hosted on the Tagadapay platform.
|
|
6
|
+
*
|
|
7
|
+
* ARCHITECTURE:
|
|
8
|
+
* - Reuses core SDK infrastructure (TagadaClient) for authentication and API handling
|
|
9
|
+
* - All tracking handled automatically by funnelOrchestrator via autoInitialize() and navigate()
|
|
10
|
+
* - No separate tracking endpoints needed - unified tracking through orchestrator
|
|
11
|
+
*
|
|
12
|
+
* Usage (via CDN):
|
|
13
|
+
* <script src="https://cdn.jsdelivr.net/npm/@tagadapay/plugin-sdk/dist/external-tracker.min.js"></script>
|
|
14
|
+
* <script>
|
|
15
|
+
* TagadaTracker.init({
|
|
16
|
+
* storeId: 'store_xxx',
|
|
17
|
+
* accountId: 'acc_xxx',
|
|
18
|
+
* funnelId: 'funnel_xxx', // Optional: detected from URL
|
|
19
|
+
* stepId: 'step_xxx',
|
|
20
|
+
* stepName: 'External Offer Page',
|
|
21
|
+
* stepType: 'external'
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* // Navigate to next step (tracking automatic)
|
|
25
|
+
* TagadaTracker.navigate({
|
|
26
|
+
* eventType: 'form.submitted',
|
|
27
|
+
* eventData: { email: 'user@example.com' }
|
|
28
|
+
* });
|
|
29
|
+
* </script>
|
|
30
|
+
*/
|
|
31
|
+
import type { TagadaClient } from '../core/client';
|
|
32
|
+
export interface TagadaTrackerConfig {
|
|
33
|
+
/** Store ID (required) */
|
|
34
|
+
storeId: string;
|
|
35
|
+
/** Account ID (required for session init) */
|
|
36
|
+
accountId?: string;
|
|
37
|
+
/** Funnel ID (optional - can be extracted from URL param) */
|
|
38
|
+
funnelId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Step ID (REQUIRED for external pages)
|
|
41
|
+
*
|
|
42
|
+
* External URLs cannot be mapped to steps via URL matching (only works for Tagadapay-hosted pages).
|
|
43
|
+
* You must explicitly specify which step this external page represents.
|
|
44
|
+
*/
|
|
45
|
+
stepId: string;
|
|
46
|
+
/** Step name (optional - for analytics/debugging) */
|
|
47
|
+
stepName?: string;
|
|
48
|
+
/** Step type (optional - e.g., 'landing', 'offer', 'external') */
|
|
49
|
+
stepType?: string;
|
|
50
|
+
/** API base URL (defaults to production) */
|
|
51
|
+
apiBaseUrl?: string;
|
|
52
|
+
/** Enable debug logging */
|
|
53
|
+
debug?: boolean;
|
|
54
|
+
/** Callback when session is ready */
|
|
55
|
+
onReady?: (session: ExternalTrackerSession) => void;
|
|
56
|
+
/** Callback on error */
|
|
57
|
+
onError?: (error: Error) => void;
|
|
58
|
+
}
|
|
59
|
+
export interface ExternalTrackerSession {
|
|
60
|
+
sessionId: string;
|
|
61
|
+
customerId: string;
|
|
62
|
+
storeId: string;
|
|
63
|
+
funnelId?: string;
|
|
64
|
+
currentStepId?: string;
|
|
65
|
+
cmsToken?: string;
|
|
66
|
+
}
|
|
67
|
+
export interface NavigateOptions {
|
|
68
|
+
/** Event type for navigation (e.g., 'offer.accepted', 'form.submitted') */
|
|
69
|
+
eventType: string;
|
|
70
|
+
/** Event data */
|
|
71
|
+
eventData?: Record<string, unknown>;
|
|
72
|
+
/** Override return URL */
|
|
73
|
+
returnUrl?: string;
|
|
74
|
+
}
|
|
75
|
+
declare class TagadaExternalTracker {
|
|
76
|
+
private config;
|
|
77
|
+
private client;
|
|
78
|
+
private initialized;
|
|
79
|
+
private initializing;
|
|
80
|
+
/**
|
|
81
|
+
* Initialize the tracker using SDK infrastructure
|
|
82
|
+
*/
|
|
83
|
+
init(config: TagadaTrackerConfig): Promise<ExternalTrackerSession | null>;
|
|
84
|
+
/**
|
|
85
|
+
* Get current session from SDK state
|
|
86
|
+
*/
|
|
87
|
+
getSession(): ExternalTrackerSession | null;
|
|
88
|
+
/**
|
|
89
|
+
* Check if tracker is ready
|
|
90
|
+
*/
|
|
91
|
+
isReady(): boolean;
|
|
92
|
+
/**
|
|
93
|
+
* Navigate to next step in funnel using SDK's funnel client
|
|
94
|
+
*/
|
|
95
|
+
navigate(options: NavigateOptions): Promise<{
|
|
96
|
+
url: string;
|
|
97
|
+
} | null>;
|
|
98
|
+
/**
|
|
99
|
+
* Get customer ID (for identifying the user)
|
|
100
|
+
*/
|
|
101
|
+
getCustomerId(): string | null;
|
|
102
|
+
/**
|
|
103
|
+
* Get funnel session ID
|
|
104
|
+
*/
|
|
105
|
+
getFunnelSessionId(): string | null;
|
|
106
|
+
/**
|
|
107
|
+
* Build a URL with funnel context parameters
|
|
108
|
+
* Useful for linking to other external pages while preserving session
|
|
109
|
+
*/
|
|
110
|
+
buildUrl(baseUrl: string, additionalParams?: Record<string, string>): string;
|
|
111
|
+
/**
|
|
112
|
+
* Get the underlying SDK client (for advanced usage)
|
|
113
|
+
*/
|
|
114
|
+
getClient(): TagadaClient | null;
|
|
115
|
+
private waitForClientReady;
|
|
116
|
+
private initializeFunnel;
|
|
117
|
+
}
|
|
118
|
+
declare const TagadaTracker: TagadaExternalTracker;
|
|
119
|
+
export { TagadaTracker, TagadaExternalTracker };
|