@tagadapay/plugin-sdk 3.1.25 → 4.0.0

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 (48) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +499 -499
  3. package/dist/external-tracker.js +5 -5
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +2 -2
  6. package/dist/react/config/payment.d.ts +2 -2
  7. package/dist/react/config/payment.js +5 -5
  8. package/dist/react/hooks/usePayment.d.ts +7 -0
  9. package/dist/react/hooks/usePayment.js +1 -0
  10. package/dist/react/providers/TagadaProvider.js +5 -5
  11. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  12. package/dist/tagada-react-sdk-minimal.min.js.map +4 -4
  13. package/dist/tagada-react-sdk.js +1616 -1275
  14. package/dist/tagada-react-sdk.min.js +2 -2
  15. package/dist/tagada-react-sdk.min.js.map +4 -4
  16. package/dist/tagada-sdk.js +869 -27
  17. package/dist/tagada-sdk.min.js +2 -2
  18. package/dist/tagada-sdk.min.js.map +4 -4
  19. package/dist/v2/core/config/environment.d.ts +3 -3
  20. package/dist/v2/core/config/environment.js +7 -7
  21. package/dist/v2/core/funnelClient.d.ts +2 -0
  22. package/dist/v2/core/resources/funnel.d.ts +1 -1
  23. package/dist/v2/core/resources/geo.d.ts +50 -0
  24. package/dist/v2/core/resources/geo.js +35 -0
  25. package/dist/v2/core/resources/payments.d.ts +19 -1
  26. package/dist/v2/core/resources/payments.js +8 -0
  27. package/dist/v2/core/utils/previewModeIndicator.js +101 -101
  28. package/dist/v2/react/components/FunnelScriptInjector.js +167 -19
  29. package/dist/v2/react/components/StripeExpressButton.d.ts +8 -0
  30. package/dist/v2/react/components/StripeExpressButton.js +22 -1
  31. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.d.ts +15 -0
  32. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.js +166 -0
  33. package/dist/v2/react/hooks/payment-actions/usePaymentActionHandler.js +12 -0
  34. package/dist/v2/react/hooks/payment-processing/usePaymentProcessors.js +1 -0
  35. package/dist/v2/react/hooks/useISOData.js +25 -7
  36. package/dist/v2/react/hooks/usePaymentPolling.d.ts +1 -1
  37. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +12 -4
  38. package/dist/v2/react/providers/TagadaProvider.js +5 -5
  39. package/dist/v2/standalone/apple-pay-service.d.ts +12 -0
  40. package/dist/v2/standalone/apple-pay-service.js +12 -0
  41. package/dist/v2/standalone/external-tracker.d.ts +1 -1
  42. package/dist/v2/standalone/google-pay-service.d.ts +9 -0
  43. package/dist/v2/standalone/google-pay-service.js +9 -0
  44. package/dist/v2/standalone/index.d.ts +8 -1
  45. package/dist/v2/standalone/index.js +7 -0
  46. package/dist/v2/standalone/payment-service.d.ts +18 -5
  47. package/dist/v2/standalone/payment-service.js +62 -9
  48. package/package.json +115 -114
@@ -8,11 +8,11 @@ import { ApiConfig, Environment, EnvironmentConfig } from '../types';
8
8
  * Environment detection priority (highest to lowest):
9
9
  * 1. **tagadaClientEnv** - Explicit override via URL param, localStorage, or cookie
10
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.)
11
+ * 2. **Production domains** → production API (api.tagada.io)
12
+ * 3. **Dev/staging domains** → development API (api.tagada.dev, vercel.app, etc.)
13
13
  * 4. **Localhost/local IPs** → local API (localhost, 127.0.0.1, etc.)
14
14
  * - Can be overridden via window.__TAGADA_ENV__.TAGADA_ENVIRONMENT
15
- * 5. **Default fallback** → production API (safest for unknown domains)
15
+ * 5. **Default fallback** → production API (safest)
16
16
  *
17
17
  * Build-time .env variables (VITE_*, REACT_APP_*, NEXT_PUBLIC_*) are IGNORED
18
18
  * to prevent incorrect API connections when plugins are deployed to different environments.
@@ -19,11 +19,11 @@ function getCookie(name) {
19
19
  * Environment detection priority (highest to lowest):
20
20
  * 1. **tagadaClientEnv** - Explicit override via URL param, localStorage, or cookie
21
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.)
22
+ * 2. **Production domains** → production API (api.tagada.io)
23
+ * 3. **Dev/staging domains** → development API (api.tagada.dev, vercel.app, etc.)
24
24
  * 4. **Localhost/local IPs** → local API (localhost, 127.0.0.1, etc.)
25
25
  * - Can be overridden via window.__TAGADA_ENV__.TAGADA_ENVIRONMENT
26
- * 5. **Default fallback** → production API (safest for unknown domains)
26
+ * 5. **Default fallback** → production API (safest)
27
27
  *
28
28
  * Build-time .env variables (VITE_*, REACT_APP_*, NEXT_PUBLIC_*) are IGNORED
29
29
  * to prevent incorrect API connections when plugins are deployed to different environments.
@@ -33,7 +33,7 @@ function getCookie(name) {
33
33
  */
34
34
  export const ENVIRONMENT_CONFIGS = {
35
35
  production: {
36
- baseUrl: 'https://app.tagadapay.com',
36
+ baseUrl: 'https://api.tagada.io',
37
37
  endpoints: {
38
38
  checkout: {
39
39
  sessionInit: '/api/v1/checkout/session/init',
@@ -51,7 +51,7 @@ export const ENVIRONMENT_CONFIGS = {
51
51
  },
52
52
  },
53
53
  development: {
54
- baseUrl: 'https://app.tagadapay.dev',
54
+ baseUrl: 'https://api.tagada.dev',
55
55
  endpoints: {
56
56
  checkout: {
57
57
  sessionInit: '/api/v1/checkout/session/init',
@@ -213,13 +213,13 @@ export function detectEnvironment() {
213
213
  return 'local';
214
214
  }
215
215
  // 2. Production: deployed to production domains
216
- if (hostname === 'app.tagadapay.com' ||
216
+ if (hostname.includes('tagada.io') ||
217
217
  hostname.includes('tagadapay.com') ||
218
218
  hostname.includes('yourproductiondomain.com')) {
219
219
  return 'production';
220
220
  }
221
221
  // 3. Development: deployed to staging/dev domains or has dev indicators
222
- if (hostname === 'app.tagadapay.dev' ||
222
+ if (hostname.includes('tagada.dev') ||
223
223
  hostname.includes('tagadapay.dev') ||
224
224
  hostname.includes('vercel.app') ||
225
225
  hostname.includes('netlify.app') ||
@@ -137,6 +137,8 @@ export interface PaymentMethodConfig {
137
137
  note?: string;
138
138
  }>;
139
139
  metadata?: Record<string, any>;
140
+ /** Integration settings (e.g. custom_payment checkout instructions, thank-you text) */
141
+ settings?: Record<string, any>;
140
142
  }
141
143
  export type PaymentSetupConfig = Record<string, PaymentMethodConfig>;
142
144
  /** Get all enabled entries from a paymentSetupConfig */
@@ -405,7 +405,7 @@ export interface SimpleFunnelContext<TCustom = {}> {
405
405
  * ✅ Environment context (staging or production)
406
406
  * - Determined at session initialization based on entry URL
407
407
  * - Ensures all navigation stays in the same environment
408
- * - 'staging': Uses funnel.config (alias domains like funnel--store.cdn.tagadapay.com)
408
+ * - 'staging': Uses funnel.config (alias domains like funnel--store.cdn.tagada.io)
409
409
  * - 'production': Uses funnel.productionConfig (custom domains)
410
410
  */
411
411
  environment?: 'staging' | 'production';
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Geo Resource Client
3
+ * API client for IP geolocation endpoint
4
+ */
5
+ import { ApiClient } from './apiClient';
6
+ export interface GeoLocationData {
7
+ ip_address?: string | null;
8
+ city?: string | null;
9
+ region?: string | null;
10
+ region_iso_code?: string | null;
11
+ postal_code?: string | null;
12
+ country?: string | null;
13
+ country_code?: string | null;
14
+ country_is_eu?: boolean | null;
15
+ continent?: string | null;
16
+ continent_code?: string | null;
17
+ latitude?: number | null;
18
+ longitude?: number | null;
19
+ security?: {
20
+ is_vpn?: boolean | null;
21
+ } | null;
22
+ timezone?: {
23
+ name?: string | null;
24
+ abbreviation?: string | null;
25
+ gmt_offset?: number | null;
26
+ current_time?: string | null;
27
+ is_dst?: boolean | null;
28
+ } | null;
29
+ currency?: {
30
+ currency_name?: string | null;
31
+ currency_code?: string | null;
32
+ } | null;
33
+ }
34
+ export declare class GeoResource {
35
+ private apiClient;
36
+ constructor(apiClient: ApiClient);
37
+ /**
38
+ * Get geolocation data from the visitor's IP address
39
+ */
40
+ getGeoLocation(): Promise<GeoLocationData>;
41
+ /**
42
+ * Get geolocation data for a specific IP address
43
+ */
44
+ getGeoLocationByIp(ip: string): Promise<GeoLocationData>;
45
+ /**
46
+ * Convenience: get just the country code from IP geolocation
47
+ * Returns ISO 3166-1 alpha-2 code (e.g. 'US', 'FR') or null
48
+ */
49
+ detectCountry(): Promise<string | null>;
50
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Geo Resource Client
3
+ * API client for IP geolocation endpoint
4
+ */
5
+ export class GeoResource {
6
+ constructor(apiClient) {
7
+ this.apiClient = apiClient;
8
+ }
9
+ /**
10
+ * Get geolocation data from the visitor's IP address
11
+ */
12
+ async getGeoLocation() {
13
+ return this.apiClient.get('/api/v1/geo');
14
+ }
15
+ /**
16
+ * Get geolocation data for a specific IP address
17
+ */
18
+ async getGeoLocationByIp(ip) {
19
+ return this.apiClient.get(`/api/v1/geo?ip=${encodeURIComponent(ip)}`);
20
+ }
21
+ /**
22
+ * Convenience: get just the country code from IP geolocation
23
+ * Returns ISO 3166-1 alpha-2 code (e.g. 'US', 'FR') or null
24
+ */
25
+ async detectCountry() {
26
+ try {
27
+ const data = await this.getGeoLocation();
28
+ const code = data?.country_code;
29
+ return code && typeof code === 'string' && code.length === 2 ? code : null;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ }
@@ -10,7 +10,7 @@ export interface Payment {
10
10
  subStatus: string;
11
11
  requireAction: 'none' | 'redirect' | 'redirect_to_payment' | 'error' | 'radar' | 'stripe_express_checkout';
12
12
  requireActionData?: {
13
- type: 'redirect' | 'redirect_to_payment' | 'threeds_auth' | 'processor_auth' | 'error' | 'stripe_radar' | 'finix_radar' | 'radar' | 'kesspay_auth' | 'trustflow_auth' | 'mastercard_auth' | 'stripe_express_checkout';
13
+ type: 'redirect' | 'redirect_to_payment' | 'threeds_auth' | 'processor_auth' | 'error' | 'stripe_radar' | 'finix_radar' | 'radar' | 'kesspay_auth' | 'trustflow_auth' | 'mastercard_auth' | 'stripe_express_checkout' | 'ngenius_3ds';
14
14
  url?: string;
15
15
  processed: boolean;
16
16
  processorId?: string;
@@ -112,6 +112,14 @@ export interface PaymentOptions {
112
112
  * Payment method type (e.g., 'klarna', 'afterpay', 'paypal')
113
113
  */
114
114
  paymentMethod?: string;
115
+ /**
116
+ * Shipping rate selected by the customer at checkout. Forwarded to
117
+ * `processPaymentDirect` so the order is created with the right shipping
118
+ * even if the session's stored rate hasn't fully round-tripped or got
119
+ * cleared (race conditions). The backend treats this as authoritative
120
+ * when present, otherwise falls back to the session's stored rate.
121
+ */
122
+ shippingRateId?: string;
115
123
  /** @deprecated Use onPaymentSuccess instead - this will be removed in v3 */
116
124
  onSuccess?: (response: PaymentResponse) => void;
117
125
  /** @deprecated Use onPaymentFailed instead - this will be removed in v3 */
@@ -247,6 +255,7 @@ export declare class PaymentsResource {
247
255
  processorId?: string;
248
256
  paymentMethod?: string;
249
257
  isExpress?: boolean;
258
+ shippingRateId?: string;
250
259
  }): Promise<PaymentResponse>;
251
260
  /**
252
261
  * Get card payment instruments for customer
@@ -304,4 +313,13 @@ export declare class PaymentsResource {
304
313
  status: 'succeeded' | 'failed' | 'pending';
305
314
  paymentIntentId: string;
306
315
  }): Promise<Payment>;
316
+ /**
317
+ * Complete N-Genius payment after WebSDK 3DS flow finishes.
318
+ * Verifies state from N-Genius server-side and updates the payment record.
319
+ */
320
+ ngeniusThreedsComplete(data: {
321
+ paymentId: string;
322
+ orderReference: string;
323
+ paymentReference: string;
324
+ }): Promise<Payment>;
307
325
  }
@@ -148,6 +148,7 @@ export class PaymentsResource {
148
148
  ...(options.processorId && { processorId: options.processorId }),
149
149
  ...(options.paymentMethod && { paymentMethod: options.paymentMethod }),
150
150
  ...(options.isExpress && { isExpress: options.isExpress }),
151
+ ...(options.shippingRateId && { shippingRateId: options.shippingRateId }),
151
152
  };
152
153
  console.log('[PaymentsResource] Request body being sent:', JSON.stringify(requestBody, null, 2));
153
154
  const response = await this.apiClient.post('/api/public/v1/checkout/pay-v2', requestBody);
@@ -206,4 +207,11 @@ export class PaymentsResource {
206
207
  async updateThreedsStatus(data) {
207
208
  return this.apiClient.post('/api/v1/threeds/status', data);
208
209
  }
210
+ /**
211
+ * Complete N-Genius payment after WebSDK 3DS flow finishes.
212
+ * Verifies state from N-Genius server-side and updates the payment record.
213
+ */
214
+ async ngeniusThreedsComplete(data) {
215
+ return this.apiClient.post('/api/v1/payments/ngenius/threeds-complete', data);
216
+ }
209
217
  }
@@ -212,143 +212,143 @@ export function injectPreviewModeIndicator() {
212
212
  // Create container
213
213
  const container = document.createElement('div');
214
214
  container.id = 'tgd-preview-indicator';
215
- container.style.cssText = `
216
- position: fixed;
217
- bottom: 16px;
218
- right: 16px;
219
- z-index: 999999;
220
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
215
+ container.style.cssText = `
216
+ position: fixed;
217
+ bottom: 16px;
218
+ right: 16px;
219
+ z-index: 999999;
220
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
221
221
  `;
222
222
  // Create badge
223
223
  const badge = document.createElement('div');
224
- badge.style.cssText = `
225
- background: ${draftMode ? '#ff9500' : '#007aff'};
226
- color: white;
227
- padding: 8px 12px;
228
- border-radius: 8px;
229
- font-size: 13px;
230
- font-weight: 600;
231
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
232
- cursor: pointer;
233
- transition: all 0.2s ease;
234
- display: flex;
235
- align-items: center;
236
- gap: 6px;
224
+ badge.style.cssText = `
225
+ background: ${draftMode ? '#ff9500' : '#007aff'};
226
+ color: white;
227
+ padding: 8px 12px;
228
+ border-radius: 8px;
229
+ font-size: 13px;
230
+ font-weight: 600;
231
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
232
+ cursor: pointer;
233
+ transition: all 0.2s ease;
234
+ display: flex;
235
+ align-items: center;
236
+ gap: 6px;
237
237
  `;
238
- badge.innerHTML = `
239
- <span style="font-size: 16px;">🔍</span>
240
- <span>${draftMode ? 'Preview Mode' : 'Dev Mode'}</span>
238
+ badge.innerHTML = `
239
+ <span style="font-size: 16px;">🔍</span>
240
+ <span>${draftMode ? 'Preview Mode' : 'Dev Mode'}</span>
241
241
  `;
242
242
  // Create details popup (with padding-top to bridge gap with badge)
243
243
  const details = document.createElement('div');
244
- details.style.cssText = `
245
- position: absolute;
246
- bottom: calc(100% + 8px);
247
- right: 0;
248
- background: white;
249
- border: 1px solid #e5e5e5;
250
- border-radius: 8px;
251
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
252
- padding: 12px;
253
- min-width: 250px;
254
- font-size: 12px;
255
- line-height: 1.5;
256
- display: none;
244
+ details.style.cssText = `
245
+ position: absolute;
246
+ bottom: calc(100% + 8px);
247
+ right: 0;
248
+ background: white;
249
+ border: 1px solid #e5e5e5;
250
+ border-radius: 8px;
251
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
252
+ padding: 12px;
253
+ min-width: 250px;
254
+ font-size: 12px;
255
+ line-height: 1.5;
256
+ display: none;
257
257
  `;
258
258
  details.style.paddingTop = '20px'; // Extra padding to bridge the gap
259
259
  // Add invisible bridge between badge and popup to prevent flickering
260
260
  const bridge = document.createElement('div');
261
- bridge.style.cssText = `
262
- position: absolute;
263
- bottom: 100%;
264
- left: 0;
265
- right: 0;
266
- height: 8px;
267
- display: none;
261
+ bridge.style.cssText = `
262
+ position: absolute;
263
+ bottom: 100%;
264
+ left: 0;
265
+ right: 0;
266
+ height: 8px;
267
+ display: none;
268
268
  `;
269
269
  // Build details content
270
270
  let detailsHTML = '<div style="margin-bottom: 8px; font-weight: 600; color: #1d1d1f;">Current Environment</div>';
271
271
  detailsHTML += '<div style="display: flex; flex-direction: column; gap: 6px;">';
272
272
  if (draftMode) {
273
- detailsHTML += `
274
- <div style="display: flex; justify-content: space-between; color: #86868b;">
275
- <span>Draft Mode:</span>
276
- <span style="color: #ff9500; font-weight: 600;">ON</span>
277
- </div>
273
+ detailsHTML += `
274
+ <div style="display: flex; justify-content: space-between; color: #86868b;">
275
+ <span>Draft Mode:</span>
276
+ <span style="color: #ff9500; font-weight: 600;">ON</span>
277
+ </div>
278
278
  `;
279
279
  }
280
280
  if (trackingDisabled) {
281
- detailsHTML += `
282
- <div style="display: flex; justify-content: space-between; color: #86868b;">
283
- <span>Tracking:</span>
284
- <span style="color: #ff3b30; font-weight: 600;">DISABLED</span>
285
- </div>
281
+ detailsHTML += `
282
+ <div style="display: flex; justify-content: space-between; color: #86868b;">
283
+ <span>Tracking:</span>
284
+ <span style="color: #ff3b30; font-weight: 600;">DISABLED</span>
285
+ </div>
286
286
  `;
287
287
  }
288
288
  if (params.funnelEnv) {
289
- detailsHTML += `
290
- <div style="display: flex; justify-content: space-between; color: #86868b;">
291
- <span>Funnel Env:</span>
292
- <span style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 11px;">
293
- ${params.funnelEnv}
294
- </span>
295
- </div>
289
+ detailsHTML += `
290
+ <div style="display: flex; justify-content: space-between; color: #86868b;">
291
+ <span>Funnel Env:</span>
292
+ <span style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 11px;">
293
+ ${params.funnelEnv}
294
+ </span>
295
+ </div>
296
296
  `;
297
297
  }
298
298
  if (params.tagadaClientEnv) {
299
- detailsHTML += `
300
- <div style="display: flex; justify-content: space-between; color: #86868b;">
301
- <span>API Env:</span>
302
- <span style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 11px;">
303
- ${params.tagadaClientEnv}
304
- </span>
305
- </div>
299
+ detailsHTML += `
300
+ <div style="display: flex; justify-content: space-between; color: #86868b;">
301
+ <span>API Env:</span>
302
+ <span style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 11px;">
303
+ ${params.tagadaClientEnv}
304
+ </span>
305
+ </div>
306
306
  `;
307
307
  }
308
308
  if (params.tagadaClientBaseUrl) {
309
- detailsHTML += `
310
- <div style="color: #86868b;">
311
- <div style="margin-bottom: 4px;">API URL:</div>
312
- <div style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 10px; word-break: break-all; background: #f5f5f7; padding: 4px 6px; border-radius: 4px;">
313
- ${params.tagadaClientBaseUrl}
314
- </div>
315
- </div>
309
+ detailsHTML += `
310
+ <div style="color: #86868b;">
311
+ <div style="margin-bottom: 4px;">API URL:</div>
312
+ <div style="color: #1d1d1f; font-weight: 600; font-family: monospace; font-size: 10px; word-break: break-all; background: #f5f5f7; padding: 4px 6px; border-radius: 4px;">
313
+ ${params.tagadaClientBaseUrl}
314
+ </div>
315
+ </div>
316
316
  `;
317
317
  }
318
318
  if (params.funnelId) {
319
- detailsHTML += `
320
- <div style="color: #86868b; margin-top: 4px; padding-top: 8px; border-top: 1px solid #e5e5e5;">
321
- <div style="margin-bottom: 4px;">Funnel ID:</div>
322
- <div style="color: #1d1d1f; font-family: monospace; font-size: 10px; word-break: break-all; background: #f5f5f7; padding: 4px 6px; border-radius: 4px;">
323
- ${params.funnelId}
324
- </div>
325
- </div>
319
+ detailsHTML += `
320
+ <div style="color: #86868b; margin-top: 4px; padding-top: 8px; border-top: 1px solid #e5e5e5;">
321
+ <div style="margin-bottom: 4px;">Funnel ID:</div>
322
+ <div style="color: #1d1d1f; font-family: monospace; font-size: 10px; word-break: break-all; background: #f5f5f7; padding: 4px 6px; border-radius: 4px;">
323
+ ${params.funnelId}
324
+ </div>
325
+ </div>
326
326
  `;
327
327
  }
328
328
  detailsHTML += '</div>';
329
- detailsHTML += `
330
- <div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #e5e5e5; font-size: 11px; color: #86868b; text-align: center;">
331
- Add <code style="background: #f5f5f7; padding: 2px 4px; border-radius: 3px;">?forceReset=true</code> to reset
332
- </div>
329
+ detailsHTML += `
330
+ <div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #e5e5e5; font-size: 11px; color: #86868b; text-align: center;">
331
+ Add <code style="background: #f5f5f7; padding: 2px 4px; border-radius: 3px;">?forceReset=true</code> to reset
332
+ </div>
333
333
  `;
334
334
  // Add action button
335
- detailsHTML += `
336
- <div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #e5e5e5;">
337
- <button id="tgd-leave-preview" style="
338
- background: #ff3b30;
339
- color: white;
340
- border: none;
341
- border-radius: 6px;
342
- padding: 10px 12px;
343
- font-size: 13px;
344
- font-weight: 600;
345
- cursor: pointer;
346
- transition: opacity 0.2s;
347
- width: 100%;
348
- ">
349
- 🚪 Leave Preview Mode
350
- </button>
351
- </div>
335
+ detailsHTML += `
336
+ <div style="margin-top: 12px; padding-top: 8px; border-top: 1px solid #e5e5e5;">
337
+ <button id="tgd-leave-preview" style="
338
+ background: #ff3b30;
339
+ color: white;
340
+ border: none;
341
+ border-radius: 6px;
342
+ padding: 10px 12px;
343
+ font-size: 13px;
344
+ font-weight: 600;
345
+ cursor: pointer;
346
+ transition: opacity 0.2s;
347
+ width: 100%;
348
+ ">
349
+ 🚪 Leave Preview Mode
350
+ </button>
351
+ </div>
352
352
  `;
353
353
  details.innerHTML = detailsHTML;
354
354
  // Hover behavior - keep popup visible when hovering over badge, bridge, or popup