@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
@@ -17,13 +17,13 @@ export declare const PAYMENT_CONFIGS: Record<string, PaymentConfig>;
17
17
  */
18
18
  export declare function getPaymentConfig(environment?: string): PaymentConfig;
19
19
  /**
20
- * Get BasisTheory API key for current environment
20
+ * Get BasisTheory API key and tenant ID based on environment detection
21
21
  *
22
- * Behavior:
23
- * - LOCAL/DEVELOPMENT: Always uses embedded test key (key_test_us_pub_VExdfbFQARn821iqP8zNaq)
24
- * Environment variables are ignored to prevent accidental production key usage
25
- *
26
- * - PRODUCTION: Uses environment variable if set, falls back to embedded production key
27
- * This allows deployment-time configuration while having safe defaults
22
+ * Uses detectEnvironment() to determine if production or test keys should be used.
23
+ * No environment variables - keys are hardcoded for consistency.
24
+ */
25
+ export declare function getBasisTheoryApiKey(): string;
26
+ /**
27
+ * Get BasisTheory tenant ID based on environment detection
28
28
  */
29
- export declare function getBasisTheoryApiKey(environment?: string): string;
29
+ export declare function getBasisTheoryTenantId(): string;
@@ -42,28 +42,24 @@ export const PAYMENT_CONFIGS = {
42
42
  export function getPaymentConfig(environment = 'local') {
43
43
  return PAYMENT_CONFIGS[environment] || PAYMENT_CONFIGS.default || PAYMENT_CONFIGS.local;
44
44
  }
45
+ import { detectEnvironment } from '../../v2/core/config/environment';
46
+ import { getBasisTheoryKeys } from '../../config/basisTheory';
45
47
  /**
46
- * Get BasisTheory API key for current environment
48
+ * Get BasisTheory API key and tenant ID based on environment detection
47
49
  *
48
- * Behavior:
49
- * - LOCAL/DEVELOPMENT: Always uses embedded test key (key_test_us_pub_VExdfbFQARn821iqP8zNaq)
50
- * Environment variables are ignored to prevent accidental production key usage
51
- *
52
- * - PRODUCTION: Uses environment variable if set, falls back to embedded production key
53
- * This allows deployment-time configuration while having safe defaults
50
+ * Uses detectEnvironment() to determine if production or test keys should be used.
51
+ * No environment variables - keys are hardcoded for consistency.
52
+ */
53
+ export function getBasisTheoryApiKey() {
54
+ const isProduction = detectEnvironment() === 'production';
55
+ const keys = getBasisTheoryKeys(isProduction);
56
+ return keys.apiKey;
57
+ }
58
+ /**
59
+ * Get BasisTheory tenant ID based on environment detection
54
60
  */
55
- export function getBasisTheoryApiKey(environment = 'local') {
56
- // For production environment, allow environment variable override
57
- if (environment === 'production') {
58
- if (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_BASIS_THEORY_PUBLIC_API_KEY) {
59
- return process.env.NEXT_PUBLIC_BASIS_THEORY_PUBLIC_API_KEY;
60
- }
61
- if (typeof process !== 'undefined' && process.env?.VITE_BASIS_THEORY_PUBLIC_API_KEY) {
62
- return process.env.VITE_BASIS_THEORY_PUBLIC_API_KEY;
63
- }
64
- }
65
- // For local/development, always use embedded test key to prevent accidental production key usage
66
- // Fall back to embedded configuration based on environment
67
- const config = getPaymentConfig(environment);
68
- return config.basisTheory.publicApiKey;
61
+ export function getBasisTheoryTenantId() {
62
+ const isProduction = detectEnvironment() === 'production';
63
+ const keys = getBasisTheoryKeys(isProduction);
64
+ return keys.tenantId;
69
65
  }
@@ -9,7 +9,7 @@ export function useApplePay(options = {}) {
9
9
  const { createApplePayPaymentInstrument, processApplePayPayment } = usePayment();
10
10
  const { environment, apiService } = useTagadaContext();
11
11
  // Get API key from environment
12
- const apiKey = getBasisTheoryApiKey(environment?.environment || 'local');
12
+ const apiKey = getBasisTheoryApiKey(); // Auto-detects environment
13
13
  // Check Apple Pay availability on mount
14
14
  useEffect(() => {
15
15
  const checkApplePayAvailability = () => {
@@ -24,10 +24,8 @@ export function usePayment() {
24
24
  const { createSession, startChallenge } = useThreeds();
25
25
  // Track challenge in progress to prevent multiple challenges
26
26
  const challengeInProgressRef = useRef(false);
27
- // Stabilize environment value to prevent re-renders
28
- const currentEnvironment = useMemo(() => environment?.environment || 'local', [environment?.environment]);
29
27
  // Get API key from embedded configuration with proper environment detection
30
- const apiKey = useMemo(() => getBasisTheoryApiKey(currentEnvironment), [currentEnvironment]);
28
+ const apiKey = useMemo(() => getBasisTheoryApiKey(), []); // Auto-detects environment
31
29
  // Initialize BasisTheory using React wrapper
32
30
  const { bt: basisTheory, error: btError } = useBasisTheory(apiKey, {
33
31
  elements: false,
@@ -41,7 +41,7 @@ export function useThreeds(options = {}) {
41
41
  throw new Error('BasisTheory3ds not loaded yet');
42
42
  }
43
43
  // Use the same API key approach as the working CMS version
44
- const apiKey = getBasisTheoryApiKey('production'); // Use production config for now
44
+ const apiKey = getBasisTheoryApiKey(); // Auto-detects environment
45
45
  const bt3ds = basisTheory3ds(apiKey);
46
46
  console.log('paymentInstrument paymentInstrument', paymentInstrument?.token);
47
47
  const session = await bt3ds.createSession({
@@ -94,7 +94,7 @@ export function useThreeds(options = {}) {
94
94
  throw new Error('BasisTheory3ds not loaded yet');
95
95
  }
96
96
  // Use the same API key approach as the working CMS version
97
- const apiKey = getBasisTheoryApiKey('production'); // Use production config for now
97
+ const apiKey = getBasisTheoryApiKey(); // Auto-detects environment
98
98
  if (!apiKey) {
99
99
  throw new Error('BasisTheory API key is not set');
100
100
  }
@@ -1,7 +1,8 @@
1
+ import { FunnelClient } from './funnelClient';
1
2
  import { ApiClient } from './resources/apiClient';
3
+ import { AuthState, Currency, Customer, Environment, EnvironmentConfig, Locale, Session, Store } from './types';
4
+ import { EventBus } from './utils/eventBus';
2
5
  import { PluginConfig, RawPluginConfig } from './utils/pluginConfig';
3
- import { Environment, EnvironmentConfig, Session, AuthState, Customer, Store, Locale, Currency } from './types';
4
- import { FunnelClient } from './funnelClient';
5
6
  export interface TagadaClientConfig {
6
7
  environment?: Environment;
7
8
  customApiConfig?: Partial<EnvironmentConfig>;
@@ -17,8 +18,11 @@ export interface TagadaClientConfig {
17
18
  /**
18
19
  * Funnel session + navigation layer.
19
20
  * Set to false to completely disable funnel behaviour.
21
+ * Pass object to configure behavior.
20
22
  */
21
- funnel?: boolean;
23
+ funnel?: boolean | {
24
+ autoRedirect?: boolean;
25
+ };
22
26
  };
23
27
  }
24
28
  export interface TagadaState {
@@ -46,6 +50,10 @@ export declare class TagadaClient {
46
50
  * low-level logic without instantiating a second client.
47
51
  */
48
52
  funnel?: FunnelClient;
53
+ /**
54
+ * Event bus for domain events and coordination
55
+ */
56
+ bus: EventBus;
49
57
  private eventDispatcher;
50
58
  private tokenPromise;
51
59
  private tokenResolver;
@@ -112,4 +120,23 @@ export declare class TagadaClient {
112
120
  private updateSessionState;
113
121
  private getCurrencySymbol;
114
122
  private getCurrencyName;
123
+ /**
124
+ * Helper to get accountId with fallbacks
125
+ */
126
+ getAccountId(): string;
127
+ /**
128
+ * Update plugin config dynamically (hot-reload)
129
+ * Used for live config editing without page reload
130
+ */
131
+ updatePluginConfig(newConfig: Partial<PluginConfig['config']>): void;
132
+ /**
133
+ * Setup listener for config updates from parent window (config editor)
134
+ * Enables live config editing via postMessage
135
+ */
136
+ private setupConfigHotReload;
137
+ /**
138
+ * Apply CSS properties to a real div child element for highlighting
139
+ * Used by config editor to highlight elements in preview without altering the element's style
140
+ */
141
+ private applyStylesToElement;
115
142
  }
@@ -1,13 +1,19 @@
1
- import { ApiClient } from './resources/apiClient';
2
1
  import { detectEnvironment, getEnvironmentConfig } from './config/environment';
3
- import { loadPluginConfig } from './utils/pluginConfig';
2
+ import { FunnelClient } from './funnelClient';
3
+ import { ApiClient } from './resources/apiClient';
4
4
  import { collectDeviceInfo, getBrowserLocale, getUrlParams } from './utils/deviceInfo';
5
+ import { EventBus } from './utils/eventBus';
6
+ import { EventDispatcher } from './utils/eventDispatcher';
5
7
  import { decodeJWTClient, isTokenExpired } from './utils/jwtDecoder';
8
+ import { loadPluginConfig } from './utils/pluginConfig';
9
+ import { handlePreviewMode, isDraftMode, setDraftMode } from './utils/previewMode';
6
10
  import { getClientToken, setClientToken } from './utils/tokenStorage';
7
- import { EventDispatcher } from './utils/eventDispatcher';
8
- import { FunnelClient } from './funnelClient';
9
11
  export class TagadaClient {
10
12
  constructor(config = {}) {
13
+ /**
14
+ * Event bus for domain events and coordination
15
+ */
16
+ this.bus = new EventBus();
11
17
  this.eventDispatcher = new EventDispatcher();
12
18
  this.tokenPromise = null;
13
19
  this.tokenResolver = null;
@@ -22,6 +28,12 @@ export class TagadaClient {
22
28
  if (this.config.debugMode) {
23
29
  console.log(`[TagadaClient ${this.instanceId}] Initializing...`);
24
30
  }
31
+ // Handle preview mode FIRST - clears state if needed
32
+ // This ensures clean state when CRM previews pages
33
+ const previewModeActive = handlePreviewMode(this.config.debugMode);
34
+ if (previewModeActive && this.config.debugMode) {
35
+ console.log(`[TagadaClient ${this.instanceId}] Preview mode active - state cleared`);
36
+ }
25
37
  // Initialize default state
26
38
  const env = this.resolveEnvironment();
27
39
  let envConfig = getEnvironmentConfig(env);
@@ -74,13 +86,16 @@ export class TagadaClient {
74
86
  baseURL: envConfig.apiConfig.baseUrl,
75
87
  });
76
88
  // Initialize optional funnel client (feature-flagged)
77
- const funnelEnabled = config.features?.funnel !== false;
89
+ const funnelFeature = config.features?.funnel;
90
+ const funnelEnabled = funnelFeature !== false;
78
91
  if (funnelEnabled) {
92
+ const funnelConfig = typeof funnelFeature === 'object' ? funnelFeature : {};
79
93
  this.funnel = new FunnelClient({
80
94
  apiClient: this.apiClient,
81
95
  debugMode: this.state.debugMode,
82
96
  pluginConfig: this.state.pluginConfig,
83
97
  environment: this.state.environment,
98
+ autoRedirect: funnelConfig.autoRedirect,
84
99
  });
85
100
  }
86
101
  // Setup token waiting mechanism
@@ -89,6 +104,8 @@ export class TagadaClient {
89
104
  if (typeof window !== 'undefined') {
90
105
  window.addEventListener('storage', this.boundHandleStorageChange);
91
106
  }
107
+ // Setup config hot-reload listener (for live config editing)
108
+ this.setupConfigHotReload();
92
109
  // Start initialization
93
110
  this.initialize();
94
111
  }
@@ -103,6 +120,7 @@ export class TagadaClient {
103
120
  console.log(`[TagadaClient ${this.instanceId}] Destroyed`);
104
121
  }
105
122
  this.eventDispatcher.clear();
123
+ this.bus.clear();
106
124
  }
107
125
  /**
108
126
  * Handle storage changes (e.g. token update in another tab)
@@ -318,10 +336,17 @@ export class TagadaClient {
318
336
  return;
319
337
  }
320
338
  try {
321
- if (this.state.debugMode)
322
- console.log('[TagadaClient] Creating anonymous token for store:', storeId);
339
+ // 🎯 Check draft mode from URL, localStorage, or cookie
340
+ const draft = isDraftMode();
341
+ if (this.state.debugMode) {
342
+ console.log('[TagadaClient] Creating anonymous token for store:', storeId, { draft });
343
+ }
323
344
  // We use fetch directly or ApiClient with skipAuth to avoid waiting for itself
324
- const response = await this.apiClient.post('/api/v1/cms/session/anonymous', { storeId, role: 'anonymous' }, { skipAuth: true });
345
+ const response = await this.apiClient.post('/api/v1/cms/session/anonymous', {
346
+ storeId,
347
+ role: 'anonymous',
348
+ draft, // 🎯 Pass draft mode to anonymous login
349
+ }, { skipAuth: true });
325
350
  this.setToken(response.token);
326
351
  setClientToken(response.token);
327
352
  const decodedSession = decodeJWTClient(response.token);
@@ -355,6 +380,13 @@ export class TagadaClient {
355
380
  const deviceInfo = collectDeviceInfo();
356
381
  const urlParams = getUrlParams();
357
382
  const browserLocale = getBrowserLocale();
383
+ // 🎯 Check draft mode from URL, localStorage, or cookie
384
+ const draft = isDraftMode();
385
+ // Store draft mode if detected from URL (urlParams may have draft from query string)
386
+ const draftParam = new URLSearchParams(window.location.search).get('draft');
387
+ if (draftParam !== null) {
388
+ setDraftMode(draftParam === 'true');
389
+ }
358
390
  const sessionInitData = {
359
391
  storeId: sessionData.storeId,
360
392
  accountId: sessionData.accountId,
@@ -375,6 +407,7 @@ export class TagadaClient {
375
407
  screenWidth: deviceInfo.screenResolution.width,
376
408
  screenHeight: deviceInfo.screenResolution.height,
377
409
  timeZone: deviceInfo.timeZone,
410
+ draft, // 🎯 Pass draft mode to session init
378
411
  };
379
412
  const response = await this.apiClient.post('/api/v1/cms/session/init', sessionInitData);
380
413
  // Success - reset error tracking
@@ -486,4 +519,289 @@ export class TagadaClient {
486
519
  };
487
520
  return names[code] || code;
488
521
  }
522
+ /**
523
+ * Helper to get accountId with fallbacks
524
+ */
525
+ getAccountId() {
526
+ return this.state.store?.accountId || this.state.pluginConfig?.accountId || this.state.session?.accountId || '';
527
+ }
528
+ /**
529
+ * Update plugin config dynamically (hot-reload)
530
+ * Used for live config editing without page reload
531
+ */
532
+ updatePluginConfig(newConfig) {
533
+ if (this.state.debugMode) {
534
+ console.log('[TagadaClient] Hot-reloading config:', newConfig);
535
+ }
536
+ // Merge new config with existing
537
+ const updatedConfig = {
538
+ ...this.state.pluginConfig.config,
539
+ ...newConfig,
540
+ };
541
+ // Update state
542
+ this.updateState({
543
+ pluginConfig: {
544
+ ...this.state.pluginConfig,
545
+ config: updatedConfig,
546
+ },
547
+ });
548
+ // Emit config update event
549
+ this.bus.emit('CONFIG_UPDATED', updatedConfig);
550
+ if (this.state.debugMode) {
551
+ console.log('[TagadaClient] Config updated successfully');
552
+ }
553
+ }
554
+ /**
555
+ * Setup listener for config updates from parent window (config editor)
556
+ * Enables live config editing via postMessage
557
+ */
558
+ setupConfigHotReload() {
559
+ if (typeof window === 'undefined')
560
+ return;
561
+ const handleMessage = (event) => {
562
+ // Security: Only accept messages from same origin or trusted origins
563
+ // In production, you might want to check event.origin more strictly
564
+ if (event.data?.type === 'TAGADAPAY_CONFIG_UPDATE') {
565
+ const { config } = event.data;
566
+ if (this.state.debugMode) {
567
+ console.log('[TagadaClient] Received config update from parent:', config);
568
+ }
569
+ this.updatePluginConfig(config);
570
+ }
571
+ else if (event.data?.type === 'APPLY_STYLES_TO_ELEMENT') {
572
+ const { elementId, styles } = event.data;
573
+ if (this.state.debugMode) {
574
+ console.log('[TagadaClient] Received style application request:', { elementId, styles });
575
+ }
576
+ this.applyStylesToElement(elementId, styles);
577
+ }
578
+ };
579
+ window.addEventListener('message', handleMessage);
580
+ if (this.state.debugMode) {
581
+ console.log('[TagadaClient] Config hot-reload and style manipulation listeners enabled');
582
+ }
583
+ }
584
+ /**
585
+ * Apply CSS properties to a real div child element for highlighting
586
+ * Used by config editor to highlight elements in preview without altering the element's style
587
+ */
588
+ applyStylesToElement(elementId, styles) {
589
+ if (typeof document === 'undefined')
590
+ return;
591
+ // Always remove any existing highlight, even if the target element is not found
592
+ const staticHighlightId = 'tagada-editor-highlight';
593
+ const existingHighlight = document.getElementById(staticHighlightId);
594
+ if (existingHighlight) {
595
+ existingHighlight.remove();
596
+ }
597
+ const applyToElement = (element) => {
598
+ // List of void/self-closing elements that don't accept children
599
+ const voidElements = new Set([
600
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
601
+ 'link', 'meta', 'param', 'source', 'track', 'wbr'
602
+ ]);
603
+ const isVoidElement = voidElements.has(element.tagName.toLowerCase());
604
+ // For void elements, wrap them in a container div
605
+ let targetElement = element;
606
+ if (isVoidElement) {
607
+ // Check if element is already wrapped (from a previous highlight)
608
+ const parent = element.parentElement;
609
+ const existingWrapper = parent?.getAttribute('data-tagada-highlight-wrapper');
610
+ if (existingWrapper === 'true' && parent instanceof HTMLElement) {
611
+ // Reuse existing wrapper
612
+ targetElement = parent;
613
+ }
614
+ else {
615
+ // Create a new wrapper
616
+ const newWrapper = document.createElement('div');
617
+ newWrapper.setAttribute('data-tagada-highlight-wrapper', 'true');
618
+ // Preserve the element's layout behavior by matching its display style
619
+ // This minimizes the visual impact of the wrapper
620
+ const computedStyle = window.getComputedStyle(element);
621
+ const elementDisplay = computedStyle.display;
622
+ // Match the display type to preserve layout
623
+ // For inline elements, use inline-block (to support position: relative)
624
+ // For all others, use the same display value
625
+ if (elementDisplay === 'inline') {
626
+ newWrapper.style.display = 'inline-block';
627
+ }
628
+ else {
629
+ newWrapper.style.display = elementDisplay;
630
+ }
631
+ newWrapper.style.position = 'relative';
632
+ // Preserve vertical alignment for inline elements
633
+ if (elementDisplay === 'inline' || elementDisplay.includes('inline')) {
634
+ const verticalAlign = computedStyle.verticalAlign;
635
+ if (verticalAlign && verticalAlign !== 'baseline') {
636
+ newWrapper.style.verticalAlign = verticalAlign;
637
+ }
638
+ }
639
+ // Preserve spacing and layout properties from the original element
640
+ // List of CSS properties that affect spacing and layout
641
+ const spacingProperties = [
642
+ // Width and height
643
+ 'width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight',
644
+ // Flex properties
645
+ 'flex', 'flexGrow', 'flexShrink', 'flexBasis',
646
+ // Grid properties
647
+ 'gridColumn', 'gridRow', 'gridColumnStart', 'gridColumnEnd',
648
+ 'gridRowStart', 'gridRowEnd', 'gridArea',
649
+ // Alignment properties
650
+ 'alignSelf', 'justifySelf',
651
+ // Box sizing
652
+ 'boxSizing',
653
+ // Gap (for grid/flex)
654
+ 'gap', 'rowGap', 'columnGap',
655
+ // Order (for flex/grid)
656
+ 'order',
657
+ // Aspect ratio
658
+ 'aspectRatio',
659
+ ];
660
+ spacingProperties.forEach(prop => {
661
+ const camelProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase();
662
+ const value = computedStyle.getPropertyValue(camelProp);
663
+ // Only copy if the value is not empty
664
+ // For most properties, we want to preserve the value even if it's 'auto' or '0px'
665
+ // as these might be intentional layout choices
666
+ if (value && value.trim() !== '') {
667
+ // Handle special cases
668
+ if (prop === 'flex' && value !== 'none' && value !== '0 1 auto') {
669
+ newWrapper.style.flex = value;
670
+ }
671
+ else if (prop === 'boxSizing') {
672
+ newWrapper.style.boxSizing = value;
673
+ }
674
+ else {
675
+ // Use setProperty for kebab-case properties
676
+ // Preserve all values including percentages, auto, etc.
677
+ newWrapper.style.setProperty(camelProp, value);
678
+ }
679
+ }
680
+ });
681
+ // Also check inline styles for spacing properties
682
+ // This handles cases where styles are set directly on the element
683
+ if (element.style) {
684
+ const inlineSpacingProps = [
685
+ 'width', 'height', 'min-width', 'min-height', 'max-width', 'max-height',
686
+ 'flex', 'flex-grow', 'flex-shrink', 'flex-basis',
687
+ 'grid-column', 'grid-row', 'grid-column-start', 'grid-column-end',
688
+ 'grid-row-start', 'grid-row-end', 'grid-area',
689
+ 'align-self', 'justify-self',
690
+ 'box-sizing', 'gap', 'row-gap', 'column-gap', 'order', 'aspect-ratio'
691
+ ];
692
+ inlineSpacingProps.forEach(prop => {
693
+ const inlineValue = element.style.getPropertyValue(prop);
694
+ if (inlineValue) {
695
+ newWrapper.style.setProperty(prop, inlineValue);
696
+ }
697
+ });
698
+ }
699
+ // Insert wrapper before element
700
+ element.parentNode?.insertBefore(newWrapper, element);
701
+ // Move element into wrapper
702
+ newWrapper.appendChild(element);
703
+ targetElement = newWrapper;
704
+ }
705
+ }
706
+ // Ensure element has position relative for absolute child positioning
707
+ const computedStyle = getComputedStyle(isVoidElement ? element : targetElement);
708
+ if (targetElement.style.position === 'static' || !targetElement.style.position) {
709
+ targetElement.style.position = 'relative';
710
+ }
711
+ // Create new highlight div with static ID
712
+ const highlightDiv = document.createElement('div');
713
+ highlightDiv.id = staticHighlightId;
714
+ highlightDiv.style.position = 'absolute';
715
+ highlightDiv.style.inset = '0';
716
+ highlightDiv.style.pointerEvents = 'none';
717
+ highlightDiv.style.zIndex = '9999';
718
+ highlightDiv.style.boxSizing = 'border-box';
719
+ highlightDiv.style.background = 'none';
720
+ highlightDiv.style.border = 'none';
721
+ highlightDiv.style.outline = 'none';
722
+ highlightDiv.style.margin = '0';
723
+ highlightDiv.style.padding = '0';
724
+ // Inherit border radius from parent
725
+ const borderRadius = computedStyle.borderRadius;
726
+ if (borderRadius) {
727
+ highlightDiv.style.borderRadius = borderRadius;
728
+ }
729
+ // Append the new highlight div to the target element
730
+ targetElement.appendChild(highlightDiv);
731
+ // Apply CSS properties to the highlight div
732
+ const stylesToApply = styles || { boxShadow: '0 0 0 2px rgb(239 68 68)' }; // Default red ring
733
+ Object.entries(stylesToApply).forEach(([property, value]) => {
734
+ // Convert kebab-case to camelCase for style object
735
+ const camelProperty = property.includes('-')
736
+ ? property.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase())
737
+ : property;
738
+ // Use setProperty for kebab-case or direct assignment for camelCase
739
+ if (property.includes('-')) {
740
+ highlightDiv.style.setProperty(property, value);
741
+ }
742
+ else {
743
+ highlightDiv.style[camelProperty] = value;
744
+ }
745
+ });
746
+ // Scroll element into view
747
+ element.scrollIntoView({ behavior: 'smooth', block: 'center' });
748
+ if (this.state.debugMode) {
749
+ const appliedStyles = Object.entries(stylesToApply).map(([k, v]) => `${k}: ${v}`).join('; ');
750
+ console.log(`[TagadaClient] Applied styles to highlight div of element #${elementId}:`, appliedStyles);
751
+ }
752
+ };
753
+ // Support multiple IDs in editor-id attribute (space-separated)
754
+ // Use ~= selector to match elementId as a space-separated value
755
+ const maxAttempts = 5;
756
+ const intervalMs = 1000;
757
+ let attempts = 0;
758
+ const isElementHidden = (element) => {
759
+ const style = window.getComputedStyle(element);
760
+ return (style.display === 'none' ||
761
+ style.visibility === 'hidden' ||
762
+ style.opacity === '0' ||
763
+ element.hidden ||
764
+ element.offsetWidth === 0 ||
765
+ element.offsetHeight === 0);
766
+ };
767
+ const findAndApply = () => {
768
+ const elements = document.querySelectorAll(`[editor-id~="${elementId}"]`);
769
+ if (elements.length === 0) {
770
+ attempts += 1;
771
+ if (attempts >= maxAttempts) {
772
+ if (this.state.debugMode) {
773
+ console.warn(`[TagadaClient] Element with editor-id containing "${elementId}" not found after ${maxAttempts} attempts`);
774
+ }
775
+ return;
776
+ }
777
+ if (this.state.debugMode) {
778
+ console.warn(`[TagadaClient] Element with editor-id containing "${elementId}" not found (attempt ${attempts}/${maxAttempts}), retrying in ${intervalMs / 1000}s`);
779
+ }
780
+ setTimeout(findAndApply, intervalMs);
781
+ return;
782
+ }
783
+ // If multiple elements found, prioritize the one that is not hidden
784
+ let element = null;
785
+ if (elements.length === 1) {
786
+ element = elements[0];
787
+ }
788
+ else {
789
+ // Find the first non-hidden element
790
+ for (let i = 0; i < elements.length; i++) {
791
+ if (!isElementHidden(elements[i])) {
792
+ element = elements[i];
793
+ break;
794
+ }
795
+ }
796
+ // If all are hidden, use the first one
797
+ if (!element) {
798
+ element = elements[0];
799
+ }
800
+ }
801
+ if (element) {
802
+ applyToElement(element);
803
+ }
804
+ };
805
+ findAndApply();
806
+ }
489
807
  }
@@ -5,9 +5,14 @@ import { ApiConfig, Environment, EnvironmentConfig } from '../types';
5
5
  * This SDK uses RUNTIME hostname detection, NOT build-time environment variables.
6
6
  * This ensures the SDK always connects to the correct API based on where it's deployed.
7
7
  *
8
- * - Production domains production API
9
- * - Dev/staging domains development API
10
- * - Localhost/local IPs → local API (with optional override via window.__TAGADA_ENV__)
8
+ * Environment detection priority (highest to lowest):
9
+ * 1. **tagadaClientEnv** - Explicit override via URL param, localStorage, or cookie
10
+ * Example: ?tagadaClientEnv=production
11
+ * 2. **Production domains** → production API (app.tagadapay.com)
12
+ * 3. **Dev/staging domains** → development API (app.tagadapay.dev, vercel.app, etc.)
13
+ * 4. **Localhost/local IPs** → local API (localhost, 127.0.0.1, etc.)
14
+ * - Can be overridden via window.__TAGADA_ENV__.TAGADA_ENVIRONMENT
15
+ * 5. **Default fallback** → production API (safest for unknown domains)
11
16
  *
12
17
  * Build-time .env variables (VITE_*, REACT_APP_*, NEXT_PUBLIC_*) are IGNORED
13
18
  * to prevent incorrect API connections when plugins are deployed to different environments.
@@ -18,6 +23,8 @@ import { ApiConfig, Environment, EnvironmentConfig } from '../types';
18
23
  export declare const ENVIRONMENT_CONFIGS: Record<Environment, ApiConfig>;
19
24
  /**
20
25
  * Get the environment configuration based on the current environment
26
+ *
27
+ * Checks for custom base URL override via tagadaClientBaseUrl parameter
21
28
  */
22
29
  export declare function getEnvironmentConfig(environment?: Environment): EnvironmentConfig;
23
30
  /**
@@ -32,6 +39,12 @@ export declare function getEndpointUrl(config: EnvironmentConfig, category: keyo
32
39
  * Auto-detect environment based on hostname and URL patterns at RUNTIME
33
40
  * ⚠️ IMPORTANT: Ignores build-time .env variables to ensure correct detection in all environments
34
41
  * .env variables are ONLY used for local development via window.__TAGADA_ENV__
42
+ *
43
+ * Priority (highest to lowest):
44
+ * 1. tagadaClientEnv - Explicit override via URL/localStorage/cookie
45
+ * 2. __TAGADA_ENV__ - Local development override
46
+ * 3. Hostname-based detection - Production/staging domains
47
+ * 4. Default fallback - Production (safest)
35
48
  */
36
49
  export declare function detectEnvironment(): Environment;
37
50
  /**