@tagadapay/plugin-sdk 3.0.9 → 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.
Files changed (31) hide show
  1. package/dist/external-tracker.js +3802 -195
  2. package/dist/external-tracker.min.js +25 -2
  3. package/dist/external-tracker.min.js.map +4 -4
  4. package/dist/react/types.d.ts +2 -0
  5. package/dist/v2/core/client.d.ts +4 -0
  6. package/dist/v2/core/client.js +314 -123
  7. package/dist/v2/core/config/environment.js +6 -0
  8. package/dist/v2/core/funnelClient.d.ts +18 -1
  9. package/dist/v2/core/funnelClient.js +90 -17
  10. package/dist/v2/core/resources/checkout.d.ts +44 -1
  11. package/dist/v2/core/resources/checkout.js +48 -1
  12. package/dist/v2/core/resources/funnel.d.ts +44 -4
  13. package/dist/v2/core/resources/offers.d.ts +26 -0
  14. package/dist/v2/core/resources/offers.js +37 -0
  15. package/dist/v2/core/types.d.ts +3 -1
  16. package/dist/v2/core/utils/authHandoff.d.ts +60 -0
  17. package/dist/v2/core/utils/authHandoff.js +154 -0
  18. package/dist/v2/core/utils/deviceInfo.d.ts +20 -3
  19. package/dist/v2/core/utils/deviceInfo.js +62 -94
  20. package/dist/v2/core/utils/previewMode.d.ts +4 -0
  21. package/dist/v2/core/utils/previewMode.js +4 -0
  22. package/dist/v2/react/components/DebugDrawer.js +68 -46
  23. package/dist/v2/react/hooks/useCheckoutQuery.d.ts +0 -1
  24. package/dist/v2/react/hooks/useCheckoutQuery.js +12 -4
  25. package/dist/v2/react/hooks/useFunnelLegacy.js +39 -11
  26. package/dist/v2/react/hooks/usePreviewOffer.d.ts +3 -3
  27. package/dist/v2/react/hooks/usePreviewOffer.js +20 -15
  28. package/dist/v2/react/hooks/useTranslation.js +12 -4
  29. package/dist/v2/standalone/index.d.ts +2 -1
  30. package/dist/v2/standalone/index.js +2 -1
  31. package/package.json +3 -1
@@ -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 = {
@@ -588,113 +672,220 @@ export class TagadaClient {
588
672
  applyStylesToElement(elementId, styles) {
589
673
  if (typeof document === 'undefined')
590
674
  return;
591
- // Support multiple IDs in editor-id attribute (space-separated)
592
- // Use ~= selector to match elementId as a space-separated value
593
- const element = document.querySelector(`[editor-id~="${elementId}"]`);
594
- if (!element) {
595
- if (this.state.debugMode) {
596
- console.warn(`[TagadaClient] Element with editor-id containing "${elementId}" not found`);
597
- }
598
- return;
675
+ // Always remove any existing highlight, even if the target element is not found
676
+ const staticHighlightId = 'tagada-editor-highlight';
677
+ const existingHighlight = document.getElementById(staticHighlightId);
678
+ if (existingHighlight) {
679
+ existingHighlight.remove();
599
680
  }
600
- // List of void/self-closing elements that don't accept children
601
- const voidElements = new Set([
602
- 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
603
- 'link', 'meta', 'param', 'source', 'track', 'wbr'
604
- ]);
605
- const isVoidElement = voidElements.has(element.tagName.toLowerCase());
606
- // For void elements, wrap them in a container div
607
- let targetElement = element;
608
- let wrapper = null;
609
- if (isVoidElement) {
610
- // Check if element is already wrapped (from a previous highlight)
611
- const parent = element.parentElement;
612
- const existingWrapper = parent?.getAttribute('data-tagada-highlight-wrapper');
613
- if (existingWrapper === 'true') {
614
- // Reuse existing wrapper
615
- targetElement = parent;
681
+ const applyToElement = (element) => {
682
+ // List of void/self-closing elements that don't accept children
683
+ const voidElements = new Set([
684
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
685
+ 'link', 'meta', 'param', 'source', 'track', 'wbr'
686
+ ]);
687
+ const isVoidElement = voidElements.has(element.tagName.toLowerCase());
688
+ // For void elements, wrap them in a container div
689
+ let targetElement = element;
690
+ if (isVoidElement) {
691
+ // Check if element is already wrapped (from a previous highlight)
692
+ const parent = element.parentElement;
693
+ const existingWrapper = parent?.getAttribute('data-tagada-highlight-wrapper');
694
+ if (existingWrapper === 'true' && parent instanceof HTMLElement) {
695
+ // Reuse existing wrapper
696
+ targetElement = parent;
697
+ }
698
+ else {
699
+ // Create a new wrapper
700
+ const newWrapper = document.createElement('div');
701
+ newWrapper.setAttribute('data-tagada-highlight-wrapper', 'true');
702
+ // Preserve the element's layout behavior by matching its display style
703
+ // This minimizes the visual impact of the wrapper
704
+ const computedStyle = window.getComputedStyle(element);
705
+ const elementDisplay = computedStyle.display;
706
+ // Match the display type to preserve layout
707
+ // For inline elements, use inline-block (to support position: relative)
708
+ // For all others, use the same display value
709
+ if (elementDisplay === 'inline') {
710
+ newWrapper.style.display = 'inline-block';
711
+ }
712
+ else {
713
+ newWrapper.style.display = elementDisplay;
714
+ }
715
+ newWrapper.style.position = 'relative';
716
+ // Preserve vertical alignment for inline elements
717
+ if (elementDisplay === 'inline' || elementDisplay.includes('inline')) {
718
+ const verticalAlign = computedStyle.verticalAlign;
719
+ if (verticalAlign && verticalAlign !== 'baseline') {
720
+ newWrapper.style.verticalAlign = verticalAlign;
721
+ }
722
+ }
723
+ // Preserve spacing and layout properties from the original element
724
+ // List of CSS properties that affect spacing and layout
725
+ const spacingProperties = [
726
+ // Width and height
727
+ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
728
+ // Flex properties
729
+ 'flex', 'flexGrow', 'flexShrink', 'flexBasis',
730
+ // Grid properties
731
+ 'gridColumn', 'gridRow', 'gridColumnStart', 'gridColumnEnd',
732
+ 'gridRowStart', 'gridRowEnd', 'gridArea',
733
+ // Alignment properties
734
+ 'alignSelf', 'justifySelf',
735
+ // Box sizing
736
+ 'boxSizing',
737
+ // Gap (for grid/flex)
738
+ 'gap', 'rowGap', 'columnGap',
739
+ // Order (for flex/grid)
740
+ 'order',
741
+ // Aspect ratio
742
+ 'aspectRatio',
743
+ ];
744
+ spacingProperties.forEach(prop => {
745
+ const camelProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
746
+ const value = computedStyle.getPropertyValue(camelProp);
747
+ // Only copy if the value is not empty
748
+ // For most properties, we want to preserve the value even if it's 'auto' or '0px'
749
+ // as these might be intentional layout choices
750
+ if (value && value.trim() !== '') {
751
+ // Handle special cases
752
+ if (prop === 'flex' && value !== 'none' && value !== '0 1 auto') {
753
+ newWrapper.style.flex = value;
754
+ }
755
+ else if (prop === 'boxSizing') {
756
+ newWrapper.style.boxSizing = value;
757
+ }
758
+ else {
759
+ // Use setProperty for kebab-case properties
760
+ // Preserve all values including percentages, auto, etc.
761
+ newWrapper.style.setProperty(camelProp, value);
762
+ }
763
+ }
764
+ });
765
+ // Also check inline styles for spacing properties
766
+ // This handles cases where styles are set directly on the element
767
+ if (element.style) {
768
+ const inlineSpacingProps = [
769
+ 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
770
+ 'flex', 'flex-grow', 'flex-shrink', 'flex-basis',
771
+ 'grid-column', 'grid-row', 'grid-column-start', 'grid-column-end',
772
+ 'grid-row-start', 'grid-row-end', 'grid-area',
773
+ 'align-self', 'justify-self',
774
+ 'box-sizing', 'gap', 'row-gap', 'column-gap', 'order', 'aspect-ratio'
775
+ ];
776
+ inlineSpacingProps.forEach(prop => {
777
+ const inlineValue = element.style.getPropertyValue(prop);
778
+ if (inlineValue) {
779
+ newWrapper.style.setProperty(prop, inlineValue);
780
+ }
781
+ });
782
+ }
783
+ // Insert wrapper before element
784
+ element.parentNode?.insertBefore(newWrapper, element);
785
+ // Move element into wrapper
786
+ newWrapper.appendChild(element);
787
+ targetElement = newWrapper;
788
+ }
616
789
  }
617
- else {
618
- // Create a new wrapper
619
- wrapper = document.createElement('div');
620
- wrapper.setAttribute('data-tagada-highlight-wrapper', 'true');
621
- // Preserve the element's layout behavior by matching its display style
622
- // This minimizes the visual impact of the wrapper
623
- const computedStyle = window.getComputedStyle(element);
624
- const elementDisplay = computedStyle.display;
625
- // Match the display type to preserve layout
626
- // For inline elements, use inline-block (to support position: relative)
627
- // For all others, use the same display value
628
- if (elementDisplay === 'inline') {
629
- wrapper.style.display = 'inline-block';
790
+ // Ensure element has position relative for absolute child positioning
791
+ const computedStyle = getComputedStyle(isVoidElement ? element : targetElement);
792
+ if (targetElement.style.position === 'static' || !targetElement.style.position) {
793
+ targetElement.style.position = 'relative';
794
+ }
795
+ // Create new highlight div with static ID
796
+ const highlightDiv = document.createElement('div');
797
+ highlightDiv.id = staticHighlightId;
798
+ highlightDiv.style.position = 'absolute';
799
+ highlightDiv.style.inset = '0';
800
+ highlightDiv.style.pointerEvents = 'none';
801
+ highlightDiv.style.zIndex = '9999';
802
+ highlightDiv.style.boxSizing = 'border-box';
803
+ highlightDiv.style.background = 'none';
804
+ highlightDiv.style.border = 'none';
805
+ highlightDiv.style.outline = 'none';
806
+ highlightDiv.style.margin = '0';
807
+ highlightDiv.style.padding = '0';
808
+ // Inherit border radius from parent
809
+ const borderRadius = computedStyle.borderRadius;
810
+ if (borderRadius) {
811
+ highlightDiv.style.borderRadius = borderRadius;
812
+ }
813
+ // Append the new highlight div to the target element
814
+ targetElement.appendChild(highlightDiv);
815
+ // Apply CSS properties to the highlight div
816
+ const stylesToApply = styles || { boxShadow: '0 0 0 2px rgb(239 68 68)' }; // Default red ring
817
+ Object.entries(stylesToApply).forEach(([property, value]) => {
818
+ // Convert kebab-case to camelCase for style object
819
+ const camelProperty = property.includes('-')
820
+ ? property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
821
+ : property;
822
+ // Use setProperty for kebab-case or direct assignment for camelCase
823
+ if (property.includes('-')) {
824
+ highlightDiv.style.setProperty(property, value);
630
825
  }
631
826
  else {
632
- wrapper.style.display = elementDisplay;
827
+ highlightDiv.style[camelProperty] = value;
633
828
  }
634
- wrapper.style.position = 'relative';
635
- // Preserve vertical alignment for inline elements
636
- if (elementDisplay === 'inline' || elementDisplay.includes('inline')) {
637
- const verticalAlign = computedStyle.verticalAlign;
638
- if (verticalAlign && verticalAlign !== 'baseline') {
639
- wrapper.style.verticalAlign = verticalAlign;
829
+ });
830
+ // Scroll element into view
831
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
832
+ if (this.state.debugMode) {
833
+ const appliedStyles = Object.entries(stylesToApply).map(([k, v]) => `${k}: ${v}`).join('; ');
834
+ console.log(`[TagadaClient] Applied styles to highlight div of element #${elementId}:`, appliedStyles);
835
+ }
836
+ };
837
+ // Support multiple IDs in editor-id attribute (space-separated)
838
+ // Use ~= selector to match elementId as a space-separated value
839
+ const maxAttempts = 5;
840
+ const intervalMs = 1000;
841
+ let attempts = 0;
842
+ const isElementHidden = (element) => {
843
+ const style = window.getComputedStyle(element);
844
+ return (style.display === 'none' ||
845
+ style.visibility === 'hidden' ||
846
+ style.opacity === '0' ||
847
+ element.hidden ||
848
+ element.offsetWidth === 0 ||
849
+ element.offsetHeight === 0);
850
+ };
851
+ const findAndApply = () => {
852
+ const elements = document.querySelectorAll(`[editor-id~="${elementId}"]`);
853
+ if (elements.length === 0) {
854
+ attempts += 1;
855
+ if (attempts >= maxAttempts) {
856
+ if (this.state.debugMode) {
857
+ console.warn(`[TagadaClient] Element with editor-id containing "${elementId}" not found after ${maxAttempts} attempts`);
640
858
  }
859
+ return;
641
860
  }
642
- // Insert wrapper before element
643
- element.parentNode?.insertBefore(wrapper, element);
644
- // Move element into wrapper
645
- wrapper.appendChild(element);
646
- targetElement = wrapper;
861
+ if (this.state.debugMode) {
862
+ console.warn(`[TagadaClient] Element with editor-id containing "${elementId}" not found (attempt ${attempts}/${maxAttempts}), retrying in ${intervalMs / 1000}s`);
863
+ }
864
+ setTimeout(findAndApply, intervalMs);
865
+ return;
647
866
  }
648
- }
649
- // Ensure element has position relative for absolute child positioning
650
- const computedStyle = getComputedStyle(isVoidElement ? element : targetElement);
651
- if (targetElement.style.position === 'static' || !targetElement.style.position) {
652
- targetElement.style.position = 'relative';
653
- }
654
- // Remove any existing highlight div before creating a new one
655
- const staticHighlightId = 'tagada-editor-highlight';
656
- const existingHighlight = document.getElementById(staticHighlightId);
657
- if (existingHighlight) {
658
- existingHighlight.remove();
659
- }
660
- // Create new highlight div with static ID
661
- const highlightDiv = document.createElement('div');
662
- highlightDiv.id = staticHighlightId;
663
- highlightDiv.style.position = 'absolute';
664
- highlightDiv.style.inset = '0';
665
- highlightDiv.style.pointerEvents = 'none';
666
- highlightDiv.style.zIndex = '9999';
667
- highlightDiv.style.boxSizing = 'border-box';
668
- highlightDiv.style.background = 'none';
669
- highlightDiv.style.border = 'none';
670
- highlightDiv.style.outline = 'none';
671
- // Inherit border radius from parent
672
- const borderRadius = computedStyle.borderRadius;
673
- if (borderRadius) {
674
- highlightDiv.style.borderRadius = borderRadius;
675
- }
676
- // Append the new highlight div to the target element
677
- targetElement.appendChild(highlightDiv);
678
- // Apply CSS properties to the highlight div
679
- const stylesToApply = styles || { boxShadow: '0 0 0 2px rgb(239 68 68)' }; // Default red ring
680
- Object.entries(stylesToApply).forEach(([property, value]) => {
681
- // Convert kebab-case to camelCase for style object
682
- const camelProperty = property.includes('-')
683
- ? property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
684
- : property;
685
- // Use setProperty for kebab-case or direct assignment for camelCase
686
- if (property.includes('-')) {
687
- highlightDiv.style.setProperty(property, value);
867
+ // If multiple elements found, prioritize the one that is not hidden
868
+ let element = null;
869
+ if (elements.length === 1) {
870
+ element = elements[0];
688
871
  }
689
872
  else {
690
- highlightDiv.style[camelProperty] = value;
873
+ // Find the first non-hidden element
874
+ for (let i = 0; i < elements.length; i++) {
875
+ if (!isElementHidden(elements[i])) {
876
+ element = elements[i];
877
+ break;
878
+ }
879
+ }
880
+ // If all are hidden, use the first one
881
+ if (!element) {
882
+ element = elements[0];
883
+ }
691
884
  }
692
- });
693
- // Scroll element into view
694
- element.scrollIntoView({ behavior: 'smooth', block: 'center' });
695
- if (this.state.debugMode) {
696
- const appliedStyles = Object.entries(stylesToApply).map(([k, v]) => `${k}: ${v}`).join('; ');
697
- console.log(`[TagadaClient] Applied styles to highlight div of element #${elementId}:`, appliedStyles);
698
- }
885
+ if (element) {
886
+ applyToElement(element);
887
+ }
888
+ };
889
+ findAndApply();
699
890
  }
700
891
  }
@@ -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
  */