@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.
Files changed (49) hide show
  1. package/build-cdn.js +113 -0
  2. package/dist/config/basisTheory.d.ts +26 -0
  3. package/dist/config/basisTheory.js +29 -0
  4. package/dist/external-tracker.js +5072 -0
  5. package/dist/external-tracker.min.js +11 -0
  6. package/dist/external-tracker.min.js.map +7 -0
  7. package/dist/react/config/payment.d.ts +8 -8
  8. package/dist/react/config/payment.js +17 -21
  9. package/dist/react/hooks/useApplePay.js +1 -1
  10. package/dist/react/hooks/usePayment.js +1 -3
  11. package/dist/react/hooks/useThreeds.js +2 -2
  12. package/dist/v2/core/client.d.ts +30 -3
  13. package/dist/v2/core/client.js +326 -8
  14. package/dist/v2/core/config/environment.d.ts +16 -3
  15. package/dist/v2/core/config/environment.js +72 -3
  16. package/dist/v2/core/funnelClient.d.ts +4 -0
  17. package/dist/v2/core/funnelClient.js +106 -4
  18. package/dist/v2/core/resources/funnel.d.ts +22 -0
  19. package/dist/v2/core/resources/offers.d.ts +64 -3
  20. package/dist/v2/core/resources/offers.js +112 -10
  21. package/dist/v2/core/resources/postPurchases.js +4 -1
  22. package/dist/v2/core/utils/configHotReload.d.ts +39 -0
  23. package/dist/v2/core/utils/configHotReload.js +75 -0
  24. package/dist/v2/core/utils/eventBus.d.ts +11 -0
  25. package/dist/v2/core/utils/eventBus.js +34 -0
  26. package/dist/v2/core/utils/pluginConfig.d.ts +14 -5
  27. package/dist/v2/core/utils/pluginConfig.js +74 -59
  28. package/dist/v2/core/utils/previewMode.d.ts +114 -0
  29. package/dist/v2/core/utils/previewMode.js +379 -0
  30. package/dist/v2/core/utils/sessionStorage.d.ts +5 -0
  31. package/dist/v2/core/utils/sessionStorage.js +22 -0
  32. package/dist/v2/index.d.ts +4 -1
  33. package/dist/v2/index.js +3 -1
  34. package/dist/v2/react/components/DebugDrawer.js +68 -46
  35. package/dist/v2/react/hooks/useOfferQuery.js +50 -17
  36. package/dist/v2/react/hooks/usePaymentQuery.js +1 -3
  37. package/dist/v2/react/hooks/usePreviewOffer.d.ts +84 -0
  38. package/dist/v2/react/hooks/usePreviewOffer.js +290 -0
  39. package/dist/v2/react/hooks/useThreeds.js +2 -2
  40. package/dist/v2/react/index.d.ts +2 -0
  41. package/dist/v2/react/index.js +1 -0
  42. package/dist/v2/react/providers/TagadaProvider.js +49 -32
  43. package/dist/v2/standalone/external-tracker.d.ts +119 -0
  44. package/dist/v2/standalone/external-tracker.js +260 -0
  45. package/dist/v2/standalone/index.d.ts +2 -0
  46. package/dist/v2/standalone/index.js +6 -0
  47. package/package.json +11 -3
  48. package/dist/v2/react/hooks/useOffersQuery.d.ts +0 -12
  49. 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
- * - Production domains production API
8
- * - Dev/staging domains development API
9
- * - Localhost/local IPs → local API (with optional override via window.__TAGADA_ENV__)
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...', { existingSessionId, effectiveFunnelId });
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: effectiveFunnelId,
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
- ...(ctx.static || {})
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: string, customerId?: string): Promise<{
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
- const response = await this.apiClient.get(`/api/v1/stores/${storeId}/offers`);
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
- const allOffers = await this.getOffers(storeId);
28
- return allOffers.filter(offer => offerIds.includes(offer.id));
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
- const response = await this.apiClient.post('/api/v1/checkout/offer/init', {
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
- orderId,
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: false,
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: false,
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: false,
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;