@tagadapay/plugin-sdk 3.0.12 → 3.0.14

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.
@@ -7,7 +7,9 @@ export interface ApiConfig {
7
7
  endpoints: {
8
8
  checkout: {
9
9
  sessionInit: string;
10
+ sessionInitAsync: string;
10
11
  sessionStatus: string;
12
+ asyncStatus: string;
11
13
  };
12
14
  customer: {
13
15
  profile: string;
@@ -101,6 +101,10 @@ export declare class TagadaClient {
101
101
  * Initialize token and session
102
102
  */
103
103
  private initializeToken;
104
+ /**
105
+ * Normal token initialization flow (no cross-domain handoff)
106
+ */
107
+ private fallbackToNormalFlow;
104
108
  /**
105
109
  * Set token and resolve waiting requests
106
110
  */
@@ -8,6 +8,7 @@ import { decodeJWTClient, isTokenExpired } from './utils/jwtDecoder';
8
8
  import { loadPluginConfig } from './utils/pluginConfig';
9
9
  import { handlePreviewMode, isDraftMode, setDraftMode } from './utils/previewMode';
10
10
  import { getClientToken, setClientToken } from './utils/tokenStorage';
11
+ import { shouldResolveAuthCode, resolveAuthHandoff } from './utils/authHandoff';
11
12
  export class TagadaClient {
12
13
  constructor(config = {}) {
13
14
  /**
@@ -25,9 +26,13 @@ export class TagadaClient {
25
26
  this.config = config;
26
27
  this.instanceId = Math.random().toString(36).substr(2, 9);
27
28
  this.boundHandleStorageChange = this.handleStorageChange.bind(this);
28
- if (this.config.debugMode) {
29
- console.log(`[TagadaClient ${this.instanceId}] Initializing...`);
30
- }
29
+ console.log(`[TagadaClient ${this.instanceId}] Initializing...`);
30
+ console.log(`[TagadaClient ${this.instanceId}] Config:`, {
31
+ debugMode: config.debugMode,
32
+ hasRawPluginConfig: !!config.rawPluginConfig,
33
+ rawPluginConfig: config.rawPluginConfig,
34
+ features: config.features,
35
+ });
31
36
  // Handle preview mode FIRST - clears state if needed
32
37
  // This ensures clean state when CRM previews pages
33
38
  const previewModeActive = handlePreviewMode(this.config.debugMode);
@@ -77,10 +82,14 @@ export class TagadaClient {
77
82
  isInitialized: false,
78
83
  isSessionInitialized: false,
79
84
  pluginConfig: { basePath: '/', config: {} },
80
- pluginConfigLoading: !config.rawPluginConfig,
85
+ pluginConfigLoading: true, // Always true - loadPluginConfig will process rawPluginConfig
81
86
  debugMode: config.debugMode ?? env !== 'production',
82
87
  token: null,
83
88
  };
89
+ console.log(`[TagadaClient ${this.instanceId}] Initial state:`, {
90
+ pluginConfigLoading: this.state.pluginConfigLoading,
91
+ hasRawPluginConfig: !!config.rawPluginConfig,
92
+ });
84
93
  // Initialize API Client
85
94
  this.apiClient = new ApiClient({
86
95
  baseURL: envConfig.apiConfig.baseUrl,
@@ -96,6 +105,9 @@ export class TagadaClient {
96
105
  pluginConfig: this.state.pluginConfig,
97
106
  environment: this.state.environment,
98
107
  autoRedirect: funnelConfig.autoRedirect,
108
+ // Pass funnelId and stepId from rawPluginConfig to enable config-based initialization
109
+ funnelId: config.rawPluginConfig?.funnelId,
110
+ stepId: config.rawPluginConfig?.stepId,
99
111
  });
100
112
  }
101
113
  // Setup token waiting mechanism
@@ -206,11 +218,19 @@ export class TagadaClient {
206
218
  * Load plugin configuration
207
219
  */
208
220
  async initializePluginConfig() {
209
- if (!this.state.pluginConfigLoading)
221
+ console.log(`[TagadaClient ${this.instanceId}] initializePluginConfig called`, {
222
+ pluginConfigLoading: this.state.pluginConfigLoading,
223
+ hasRawPluginConfig: !!this.config.rawPluginConfig,
224
+ });
225
+ if (!this.state.pluginConfigLoading) {
226
+ console.log(`[TagadaClient ${this.instanceId}] Plugin config already loading or loaded, skipping...`);
210
227
  return;
228
+ }
211
229
  try {
212
230
  const configVariant = this.config.localConfig || 'default';
231
+ console.log(`[TagadaClient ${this.instanceId}] Loading plugin config with variant: ${configVariant}`);
213
232
  const config = await loadPluginConfig(configVariant, this.config.rawPluginConfig);
233
+ console.log(`[TagadaClient ${this.instanceId}] Plugin config loaded:`, config);
214
234
  this.updateState({
215
235
  pluginConfig: config,
216
236
  pluginConfigLoading: false,
@@ -222,9 +242,6 @@ export class TagadaClient {
222
242
  environment: this.state.environment,
223
243
  });
224
244
  }
225
- if (this.state.debugMode) {
226
- console.log('[TagadaClient] Plugin config loaded:', config);
227
- }
228
245
  }
229
246
  catch (error) {
230
247
  console.error('[TagadaClient] Failed to load plugin config:', error);
@@ -238,16 +255,69 @@ export class TagadaClient {
238
255
  * Initialize token and session
239
256
  */
240
257
  async initializeToken() {
258
+ // 🔐 PRIORITY 1: Check for authCode (cross-domain handoff)
259
+ // This ALWAYS takes precedence over existing tokens
260
+ if (shouldResolveAuthCode()) {
261
+ const storeId = this.state.pluginConfig.storeId;
262
+ if (!storeId) {
263
+ console.error('[TagadaClient] Cannot resolve authCode: storeId not found in config');
264
+ return this.fallbackToNormalFlow();
265
+ }
266
+ console.log(`[TagadaClient ${this.instanceId}] 🔐 Cross-domain auth detected, resolving...`);
267
+ try {
268
+ const authCode = new URLSearchParams(window.location.search).get('authCode');
269
+ if (!authCode) {
270
+ return this.fallbackToNormalFlow();
271
+ }
272
+ // Resolve the handoff
273
+ const handoffData = await resolveAuthHandoff(authCode, storeId, this.state.environment.apiConfig.baseUrl, this.state.debugMode);
274
+ console.log(`[TagadaClient ${this.instanceId}] ✅ Auth handoff resolved:`, {
275
+ customerId: handoffData.customer.id,
276
+ role: handoffData.customer.role,
277
+ hasContext: Object.keys(handoffData.context).length > 0,
278
+ });
279
+ // Set the new token (already stored by resolveAuthHandoff)
280
+ this.setToken(handoffData.token);
281
+ // Decode session from token
282
+ const decodedSession = decodeJWTClient(handoffData.token);
283
+ if (decodedSession) {
284
+ this.updateState({ session: decodedSession });
285
+ await this.initializeSession(decodedSession);
286
+ // If context has funnelSessionId, restore it
287
+ if (handoffData.context?.funnelSessionId && this.funnel) {
288
+ if (this.state.debugMode) {
289
+ console.log(`[TagadaClient ${this.instanceId}] Restoring funnel session from handoff context:`, handoffData.context.funnelSessionId);
290
+ }
291
+ // The funnel client will pick this up during auto-initialization
292
+ }
293
+ }
294
+ else {
295
+ console.error('[TagadaClient] Failed to decode token from handoff');
296
+ this.updateState({ isInitialized: true, isLoading: false });
297
+ }
298
+ return; // ✅ Auth handoff resolved successfully, exit early
299
+ }
300
+ catch (error) {
301
+ console.error(`[TagadaClient ${this.instanceId}] ❌ Auth handoff failed, falling back to normal flow:`, error);
302
+ // Fall through to normal initialization
303
+ }
304
+ }
305
+ // Continue with normal flow if no authCode or resolution failed
306
+ await this.fallbackToNormalFlow();
307
+ }
308
+ /**
309
+ * Normal token initialization flow (no cross-domain handoff)
310
+ */
311
+ async fallbackToNormalFlow() {
241
312
  // Check for existing token in URL or storage
242
313
  const existingToken = getClientToken();
243
314
  const urlParams = new URLSearchParams(typeof window !== 'undefined' ? window.location.search : '');
244
315
  const queryToken = urlParams.get('token');
245
- if (this.state.debugMode) {
246
- console.log(`[TagadaClient ${this.instanceId}] Initializing token...`, {
247
- hasExistingToken: !!existingToken,
248
- hasQueryToken: !!queryToken
249
- });
250
- }
316
+ console.log(`[TagadaClient ${this.instanceId}] Initializing token (normal flow)...`, {
317
+ hasExistingToken: !!existingToken,
318
+ hasQueryToken: !!queryToken,
319
+ storeId: this.state.pluginConfig.storeId,
320
+ });
251
321
  let tokenToUse = null;
252
322
  let shouldPersist = false;
253
323
  if (queryToken) {
@@ -280,13 +350,15 @@ export class TagadaClient {
280
350
  else {
281
351
  // Create anonymous token
282
352
  const storeId = this.state.pluginConfig.storeId;
353
+ console.log(`[TagadaClient ${this.instanceId}] No existing token, creating anonymous token...`, {
354
+ hasStoreId: !!storeId,
355
+ storeId,
356
+ });
283
357
  if (storeId) {
284
- if (this.state.debugMode) {
285
- console.log(`[TagadaClient ${this.instanceId}] Creating anonymous token for store:`, storeId);
286
- }
287
358
  await this.createAnonymousToken(storeId);
288
359
  }
289
360
  else {
361
+ console.warn(`[TagadaClient ${this.instanceId}] No storeId in plugin config, skipping anonymous token creation`);
290
362
  this.updateState({ isInitialized: true, isLoading: false });
291
363
  }
292
364
  }
@@ -404,12 +476,22 @@ export class TagadaClient {
404
476
  osVersion: deviceInfo.userAgent.os.version,
405
477
  deviceType: deviceInfo.userAgent.device?.type,
406
478
  deviceModel: deviceInfo.userAgent.device?.model,
479
+ deviceVendor: deviceInfo.userAgent.device?.vendor,
480
+ userAgent: deviceInfo.userAgent.name,
481
+ engineName: deviceInfo.userAgent.engine.name,
482
+ engineVersion: deviceInfo.userAgent.engine.version,
483
+ cpuArchitecture: deviceInfo.userAgent.cpu.architecture,
484
+ isBot: deviceInfo.flags?.isBot ?? false,
485
+ isChromeFamily: deviceInfo.flags?.isChromeFamily ?? false,
486
+ isStandalonePWA: deviceInfo.flags?.isStandalonePWA ?? false,
487
+ isAppleSilicon: deviceInfo.flags?.isAppleSilicon ?? false,
407
488
  screenWidth: deviceInfo.screenResolution.width,
408
489
  screenHeight: deviceInfo.screenResolution.height,
409
490
  timeZone: deviceInfo.timeZone,
410
491
  draft, // 🎯 Pass draft mode to session init
492
+ fetchMessages: false,
411
493
  };
412
- const response = await this.apiClient.post('/api/v1/cms/session/init', sessionInitData);
494
+ const response = await this.apiClient.post('/api/v1/cms/session/v2/init', sessionInitData);
413
495
  // Success - reset error tracking
414
496
  this.lastSessionInitError = null;
415
497
  this.sessionInitRetryCount = 0;
@@ -451,14 +533,16 @@ export class TagadaClient {
451
533
  };
452
534
  this.updateState({ store: storeConfig });
453
535
  }
454
- // Update Locale
455
- const localeConfig = {
456
- locale: response.locale,
457
- language: response.locale.split('-')[0],
458
- region: response.locale.split('-')[1] ?? 'US',
459
- messages: response.messages ?? {},
460
- };
461
- this.updateState({ locale: localeConfig });
536
+ // Update Locale (only if provided - V2 endpoint doesn't return locale)
537
+ if (response.locale) {
538
+ const localeConfig = {
539
+ locale: response.locale,
540
+ language: response.locale.split('-')[0],
541
+ region: response.locale.split('-')[1] ?? 'US',
542
+ messages: response.messages ?? {},
543
+ };
544
+ this.updateState({ locale: localeConfig });
545
+ }
462
546
  // Update Currency
463
547
  if (response.store) {
464
548
  const currencyConfig = {
@@ -37,7 +37,9 @@ export const ENVIRONMENT_CONFIGS = {
37
37
  endpoints: {
38
38
  checkout: {
39
39
  sessionInit: '/api/v1/checkout/session/init',
40
+ sessionInitAsync: '/api/v1/checkout/session/init-async',
40
41
  sessionStatus: '/api/v1/checkout/session/status',
42
+ asyncStatus: '/api/public/v1/checkout/async-status',
41
43
  },
42
44
  customer: {
43
45
  profile: '/api/v1/customer/profile',
@@ -53,7 +55,9 @@ export const ENVIRONMENT_CONFIGS = {
53
55
  endpoints: {
54
56
  checkout: {
55
57
  sessionInit: '/api/v1/checkout/session/init',
58
+ sessionInitAsync: '/api/v1/checkout/session/init-async',
56
59
  sessionStatus: '/api/v1/checkout/session/status',
60
+ asyncStatus: '/api/public/v1/checkout/async-status',
57
61
  },
58
62
  customer: {
59
63
  profile: '/api/v1/customer/profile',
@@ -69,7 +73,9 @@ export const ENVIRONMENT_CONFIGS = {
69
73
  endpoints: {
70
74
  checkout: {
71
75
  sessionInit: '/api/v1/checkout/session/init',
76
+ sessionInitAsync: '/api/v1/checkout/session/init-async',
72
77
  sessionStatus: '/api/v1/checkout/session/status',
78
+ asyncStatus: '/api/public/v1/checkout/async-status',
73
79
  },
74
80
  customer: {
75
81
  profile: '/api/v1/customer/profile',
@@ -13,6 +13,14 @@ export interface FunnelClientConfig {
13
13
  * Set to false if you want to handle navigation manually
14
14
  */
15
15
  autoRedirect?: boolean;
16
+ /**
17
+ * Override funnelId from rawPluginConfig
18
+ */
19
+ funnelId?: string;
20
+ /**
21
+ * Override stepId from rawPluginConfig
22
+ */
23
+ stepId?: string;
16
24
  }
17
25
  export interface FunnelState {
18
26
  context: SimpleFunnelContext | null;
@@ -64,8 +72,17 @@ export declare class FunnelClient {
64
72
  }, funnelId?: string, entryStepId?: string): Promise<SimpleFunnelContext<{}>>;
65
73
  /**
66
74
  * Navigate
75
+ * @param event - Navigation event/action
76
+ * @param options - Navigation options
77
+ * @param options.fireAndForget - If true, queues navigation to QStash and returns immediately without waiting for result
78
+ * @param options.customerTags - Customer tags to set (merged with existing customer tags)
79
+ * @param options.deviceId - Device ID for geo/device tag enrichment (optional, rarely needed)
67
80
  */
68
- navigate(event: FunnelAction): Promise<FunnelNavigationResult>;
81
+ navigate(event: FunnelAction, options?: {
82
+ fireAndForget?: boolean;
83
+ customerTags?: string[];
84
+ deviceId?: string;
85
+ }): Promise<FunnelNavigationResult>;
69
86
  /**
70
87
  * Go to a specific step (direct navigation)
71
88
  */
@@ -118,19 +118,24 @@ export class FunnelClient {
118
118
  // 🎯 Read funnel tracking data from injected HTML
119
119
  const injectedFunnelId = getAssignedFunnelId(); // Funnel ID from server
120
120
  const funnelVariantId = getAssignedFunnelVariant(); // A/B test variant ID
121
- const funnelStepId = getAssignedFunnelStep(); // Current step ID
121
+ const injectedStepId = getAssignedFunnelStep(); // Current step ID
122
122
  // 🎯 Get SDK override parameters (draft, funnelTracking, etc.)
123
123
  const sdkParams = getSDKParams();
124
- // Prefer injected funnelId over URL/prop funnelId (more reliable)
125
- const finalFunnelId = injectedFunnelId || effectiveFunnelId;
124
+ // Priority: config override > injected > URL/prop
125
+ const finalFunnelId = this.config.funnelId || injectedFunnelId || effectiveFunnelId;
126
+ const finalStepId = this.config.stepId || injectedStepId;
126
127
  if (this.config.debugMode) {
127
128
  console.log('🚀 [FunnelClient] Auto-initializing...', {
128
129
  existingSessionId,
129
130
  effectiveFunnelId: finalFunnelId,
130
131
  funnelVariantId, // 🎯 Log variant ID for debugging
131
- funnelStepId, // 🎯 Log step ID for debugging
132
+ funnelStepId: finalStepId, // 🎯 Log step ID for debugging
132
133
  draft: sdkParams.draft, // 🎯 Log draft mode
133
134
  funnelTracking: sdkParams.funnelTracking, // 🎯 Log tracking flag
135
+ source: {
136
+ funnelId: this.config.funnelId ? 'config' : injectedFunnelId ? 'injected' : effectiveFunnelId ? 'url/prop' : 'none',
137
+ stepId: this.config.stepId ? 'config' : injectedStepId ? 'injected' : 'none',
138
+ },
134
139
  });
135
140
  }
136
141
  // Note: We proceed even without funnelId/sessionId - the backend will create a new anonymous session if needed
@@ -145,7 +150,7 @@ export class FunnelClient {
145
150
  existingSessionId: existingSessionId || undefined,
146
151
  currentUrl: typeof window !== 'undefined' ? window.location.href : undefined,
147
152
  funnelVariantId, // 🎯 Pass A/B test variant ID to backend
148
- funnelStepId, // 🎯 Pass step ID to backend
153
+ funnelStepId: finalStepId, // 🎯 Pass step ID to backend (with config override)
149
154
  draft: sdkParams.draft, // 🎯 Pass draft mode explicitly (more robust than URL parsing)
150
155
  funnelTracking: sdkParams.funnelTracking, // 🎯 Pass funnel tracking flag explicitly
151
156
  });
@@ -215,32 +220,100 @@ export class FunnelClient {
215
220
  }
216
221
  /**
217
222
  * Navigate
223
+ * @param event - Navigation event/action
224
+ * @param options - Navigation options
225
+ * @param options.fireAndForget - If true, queues navigation to QStash and returns immediately without waiting for result
226
+ * @param options.customerTags - Customer tags to set (merged with existing customer tags)
227
+ * @param options.deviceId - Device ID for geo/device tag enrichment (optional, rarely needed)
218
228
  */
219
- async navigate(event) {
229
+ async navigate(event, options) {
220
230
  if (!this.state.context?.sessionId)
221
231
  throw new Error('No active session');
222
232
  this.updateState({ isNavigating: true, isLoading: true });
223
233
  try {
234
+ // Get current funnel state from injected HTML/meta tags
235
+ let funnelVariantId = getAssignedFunnelVariant();
236
+ let funnelStepId = getAssignedFunnelStep();
237
+ const currentUrl = typeof window !== 'undefined' ? window.location.href : undefined;
238
+ // ✅ FALLBACK: Use config values if injection not available (e.g., on Shopify storefront)
239
+ if (!funnelStepId && this.config.stepId) {
240
+ funnelStepId = this.config.stepId;
241
+ if (this.config.debugMode) {
242
+ console.log('🔍 [FunnelClient.navigate] Using stepId from config (no injection):', funnelStepId);
243
+ }
244
+ }
245
+ if (!funnelVariantId && this.config.variantId) {
246
+ funnelVariantId = this.config.variantId;
247
+ if (this.config.debugMode) {
248
+ console.log('🔍 [FunnelClient.navigate] Using variantId from config (no injection):', funnelVariantId);
249
+ }
250
+ }
251
+ // ✅ DEBUG: Log what we're sending
252
+ if (this.config.debugMode) {
253
+ console.log('🔍 [FunnelClient.navigate] Sending to backend:', {
254
+ sessionId: this.state.context.sessionId,
255
+ currentUrl,
256
+ funnelStepId: funnelStepId || '(not found)',
257
+ funnelVariantId: funnelVariantId || '(not found)',
258
+ hasInjectedStepId: !!getAssignedFunnelStep(),
259
+ hasInjectedVariantId: !!getAssignedFunnelVariant(),
260
+ usedConfigFallback: !getAssignedFunnelStep() && !!this.config.stepId,
261
+ customerTags: options?.customerTags || '(none)',
262
+ deviceId: options?.deviceId || '(none)',
263
+ });
264
+ }
265
+ const fireAndForget = options?.fireAndForget || false;
224
266
  const response = await this.resource.navigate({
225
267
  sessionId: this.state.context.sessionId,
226
- event
268
+ event,
269
+ currentUrl,
270
+ funnelStepId,
271
+ funnelVariantId,
272
+ fireAndForget,
273
+ customerTags: options?.customerTags,
274
+ deviceId: options?.deviceId,
227
275
  });
228
- if (response.success && response.result) {
229
- // Refresh session to get updated context
230
- await this.refreshSession();
276
+ if (!response.success || !response.result) {
277
+ throw new Error(response.error || 'Navigation failed');
278
+ }
279
+ const result = response.result;
280
+ // 🔥 Fire-and-forget mode: Just return acknowledgment, skip everything else
281
+ if (result.queued) {
231
282
  this.updateState({ isNavigating: false, isLoading: false });
232
- const result = response.result;
233
- // Auto-redirect if enabled (default: true) and result has a URL
234
- const shouldAutoRedirect = this.config.autoRedirect !== false; // Default to true
235
- if (shouldAutoRedirect && result?.url && typeof window !== 'undefined') {
283
+ // Update session ID if it changed (session recovery)
284
+ if (result.sessionId && result.sessionId !== this.state.context?.sessionId) {
236
285
  if (this.config.debugMode) {
237
- console.log('🚀 [FunnelClient] Auto-redirecting to:', result.url);
286
+ console.log(`🔥 [FunnelClient] Session ID updated: ${this.state.context?.sessionId} → ${result.sessionId}`);
287
+ }
288
+ // Update context session ID
289
+ if (this.state.context) {
290
+ this.state.context.sessionId = result.sessionId;
238
291
  }
239
- window.location.href = result.url;
292
+ }
293
+ if (this.config.debugMode) {
294
+ console.log('🔥 [FunnelClient] Navigation queued (fire-and-forget mode)');
240
295
  }
241
296
  return result;
242
297
  }
243
- throw new Error(response.error || 'Navigation failed');
298
+ // Normal navigation: handle redirect
299
+ const shouldAutoRedirect = this.config.autoRedirect !== false; // Default to true
300
+ // Skip refreshSession if auto-redirecting (next page will initialize with fresh state)
301
+ // Only refresh if staying on same page (autoRedirect: false)
302
+ if (!shouldAutoRedirect) {
303
+ if (this.config.debugMode) {
304
+ console.log('🔄 [FunnelClient] Refreshing session (no auto-redirect)');
305
+ }
306
+ await this.refreshSession();
307
+ }
308
+ this.updateState({ isNavigating: false, isLoading: false });
309
+ // Auto-redirect if enabled and result has a URL
310
+ if (shouldAutoRedirect && result?.url && typeof window !== 'undefined') {
311
+ if (this.config.debugMode) {
312
+ console.log('🚀 [FunnelClient] Auto-redirecting to:', result.url, '(skipped session refresh - next page will initialize)');
313
+ }
314
+ window.location.href = result.url;
315
+ }
316
+ return result;
244
317
  }
245
318
  catch (error) {
246
319
  const err = error instanceof Error ? error : new Error(String(error));
@@ -180,17 +180,60 @@ export declare class CheckoutResource {
180
180
  private apiClient;
181
181
  constructor(apiClient: ApiClient);
182
182
  /**
183
- * Initialize a new checkout session
183
+ * Initialize a new checkout session (sync mode)
184
+ * Response time: 2-5 seconds (full processing)
184
185
  */
185
186
  initCheckout(params: CheckoutInitParams): Promise<{
186
187
  checkoutUrl: string;
187
188
  checkoutSession: CheckoutSession;
188
189
  checkoutToken: string;
189
190
  }>;
191
+ /**
192
+ * Initialize a new checkout session (async mode) ⚡
193
+ * Response time: ~50ms (20-50x faster!)
194
+ *
195
+ * Returns checkoutToken immediately, background job completes processing.
196
+ * Use getCheckout() to fetch full session data - it auto-waits for completion.
197
+ *
198
+ * @example
199
+ * // Fast init
200
+ * const { checkoutToken } = await checkout.initCheckoutAsync(params);
201
+ *
202
+ * // Redirect user immediately
203
+ * window.location.href = `/checkout/${checkoutToken}/op`;
204
+ *
205
+ * // By the time page loads, background processing is usually complete
206
+ */
207
+ initCheckoutAsync(params: CheckoutInitParams): Promise<{
208
+ checkoutToken: string;
209
+ customerId: string;
210
+ status: 'processing';
211
+ }>;
212
+ /**
213
+ * Check async checkout processing status (instant, no waiting)
214
+ * Perfect for polling or checking if background job completed
215
+ */
216
+ checkAsyncStatus(checkoutToken: string): Promise<{
217
+ isAsync: boolean;
218
+ isProcessing: boolean;
219
+ isComplete: boolean;
220
+ hasError: boolean;
221
+ error?: string;
222
+ }>;
190
223
  /**
191
224
  * Get checkout session by token
225
+ *
226
+ * SDK automatically waits for async completion (skipAsyncWait=false) for seamless UX.
227
+ * This ensures async checkout sessions are fully processed before returning data.
228
+ *
229
+ * If you need to skip waiting (e.g., for manual polling), use getCheckoutRaw() instead.
192
230
  */
193
231
  getCheckout(checkoutToken: string, currency?: string): Promise<CheckoutData>;
232
+ /**
233
+ * Get checkout session by token without waiting for async completion
234
+ * Useful for manual polling scenarios
235
+ */
236
+ getCheckoutRaw(checkoutToken: string, currency?: string): Promise<CheckoutData>;
194
237
  /**
195
238
  * Update checkout address
196
239
  */
@@ -7,20 +7,67 @@ export class CheckoutResource {
7
7
  this.apiClient = apiClient;
8
8
  }
9
9
  /**
10
- * Initialize a new checkout session
10
+ * Initialize a new checkout session (sync mode)
11
+ * Response time: 2-5 seconds (full processing)
11
12
  */
12
13
  async initCheckout(params) {
13
14
  // Pass all params including customerId to prevent duplicate customer creation
14
15
  return this.apiClient.post('/api/v1/checkout/session/init', params);
15
16
  }
17
+ /**
18
+ * Initialize a new checkout session (async mode) ⚡
19
+ * Response time: ~50ms (20-50x faster!)
20
+ *
21
+ * Returns checkoutToken immediately, background job completes processing.
22
+ * Use getCheckout() to fetch full session data - it auto-waits for completion.
23
+ *
24
+ * @example
25
+ * // Fast init
26
+ * const { checkoutToken } = await checkout.initCheckoutAsync(params);
27
+ *
28
+ * // Redirect user immediately
29
+ * window.location.href = `/checkout/${checkoutToken}/op`;
30
+ *
31
+ * // By the time page loads, background processing is usually complete
32
+ */
33
+ async initCheckoutAsync(params) {
34
+ return this.apiClient.post('/api/v1/checkout/session/init-async', params);
35
+ }
36
+ /**
37
+ * Check async checkout processing status (instant, no waiting)
38
+ * Perfect for polling or checking if background job completed
39
+ */
40
+ async checkAsyncStatus(checkoutToken) {
41
+ return this.apiClient.get(`/api/public/v1/checkout/async-status/${checkoutToken}`);
42
+ }
16
43
  /**
17
44
  * Get checkout session by token
45
+ *
46
+ * SDK automatically waits for async completion (skipAsyncWait=false) for seamless UX.
47
+ * This ensures async checkout sessions are fully processed before returning data.
48
+ *
49
+ * If you need to skip waiting (e.g., for manual polling), use getCheckoutRaw() instead.
18
50
  */
19
51
  async getCheckout(checkoutToken, currency) {
20
52
  const queryParams = new URLSearchParams();
21
53
  if (currency) {
22
54
  queryParams.set('currency', currency);
23
55
  }
56
+ // SDK explicitly waits for async completion (default is skip=true for retro compat)
57
+ queryParams.set('skipAsyncWait', 'false');
58
+ const url = `/api/v1/checkout-sessions/${checkoutToken}/v2${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
59
+ return this.apiClient.get(url);
60
+ }
61
+ /**
62
+ * Get checkout session by token without waiting for async completion
63
+ * Useful for manual polling scenarios
64
+ */
65
+ async getCheckoutRaw(checkoutToken, currency) {
66
+ const queryParams = new URLSearchParams();
67
+ if (currency) {
68
+ queryParams.set('currency', currency);
69
+ }
70
+ queryParams.set('skipAsyncWait', 'true');
24
71
  const url = `/api/v1/checkout-sessions/${checkoutToken}/v2${queryParams.toString() ? `?${queryParams.toString()}` : ''}`;
25
72
  return this.apiClient.get(url);
26
73
  }