@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
|
@@ -1,12 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get cookie value by name
|
|
3
|
+
*/
|
|
4
|
+
function getCookie(name) {
|
|
5
|
+
if (typeof document === 'undefined')
|
|
6
|
+
return null;
|
|
7
|
+
const value = `; ${document.cookie}`;
|
|
8
|
+
const parts = value.split(`; ${name}=`);
|
|
9
|
+
if (parts.length === 2)
|
|
10
|
+
return parts.pop()?.split(';').shift() || null;
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
1
13
|
/**
|
|
2
14
|
* ⚠️ IMPORTANT: Runtime Environment Detection
|
|
3
15
|
*
|
|
4
16
|
* This SDK uses RUNTIME hostname detection, NOT build-time environment variables.
|
|
5
17
|
* This ensures the SDK always connects to the correct API based on where it's deployed.
|
|
6
18
|
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
19
|
+
* Environment detection priority (highest to lowest):
|
|
20
|
+
* 1. **tagadaClientEnv** - Explicit override via URL param, localStorage, or cookie
|
|
21
|
+
* Example: ?tagadaClientEnv=production
|
|
22
|
+
* 2. **Production domains** → production API (app.tagadapay.com)
|
|
23
|
+
* 3. **Dev/staging domains** → development API (app.tagadapay.dev, vercel.app, etc.)
|
|
24
|
+
* 4. **Localhost/local IPs** → local API (localhost, 127.0.0.1, etc.)
|
|
25
|
+
* - Can be overridden via window.__TAGADA_ENV__.TAGADA_ENVIRONMENT
|
|
26
|
+
* 5. **Default fallback** → production API (safest for unknown domains)
|
|
10
27
|
*
|
|
11
28
|
* Build-time .env variables (VITE_*, REACT_APP_*, NEXT_PUBLIC_*) are IGNORED
|
|
12
29
|
* to prevent incorrect API connections when plugins are deployed to different environments.
|
|
@@ -66,6 +83,8 @@ export const ENVIRONMENT_CONFIGS = {
|
|
|
66
83
|
};
|
|
67
84
|
/**
|
|
68
85
|
* Get the environment configuration based on the current environment
|
|
86
|
+
*
|
|
87
|
+
* Checks for custom base URL override via tagadaClientBaseUrl parameter
|
|
69
88
|
*/
|
|
70
89
|
export function getEnvironmentConfig(environment = 'local') {
|
|
71
90
|
const apiConfig = ENVIRONMENT_CONFIGS[environment];
|
|
@@ -76,6 +95,31 @@ export function getEnvironmentConfig(environment = 'local') {
|
|
|
76
95
|
apiConfig: ENVIRONMENT_CONFIGS.local,
|
|
77
96
|
};
|
|
78
97
|
}
|
|
98
|
+
// 🎯 Check for custom base URL override (URL > localStorage > cookie)
|
|
99
|
+
let customBaseUrl = null;
|
|
100
|
+
if (typeof window !== 'undefined') {
|
|
101
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
102
|
+
customBaseUrl = urlParams.get('tagadaClientBaseUrl');
|
|
103
|
+
if (!customBaseUrl) {
|
|
104
|
+
try {
|
|
105
|
+
customBaseUrl = localStorage.getItem('tgd_client_base_url') || getCookie('tgd_client_base_url');
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Storage not available
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// If custom base URL is set, override the apiConfig.baseUrl
|
|
113
|
+
if (customBaseUrl) {
|
|
114
|
+
console.log(`[SDK] Using custom API base URL override: ${customBaseUrl}`);
|
|
115
|
+
return {
|
|
116
|
+
environment,
|
|
117
|
+
apiConfig: {
|
|
118
|
+
...apiConfig,
|
|
119
|
+
baseUrl: customBaseUrl,
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
}
|
|
79
123
|
return {
|
|
80
124
|
environment,
|
|
81
125
|
apiConfig,
|
|
@@ -102,12 +146,37 @@ export function getEndpointUrl(config, category, endpoint) {
|
|
|
102
146
|
* Auto-detect environment based on hostname and URL patterns at RUNTIME
|
|
103
147
|
* ⚠️ IMPORTANT: Ignores build-time .env variables to ensure correct detection in all environments
|
|
104
148
|
* .env variables are ONLY used for local development via window.__TAGADA_ENV__
|
|
149
|
+
*
|
|
150
|
+
* Priority (highest to lowest):
|
|
151
|
+
* 1. tagadaClientEnv - Explicit override via URL/localStorage/cookie
|
|
152
|
+
* 2. __TAGADA_ENV__ - Local development override
|
|
153
|
+
* 3. Hostname-based detection - Production/staging domains
|
|
154
|
+
* 4. Default fallback - Production (safest)
|
|
105
155
|
*/
|
|
106
156
|
export function detectEnvironment() {
|
|
107
157
|
// Check if we're in browser
|
|
108
158
|
if (typeof window === 'undefined') {
|
|
109
159
|
return 'local'; // SSR fallback
|
|
110
160
|
}
|
|
161
|
+
// 🎯 PRIORITY 1: Check for explicit tagadaClientEnv override (URL > localStorage > cookie)
|
|
162
|
+
// This allows forcing environment regardless of hostname
|
|
163
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
164
|
+
const urlEnv = urlParams.get('tagadaClientEnv');
|
|
165
|
+
if (urlEnv && (urlEnv === 'production' || urlEnv === 'development' || urlEnv === 'local')) {
|
|
166
|
+
console.log(`[SDK] Using explicit environment override: ${urlEnv}`);
|
|
167
|
+
return urlEnv;
|
|
168
|
+
}
|
|
169
|
+
// Check localStorage/cookie for persisted override
|
|
170
|
+
try {
|
|
171
|
+
const storageEnv = localStorage.getItem('tgd_client_env') || getCookie('tgd_client_env');
|
|
172
|
+
if (storageEnv && (storageEnv === 'production' || storageEnv === 'development' || storageEnv === 'local')) {
|
|
173
|
+
console.log(`[SDK] Using persisted environment override: ${storageEnv}`);
|
|
174
|
+
return storageEnv;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Storage not available
|
|
179
|
+
}
|
|
111
180
|
const hostname = window.location.hostname;
|
|
112
181
|
const href = window.location.href;
|
|
113
182
|
// 1. Check for LOCAL environment first (highest priority for dev)
|
|
@@ -66,6 +66,10 @@ export declare class FunnelClient {
|
|
|
66
66
|
* Navigate
|
|
67
67
|
*/
|
|
68
68
|
navigate(event: FunnelAction): Promise<FunnelNavigationResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Go to a specific step (direct navigation)
|
|
71
|
+
*/
|
|
72
|
+
goToStep(stepId: string): Promise<FunnelNavigationResult>;
|
|
69
73
|
/**
|
|
70
74
|
* Refresh session data
|
|
71
75
|
*/
|
|
@@ -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;
|