@tagadapay/plugin-sdk 3.0.3 → 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 +30 -3
- package/dist/v2/core/client.js +219 -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/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
|
@@ -1,7 +1,62 @@
|
|
|
1
|
-
import { FunnelResource } from './resources/funnel';
|
|
1
|
+
import { FunnelResource, FunnelActionType } from './resources/funnel';
|
|
2
2
|
import { EventDispatcher } from './utils/eventDispatcher';
|
|
3
3
|
import { getFunnelSessionCookie, setFunnelSessionCookie } from './utils/sessionStorage';
|
|
4
4
|
import { detectEnvironment } from './config/environment';
|
|
5
|
+
import { getSDKParams } from './utils/previewMode';
|
|
6
|
+
/**
|
|
7
|
+
* Get the funnel ID from the injected HTML
|
|
8
|
+
* Returns undefined if not available
|
|
9
|
+
*/
|
|
10
|
+
function getAssignedFunnelId() {
|
|
11
|
+
if (typeof window === 'undefined')
|
|
12
|
+
return undefined;
|
|
13
|
+
// Method 1: Window variable (preferred - synchronous, fast)
|
|
14
|
+
if (window.__TGD_FUNNEL_ID__) {
|
|
15
|
+
return window.__TGD_FUNNEL_ID__;
|
|
16
|
+
}
|
|
17
|
+
// Method 2: Meta tag fallback
|
|
18
|
+
if (typeof document !== 'undefined') {
|
|
19
|
+
const meta = document.querySelector('meta[name="x-funnel-id"]');
|
|
20
|
+
return meta?.getAttribute('content') || undefined;
|
|
21
|
+
}
|
|
22
|
+
return undefined;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Get the assigned A/B test variant ID from the injected HTML
|
|
26
|
+
* Returns undefined if not in an A/B test
|
|
27
|
+
*/
|
|
28
|
+
function getAssignedFunnelVariant() {
|
|
29
|
+
if (typeof window === 'undefined')
|
|
30
|
+
return undefined;
|
|
31
|
+
// Method 1: Window variable (preferred - synchronous, fast)
|
|
32
|
+
if (window.__TGD_FUNNEL_VARIANT_ID__) {
|
|
33
|
+
return window.__TGD_FUNNEL_VARIANT_ID__;
|
|
34
|
+
}
|
|
35
|
+
// Method 2: Meta tag fallback
|
|
36
|
+
if (typeof document !== 'undefined') {
|
|
37
|
+
const meta = document.querySelector('meta[name="x-funnel-variant-id"]');
|
|
38
|
+
return meta?.getAttribute('content') || undefined;
|
|
39
|
+
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the funnel step ID from the injected HTML
|
|
44
|
+
* Returns undefined if not available
|
|
45
|
+
*/
|
|
46
|
+
function getAssignedFunnelStep() {
|
|
47
|
+
if (typeof window === 'undefined')
|
|
48
|
+
return undefined;
|
|
49
|
+
// Method 1: Window variable (preferred - synchronous, fast)
|
|
50
|
+
if (window.__TGD_FUNNEL_STEP_ID__) {
|
|
51
|
+
return window.__TGD_FUNNEL_STEP_ID__;
|
|
52
|
+
}
|
|
53
|
+
// Method 2: Meta tag fallback
|
|
54
|
+
if (typeof document !== 'undefined') {
|
|
55
|
+
const meta = document.querySelector('meta[name="x-funnel-step-id"]');
|
|
56
|
+
return meta?.getAttribute('content') || undefined;
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
5
60
|
export class FunnelClient {
|
|
6
61
|
constructor(config) {
|
|
7
62
|
this.eventDispatcher = new EventDispatcher();
|
|
@@ -60,8 +115,23 @@ export class FunnelClient {
|
|
|
60
115
|
if (!existingSessionId) {
|
|
61
116
|
existingSessionId = getFunnelSessionCookie() || null;
|
|
62
117
|
}
|
|
118
|
+
// 🎯 Read funnel tracking data from injected HTML
|
|
119
|
+
const injectedFunnelId = getAssignedFunnelId(); // Funnel ID from server
|
|
120
|
+
const funnelVariantId = getAssignedFunnelVariant(); // A/B test variant ID
|
|
121
|
+
const funnelStepId = getAssignedFunnelStep(); // Current step ID
|
|
122
|
+
// 🎯 Get SDK override parameters (draft, funnelTracking, etc.)
|
|
123
|
+
const sdkParams = getSDKParams();
|
|
124
|
+
// Prefer injected funnelId over URL/prop funnelId (more reliable)
|
|
125
|
+
const finalFunnelId = injectedFunnelId || effectiveFunnelId;
|
|
63
126
|
if (this.config.debugMode) {
|
|
64
|
-
console.log('🚀 [FunnelClient] Auto-initializing...', {
|
|
127
|
+
console.log('🚀 [FunnelClient] Auto-initializing...', {
|
|
128
|
+
existingSessionId,
|
|
129
|
+
effectiveFunnelId: finalFunnelId,
|
|
130
|
+
funnelVariantId, // 🎯 Log variant ID for debugging
|
|
131
|
+
funnelStepId, // 🎯 Log step ID for debugging
|
|
132
|
+
draft: sdkParams.draft, // 🎯 Log draft mode
|
|
133
|
+
funnelTracking: sdkParams.funnelTracking, // 🎯 Log tracking flag
|
|
134
|
+
});
|
|
65
135
|
}
|
|
66
136
|
// Note: We proceed even without funnelId/sessionId - the backend will create a new anonymous session if needed
|
|
67
137
|
const response = await this.resource.initialize({
|
|
@@ -71,9 +141,13 @@ export class FunnelClient {
|
|
|
71
141
|
storeId: store.id,
|
|
72
142
|
accountId: store.accountId,
|
|
73
143
|
},
|
|
74
|
-
funnelId:
|
|
144
|
+
funnelId: finalFunnelId,
|
|
75
145
|
existingSessionId: existingSessionId || undefined,
|
|
76
146
|
currentUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
147
|
+
funnelVariantId, // 🎯 Pass A/B test variant ID to backend
|
|
148
|
+
funnelStepId, // 🎯 Pass step ID to backend
|
|
149
|
+
draft: sdkParams.draft, // 🎯 Pass draft mode explicitly (more robust than URL parsing)
|
|
150
|
+
funnelTracking: sdkParams.funnelTracking, // 🎯 Pass funnel tracking flag explicitly
|
|
77
151
|
});
|
|
78
152
|
if (response.success && response.context) {
|
|
79
153
|
const enriched = this.enrichContext(response.context);
|
|
@@ -102,6 +176,15 @@ export class FunnelClient {
|
|
|
102
176
|
async initialize(authSession, store, funnelId, entryStepId) {
|
|
103
177
|
this.updateState({ isLoading: true, error: null });
|
|
104
178
|
try {
|
|
179
|
+
// 🎯 Read A/B test variant ID and step ID from injected HTML
|
|
180
|
+
const funnelVariantId = getAssignedFunnelVariant();
|
|
181
|
+
const funnelStepId = getAssignedFunnelStep();
|
|
182
|
+
if (this.config.debugMode) {
|
|
183
|
+
if (funnelVariantId)
|
|
184
|
+
console.log('🎯 [FunnelClient] Detected A/B test variant:', funnelVariantId);
|
|
185
|
+
if (funnelStepId)
|
|
186
|
+
console.log('🎯 [FunnelClient] Detected step ID:', funnelStepId);
|
|
187
|
+
}
|
|
105
188
|
const response = await this.resource.initialize({
|
|
106
189
|
cmsSession: {
|
|
107
190
|
customerId: authSession.customerId,
|
|
@@ -112,6 +195,8 @@ export class FunnelClient {
|
|
|
112
195
|
funnelId: funnelId,
|
|
113
196
|
entryStepId,
|
|
114
197
|
currentUrl: typeof window !== 'undefined' ? window.location.href : undefined,
|
|
198
|
+
funnelVariantId, // 🎯 Pass A/B test variant ID to backend
|
|
199
|
+
funnelStepId, // 🎯 Pass step ID to backend
|
|
115
200
|
});
|
|
116
201
|
if (response.success && response.context) {
|
|
117
202
|
const enriched = this.enrichContext(response.context);
|
|
@@ -163,6 +248,15 @@ export class FunnelClient {
|
|
|
163
248
|
throw err;
|
|
164
249
|
}
|
|
165
250
|
}
|
|
251
|
+
/**
|
|
252
|
+
* Go to a specific step (direct navigation)
|
|
253
|
+
*/
|
|
254
|
+
async goToStep(stepId) {
|
|
255
|
+
return this.navigate({
|
|
256
|
+
type: FunnelActionType.DIRECT_NAVIGATION,
|
|
257
|
+
data: { targetStepId: stepId },
|
|
258
|
+
});
|
|
259
|
+
}
|
|
166
260
|
/**
|
|
167
261
|
* Refresh session data
|
|
168
262
|
*/
|
|
@@ -241,11 +335,19 @@ export class FunnelClient {
|
|
|
241
335
|
const localResources = this.config.pluginConfig?.staticResources || {};
|
|
242
336
|
if (Object.keys(localResources).length === 0)
|
|
243
337
|
return ctx;
|
|
338
|
+
// 🎯 Check if context already has the same static resources
|
|
339
|
+
// This prevents creating new objects unnecessarily, which would trigger React re-renders
|
|
340
|
+
const existingStatic = ctx.static || {};
|
|
341
|
+
const hasAllResources = Object.keys(localResources).every(key => existingStatic[key] === localResources[key]);
|
|
342
|
+
// If context already has all static resources with same references, don't recreate
|
|
343
|
+
if (hasAllResources && Object.keys(existingStatic).length === Object.keys(localResources).length) {
|
|
344
|
+
return ctx;
|
|
345
|
+
}
|
|
244
346
|
return {
|
|
245
347
|
...ctx,
|
|
246
348
|
static: {
|
|
247
349
|
...localResources,
|
|
248
|
-
...
|
|
350
|
+
...existingStatic
|
|
249
351
|
}
|
|
250
352
|
};
|
|
251
353
|
}
|
|
@@ -438,6 +438,28 @@ export interface FunnelInitializeRequest {
|
|
|
438
438
|
* @example '/checkout', 'https://store.com/payment'
|
|
439
439
|
*/
|
|
440
440
|
currentUrl?: string;
|
|
441
|
+
/**
|
|
442
|
+
* A/B test variant ID extracted from injected HTML
|
|
443
|
+
* Used to track which variant the user is seeing in an A/B test
|
|
444
|
+
* @example 'step_1765015842897_variant_1'
|
|
445
|
+
*/
|
|
446
|
+
funnelVariantId?: string;
|
|
447
|
+
/**
|
|
448
|
+
* Funnel step ID extracted from injected HTML
|
|
449
|
+
* Used to track which step the user is on
|
|
450
|
+
* @example 'step_1765015842897'
|
|
451
|
+
*/
|
|
452
|
+
funnelStepId?: string;
|
|
453
|
+
/**
|
|
454
|
+
* 🎯 Draft/preview mode flag (explicit from SDK, more robust than URL parsing)
|
|
455
|
+
* When true, uses draft/staging data instead of production data
|
|
456
|
+
*/
|
|
457
|
+
draft?: boolean;
|
|
458
|
+
/**
|
|
459
|
+
* 🎯 Funnel tracking enable/disable flag (explicit from SDK, more robust than URL parsing)
|
|
460
|
+
* When false, disables all funnel tracking events (useful for iframed previews in config editor)
|
|
461
|
+
*/
|
|
462
|
+
funnelTracking?: boolean;
|
|
441
463
|
}
|
|
442
464
|
export interface FunnelInitializeResponse {
|
|
443
465
|
success: boolean;
|
|
@@ -204,16 +204,77 @@ export declare class OffersResource {
|
|
|
204
204
|
getOfferById(offerId: string): Promise<Offer>;
|
|
205
205
|
/**
|
|
206
206
|
* Get offers for a store
|
|
207
|
+
* @param storeId - Store ID
|
|
208
|
+
* @param offerIds - Optional array of offer IDs to filter by
|
|
207
209
|
*/
|
|
208
|
-
getOffers(storeId: string): Promise<Offer[]>;
|
|
210
|
+
getOffers(storeId: string, offerIds?: string[]): Promise<Offer[]>;
|
|
209
211
|
/**
|
|
210
|
-
* Get offers by IDs
|
|
212
|
+
* Get offers by IDs (now uses backend filtering for better performance)
|
|
211
213
|
*/
|
|
212
214
|
getOffersByIds(storeId: string, offerIds: string[]): Promise<Offer[]>;
|
|
215
|
+
/**
|
|
216
|
+
* Preview an offer with calculated summary (no checkout session creation)
|
|
217
|
+
* @param offerId - Offer ID
|
|
218
|
+
* @param currency - Currency code
|
|
219
|
+
* @param lineItems - Optional line items with variant/quantity selections
|
|
220
|
+
* - Use lineItemId for precise updates (same product, different variants)
|
|
221
|
+
* - Use productId for simple updates (all items with this product)
|
|
222
|
+
*/
|
|
223
|
+
previewOffer(offerId: string, currency?: string, lineItems?: Array<{
|
|
224
|
+
lineItemId?: string;
|
|
225
|
+
productId?: string;
|
|
226
|
+
variantId: string;
|
|
227
|
+
quantity: number;
|
|
228
|
+
}>): Promise<any>;
|
|
229
|
+
/**
|
|
230
|
+
* Preview and pay for an offer with variant selections
|
|
231
|
+
* Combines preview + checkout session creation in one call
|
|
232
|
+
* @param offerId - Offer ID
|
|
233
|
+
* @param currency - Currency code
|
|
234
|
+
* @param lineItems - Line items with variant/quantity selections
|
|
235
|
+
* @param returnUrl - Optional return URL for checkout
|
|
236
|
+
* @param mainOrderId - Optional main order ID (for upsells)
|
|
237
|
+
*/
|
|
238
|
+
payPreviewedOffer(offerId: string, currency?: string, lineItems?: Array<{
|
|
239
|
+
lineItemId?: string;
|
|
240
|
+
productId?: string;
|
|
241
|
+
variantId: string;
|
|
242
|
+
quantity: number;
|
|
243
|
+
}>, returnUrl?: string, mainOrderId?: string): Promise<{
|
|
244
|
+
preview: any;
|
|
245
|
+
checkout: {
|
|
246
|
+
checkoutUrl: string;
|
|
247
|
+
customerId?: string;
|
|
248
|
+
checkoutSessionId?: string;
|
|
249
|
+
};
|
|
250
|
+
}>;
|
|
251
|
+
/**
|
|
252
|
+
* Create checkout session from previewed offer WITHOUT paying
|
|
253
|
+
* Used for landing pages to prepare checkout before actual payment
|
|
254
|
+
* @param offerId - Offer ID
|
|
255
|
+
* @param currency - Currency code
|
|
256
|
+
* @param lineItems - Line items with variant/quantity selections
|
|
257
|
+
* @param returnUrl - Optional return URL for checkout
|
|
258
|
+
* @param mainOrderId - Optional main order ID
|
|
259
|
+
*/
|
|
260
|
+
toCheckout(offerId: string, currency?: string, lineItems?: Array<{
|
|
261
|
+
lineItemId?: string;
|
|
262
|
+
productId?: string;
|
|
263
|
+
variantId: string;
|
|
264
|
+
quantity: number;
|
|
265
|
+
}>, returnUrl?: string, mainOrderId?: string): Promise<{
|
|
266
|
+
checkoutSessionId?: string;
|
|
267
|
+
checkoutToken?: string;
|
|
268
|
+
customerId?: string;
|
|
269
|
+
checkoutUrl: string;
|
|
270
|
+
}>;
|
|
213
271
|
/**
|
|
214
272
|
* Initialize checkout session for an offer
|
|
273
|
+
* @param offerId - Offer ID (required)
|
|
274
|
+
* @param orderId - Order ID (optional - used for post-purchase upsells)
|
|
275
|
+
* @param customerId - Customer ID (optional)
|
|
215
276
|
*/
|
|
216
|
-
initCheckoutSession(offerId: string, orderId
|
|
277
|
+
initCheckoutSession(offerId: string, orderId?: string, customerId?: string): Promise<{
|
|
217
278
|
checkoutSessionId: string;
|
|
218
279
|
}>;
|
|
219
280
|
/**
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Offers Resource Client
|
|
3
3
|
* Axios-based API client for offers endpoints
|
|
4
4
|
*/
|
|
5
|
+
import { isDraftMode } from '../utils/previewMode';
|
|
5
6
|
export class OffersResource {
|
|
6
7
|
constructor(apiClient) {
|
|
7
8
|
this.apiClient = apiClient;
|
|
@@ -15,28 +16,125 @@ export class OffersResource {
|
|
|
15
16
|
}
|
|
16
17
|
/**
|
|
17
18
|
* Get offers for a store
|
|
19
|
+
* @param storeId - Store ID
|
|
20
|
+
* @param offerIds - Optional array of offer IDs to filter by
|
|
18
21
|
*/
|
|
19
|
-
async getOffers(storeId) {
|
|
20
|
-
|
|
22
|
+
async getOffers(storeId, offerIds) {
|
|
23
|
+
let url = `/api/v1/stores/${storeId}/offers`;
|
|
24
|
+
// Add ids query parameter if provided (backend filtering for performance)
|
|
25
|
+
if (offerIds && offerIds.length > 0) {
|
|
26
|
+
const idsParam = offerIds.map(id => `ids=${encodeURIComponent(id)}`).join('&');
|
|
27
|
+
url += `?${idsParam}`;
|
|
28
|
+
}
|
|
29
|
+
const response = await this.apiClient.get(url);
|
|
21
30
|
return response.offers || [];
|
|
22
31
|
}
|
|
23
32
|
/**
|
|
24
|
-
* Get offers by IDs
|
|
33
|
+
* Get offers by IDs (now uses backend filtering for better performance)
|
|
25
34
|
*/
|
|
26
35
|
async getOffersByIds(storeId, offerIds) {
|
|
27
|
-
|
|
28
|
-
return
|
|
36
|
+
// Use backend filtering instead of fetching all and filtering client-side
|
|
37
|
+
return await this.getOffers(storeId, offerIds);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Preview an offer with calculated summary (no checkout session creation)
|
|
41
|
+
* @param offerId - Offer ID
|
|
42
|
+
* @param currency - Currency code
|
|
43
|
+
* @param lineItems - Optional line items with variant/quantity selections
|
|
44
|
+
* - Use lineItemId for precise updates (same product, different variants)
|
|
45
|
+
* - Use productId for simple updates (all items with this product)
|
|
46
|
+
*/
|
|
47
|
+
async previewOffer(offerId, currency = 'USD', lineItems) {
|
|
48
|
+
console.log('📡 [OffersResource] Calling preview API:', {
|
|
49
|
+
offerId,
|
|
50
|
+
currency,
|
|
51
|
+
lineItems,
|
|
52
|
+
endpoint: `/api/v1/offers/${offerId}/preview`,
|
|
53
|
+
});
|
|
54
|
+
const response = await this.apiClient.post(`/api/v1/offers/${offerId}/preview`, {
|
|
55
|
+
offerId,
|
|
56
|
+
currency,
|
|
57
|
+
lineItems,
|
|
58
|
+
});
|
|
59
|
+
console.log('📥 [OffersResource] Preview API response:', response);
|
|
60
|
+
return response;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Preview and pay for an offer with variant selections
|
|
64
|
+
* Combines preview + checkout session creation in one call
|
|
65
|
+
* @param offerId - Offer ID
|
|
66
|
+
* @param currency - Currency code
|
|
67
|
+
* @param lineItems - Line items with variant/quantity selections
|
|
68
|
+
* @param returnUrl - Optional return URL for checkout
|
|
69
|
+
* @param mainOrderId - Optional main order ID (for upsells)
|
|
70
|
+
*/
|
|
71
|
+
async payPreviewedOffer(offerId, currency = 'USD', lineItems, returnUrl, mainOrderId) {
|
|
72
|
+
console.log('💳 [OffersResource] Calling pay-preview API:', {
|
|
73
|
+
offerId,
|
|
74
|
+
currency,
|
|
75
|
+
lineItems,
|
|
76
|
+
returnUrl,
|
|
77
|
+
mainOrderId,
|
|
78
|
+
endpoint: `/api/v1/offers/${offerId}/pay-preview`,
|
|
79
|
+
});
|
|
80
|
+
const response = await this.apiClient.post(`/api/v1/offers/${offerId}/pay-preview`, {
|
|
81
|
+
offerId,
|
|
82
|
+
currency,
|
|
83
|
+
lineItems,
|
|
84
|
+
returnUrl: returnUrl || (typeof window !== 'undefined' ? window.location.href : ''),
|
|
85
|
+
mainOrderId,
|
|
86
|
+
});
|
|
87
|
+
console.log('📥 [OffersResource] Pay-preview API response:', response);
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Create checkout session from previewed offer WITHOUT paying
|
|
92
|
+
* Used for landing pages to prepare checkout before actual payment
|
|
93
|
+
* @param offerId - Offer ID
|
|
94
|
+
* @param currency - Currency code
|
|
95
|
+
* @param lineItems - Line items with variant/quantity selections
|
|
96
|
+
* @param returnUrl - Optional return URL for checkout
|
|
97
|
+
* @param mainOrderId - Optional main order ID
|
|
98
|
+
*/
|
|
99
|
+
async toCheckout(offerId, currency = 'USD', lineItems, returnUrl, mainOrderId) {
|
|
100
|
+
console.log('🛒 [OffersResource] Calling to-checkout API:', {
|
|
101
|
+
offerId,
|
|
102
|
+
currency,
|
|
103
|
+
lineItems,
|
|
104
|
+
returnUrl,
|
|
105
|
+
mainOrderId,
|
|
106
|
+
endpoint: `/api/v1/offers/${offerId}/to-checkout`,
|
|
107
|
+
});
|
|
108
|
+
const response = await this.apiClient.post(`/api/v1/offers/${offerId}/to-checkout`, {
|
|
109
|
+
offerId,
|
|
110
|
+
currency,
|
|
111
|
+
lineItems,
|
|
112
|
+
returnUrl: returnUrl || (typeof window !== 'undefined' ? window.location.href : ''),
|
|
113
|
+
mainOrderId,
|
|
114
|
+
});
|
|
115
|
+
console.log('📥 [OffersResource] To-checkout API response:', response);
|
|
116
|
+
return response;
|
|
29
117
|
}
|
|
30
118
|
/**
|
|
31
119
|
* Initialize checkout session for an offer
|
|
120
|
+
* @param offerId - Offer ID (required)
|
|
121
|
+
* @param orderId - Order ID (optional - used for post-purchase upsells)
|
|
122
|
+
* @param customerId - Customer ID (optional)
|
|
32
123
|
*/
|
|
33
124
|
async initCheckoutSession(offerId, orderId, customerId) {
|
|
34
|
-
|
|
125
|
+
// 🎯 Check draft mode from URL, localStorage, or cookie
|
|
126
|
+
const draft = isDraftMode();
|
|
127
|
+
const payload = {
|
|
35
128
|
offerId,
|
|
36
129
|
returnUrl: typeof window !== 'undefined' ? window.location.href : '',
|
|
37
130
|
customerId: customerId || '',
|
|
38
|
-
|
|
39
|
-
}
|
|
131
|
+
draft, // 🎯 Pass draft mode
|
|
132
|
+
};
|
|
133
|
+
// Only include orderId if provided (used for post-purchase upsells)
|
|
134
|
+
if (orderId) {
|
|
135
|
+
payload.orderId = orderId;
|
|
136
|
+
}
|
|
137
|
+
const response = await this.apiClient.post('/api/v1/checkout/offer/init', payload);
|
|
40
138
|
return { checkoutSessionId: response.checkoutSessionId };
|
|
41
139
|
}
|
|
42
140
|
/**
|
|
@@ -52,9 +150,11 @@ export class OffersResource {
|
|
|
52
150
|
* Pay with checkout session
|
|
53
151
|
*/
|
|
54
152
|
async payWithCheckoutSession(checkoutSessionId, orderId) {
|
|
153
|
+
// 🎯 Check draft mode from URL, localStorage, or cookie
|
|
154
|
+
const draft = isDraftMode();
|
|
55
155
|
await this.apiClient.post(`/api/v1/checkout-sessions/${checkoutSessionId}/pay`, {
|
|
56
156
|
checkoutSessionId,
|
|
57
|
-
draft
|
|
157
|
+
draft, // 🎯 Use dynamic draft mode instead of hardcoded false
|
|
58
158
|
returnUrl: typeof window !== 'undefined' ? window.location.href : '',
|
|
59
159
|
metadata: {
|
|
60
160
|
comingFromPostPurchase: true,
|
|
@@ -68,9 +168,11 @@ export class OffersResource {
|
|
|
68
168
|
* Pay for an offer directly
|
|
69
169
|
*/
|
|
70
170
|
async payOffer(offerId, orderId) {
|
|
171
|
+
// 🎯 Check draft mode from URL, localStorage, or cookie
|
|
172
|
+
const draft = isDraftMode();
|
|
71
173
|
return this.apiClient.post(`/api/v1/offers/${offerId}/pay`, {
|
|
72
174
|
offerId,
|
|
73
|
-
draft
|
|
175
|
+
draft, // 🎯 Use dynamic draft mode instead of hardcoded false
|
|
74
176
|
returnUrl: typeof window !== 'undefined' ? window.location.href : '',
|
|
75
177
|
metadata: orderId ? {
|
|
76
178
|
comingFromPostPurchase: true,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Post Purchases Resource Client
|
|
3
3
|
* Axios-based API client for post-purchase endpoints
|
|
4
4
|
*/
|
|
5
|
+
import { isDraftMode } from '../utils/previewMode';
|
|
5
6
|
export class PostPurchasesResource {
|
|
6
7
|
constructor(apiClient) {
|
|
7
8
|
this.apiClient = apiClient;
|
|
@@ -85,9 +86,11 @@ export class PostPurchasesResource {
|
|
|
85
86
|
* Pay with a checkout session for a post-purchase offer
|
|
86
87
|
*/
|
|
87
88
|
async payWithCheckoutSession(checkoutSessionId, orderId) {
|
|
89
|
+
// 🎯 Check draft mode from URL, localStorage, or cookie
|
|
90
|
+
const draft = isDraftMode();
|
|
88
91
|
await this.apiClient.post(`/api/v1/checkout-sessions/${checkoutSessionId}/pay`, {
|
|
89
92
|
checkoutSessionId,
|
|
90
|
-
draft
|
|
93
|
+
draft, // 🎯 Use dynamic draft mode instead of hardcoded false
|
|
91
94
|
returnUrl: typeof window !== 'undefined' ? window.location.href : '',
|
|
92
95
|
metadata: {
|
|
93
96
|
comingFromPostPurchase: true,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Hot Reload Utilities
|
|
3
|
+
*
|
|
4
|
+
* Enables live config editing without page reload.
|
|
5
|
+
* Used by the config editor to push updates to preview iframes.
|
|
6
|
+
*/
|
|
7
|
+
export interface ConfigUpdateMessage {
|
|
8
|
+
type: 'TAGADAPAY_CONFIG_UPDATE';
|
|
9
|
+
config: Record<string, any>;
|
|
10
|
+
timestamp?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Send config update to target window (iframe, popup, etc.)
|
|
14
|
+
*
|
|
15
|
+
* @param targetWindow - Window to send update to (e.g., iframe.contentWindow)
|
|
16
|
+
* @param config - Updated config object
|
|
17
|
+
* @param targetOrigin - Target origin (default: '*' for same-origin)
|
|
18
|
+
*/
|
|
19
|
+
export declare function sendConfigUpdate(targetWindow: Window, config: Record<string, any>, targetOrigin?: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Send config update to all iframes on the page
|
|
22
|
+
* Useful for config editor with multiple preview panes
|
|
23
|
+
*
|
|
24
|
+
* @param config - Updated config object
|
|
25
|
+
* @param selector - CSS selector for iframes (default: 'iframe')
|
|
26
|
+
*/
|
|
27
|
+
export declare function broadcastConfigUpdate(config: Record<string, any>, selector?: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* Setup a listener for config updates
|
|
30
|
+
* Returns cleanup function
|
|
31
|
+
*
|
|
32
|
+
* @param callback - Function to call when config is updated
|
|
33
|
+
*/
|
|
34
|
+
export declare function onConfigUpdate(callback: (config: Record<string, any>) => void): () => void;
|
|
35
|
+
/**
|
|
36
|
+
* Debounce helper for config updates
|
|
37
|
+
* Prevents too many updates in quick succession
|
|
38
|
+
*/
|
|
39
|
+
export declare function debounceConfigUpdate(fn: (config: Record<string, any>) => void, delay?: number): (config: Record<string, any>) => void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Hot Reload Utilities
|
|
3
|
+
*
|
|
4
|
+
* Enables live config editing without page reload.
|
|
5
|
+
* Used by the config editor to push updates to preview iframes.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Send config update to target window (iframe, popup, etc.)
|
|
9
|
+
*
|
|
10
|
+
* @param targetWindow - Window to send update to (e.g., iframe.contentWindow)
|
|
11
|
+
* @param config - Updated config object
|
|
12
|
+
* @param targetOrigin - Target origin (default: '*' for same-origin)
|
|
13
|
+
*/
|
|
14
|
+
export function sendConfigUpdate(targetWindow, config, targetOrigin = '*') {
|
|
15
|
+
const message = {
|
|
16
|
+
type: 'TAGADAPAY_CONFIG_UPDATE',
|
|
17
|
+
config,
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
};
|
|
20
|
+
targetWindow.postMessage(message, targetOrigin);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Send config update to all iframes on the page
|
|
24
|
+
* Useful for config editor with multiple preview panes
|
|
25
|
+
*
|
|
26
|
+
* @param config - Updated config object
|
|
27
|
+
* @param selector - CSS selector for iframes (default: 'iframe')
|
|
28
|
+
*/
|
|
29
|
+
export function broadcastConfigUpdate(config, selector = 'iframe') {
|
|
30
|
+
if (typeof document === 'undefined')
|
|
31
|
+
return;
|
|
32
|
+
const iframes = document.querySelectorAll(selector);
|
|
33
|
+
iframes.forEach((iframe) => {
|
|
34
|
+
if (iframe.contentWindow) {
|
|
35
|
+
sendConfigUpdate(iframe.contentWindow, config);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
console.log(`[ConfigHotReload] Broadcasted config update to ${iframes.length} iframe(s)`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Setup a listener for config updates
|
|
42
|
+
* Returns cleanup function
|
|
43
|
+
*
|
|
44
|
+
* @param callback - Function to call when config is updated
|
|
45
|
+
*/
|
|
46
|
+
export function onConfigUpdate(callback) {
|
|
47
|
+
if (typeof window === 'undefined') {
|
|
48
|
+
return () => { };
|
|
49
|
+
}
|
|
50
|
+
const handleMessage = (event) => {
|
|
51
|
+
if (event.data?.type === 'TAGADAPAY_CONFIG_UPDATE') {
|
|
52
|
+
callback(event.data.config);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
window.addEventListener('message', handleMessage);
|
|
56
|
+
return () => {
|
|
57
|
+
window.removeEventListener('message', handleMessage);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Debounce helper for config updates
|
|
62
|
+
* Prevents too many updates in quick succession
|
|
63
|
+
*/
|
|
64
|
+
export function debounceConfigUpdate(fn, delay = 150) {
|
|
65
|
+
let timeoutId = null;
|
|
66
|
+
return (config) => {
|
|
67
|
+
if (timeoutId) {
|
|
68
|
+
clearTimeout(timeoutId);
|
|
69
|
+
}
|
|
70
|
+
timeoutId = setTimeout(() => {
|
|
71
|
+
fn(config);
|
|
72
|
+
timeoutId = null;
|
|
73
|
+
}, delay);
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Bus for handling domain events and coordination
|
|
3
|
+
*/
|
|
4
|
+
export type EventCallback<T = any> = (data: T) => void | Promise<void>;
|
|
5
|
+
export declare class EventBus {
|
|
6
|
+
private listeners;
|
|
7
|
+
on<T = any>(event: string, callback: EventCallback<T>): () => void;
|
|
8
|
+
off<T = any>(event: string, callback: EventCallback<T>): void;
|
|
9
|
+
emit<T = any>(event: string, data?: T): Promise<void>;
|
|
10
|
+
clear(): void;
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export class EventBus {
|
|
2
|
+
constructor() {
|
|
3
|
+
this.listeners = new Map();
|
|
4
|
+
}
|
|
5
|
+
on(event, callback) {
|
|
6
|
+
if (!this.listeners.has(event)) {
|
|
7
|
+
this.listeners.set(event, new Set());
|
|
8
|
+
}
|
|
9
|
+
this.listeners.get(event).add(callback);
|
|
10
|
+
return () => this.off(event, callback);
|
|
11
|
+
}
|
|
12
|
+
off(event, callback) {
|
|
13
|
+
if (this.listeners.has(event)) {
|
|
14
|
+
this.listeners.get(event).delete(callback);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
async emit(event, data) {
|
|
18
|
+
if (this.listeners.has(event)) {
|
|
19
|
+
const callbacks = Array.from(this.listeners.get(event));
|
|
20
|
+
await Promise.all(callbacks.map(cb => {
|
|
21
|
+
try {
|
|
22
|
+
return Promise.resolve(cb(data));
|
|
23
|
+
}
|
|
24
|
+
catch (e) {
|
|
25
|
+
console.error(`[EventBus] Error in listener for event "${event}":`, e);
|
|
26
|
+
return Promise.resolve();
|
|
27
|
+
}
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
clear() {
|
|
32
|
+
this.listeners.clear();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -15,7 +15,6 @@ export type PluginConfig<TConfig = Record<string, any>> = {
|
|
|
15
15
|
hidePriceId?: string;
|
|
16
16
|
upsellId?: string;
|
|
17
17
|
googleApiKey?: string;
|
|
18
|
-
branding?: any;
|
|
19
18
|
} & TConfig;
|
|
20
19
|
export type RawPluginConfig<TConfig = Record<string, any>> = {
|
|
21
20
|
storeId?: string;
|
|
@@ -25,7 +24,13 @@ export type RawPluginConfig<TConfig = Record<string, any>> = {
|
|
|
25
24
|
} & TConfig;
|
|
26
25
|
/**
|
|
27
26
|
* Core plugin config loading function
|
|
28
|
-
*
|
|
27
|
+
*
|
|
28
|
+
* PRIORITY ORDER (highest to lowest):
|
|
29
|
+
* 1. Injected meta tags (runtime config) - ALWAYS CHECK FIRST
|
|
30
|
+
* 2. Raw config parameter (programmatic override)
|
|
31
|
+
* 3. Environment variables (build-time config)
|
|
32
|
+
* 4. Local dev files (.local.json + config files)
|
|
33
|
+
* 5. Defaults
|
|
29
34
|
*/
|
|
30
35
|
export declare const loadPluginConfig: (configVariant?: string, rawConfig?: RawPluginConfig) => Promise<PluginConfig>;
|
|
31
36
|
/**
|
|
@@ -34,9 +39,13 @@ export declare const loadPluginConfig: (configVariant?: string, rawConfig?: RawP
|
|
|
34
39
|
*/
|
|
35
40
|
export declare function loadLocalConfig(configName?: string, defaultConfig?: any): Promise<Record<string, unknown> | null>;
|
|
36
41
|
/**
|
|
37
|
-
* Creates a RawPluginConfig
|
|
38
|
-
*
|
|
39
|
-
*
|
|
42
|
+
* Creates a RawPluginConfig from environment variables (build-time config)
|
|
43
|
+
*
|
|
44
|
+
* This should ONLY be used when:
|
|
45
|
+
* 1. Running in localhost development
|
|
46
|
+
* 2. Meta tags are not present (not a deployed plugin)
|
|
47
|
+
*
|
|
48
|
+
* @returns A RawPluginConfig object or undefined if not applicable
|
|
40
49
|
*/
|
|
41
50
|
export declare function createRawPluginConfig(): Promise<RawPluginConfig | undefined>;
|
|
42
51
|
export declare class PluginConfigUtils {
|