@tagadapay/plugin-sdk 3.1.24 → 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 (69) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +499 -499
  3. package/dist/external-tracker.js +247 -2875
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/config/payment.d.ts +2 -2
  7. package/dist/react/config/payment.js +5 -5
  8. package/dist/react/hooks/useCheckout.js +7 -2
  9. package/dist/react/hooks/usePayment.d.ts +7 -0
  10. package/dist/react/hooks/usePayment.js +1 -0
  11. package/dist/react/providers/TagadaProvider.js +5 -5
  12. package/dist/tagada-react-sdk-minimal.min.js +2 -2
  13. package/dist/tagada-react-sdk-minimal.min.js.map +4 -4
  14. package/dist/tagada-react-sdk.js +1680 -1172
  15. package/dist/tagada-react-sdk.min.js +2 -2
  16. package/dist/tagada-react-sdk.min.js.map +4 -4
  17. package/dist/tagada-sdk.js +1701 -3410
  18. package/dist/tagada-sdk.min.js +2 -2
  19. package/dist/tagada-sdk.min.js.map +4 -4
  20. package/dist/v2/core/client.js +1 -0
  21. package/dist/v2/core/config/environment.d.ts +3 -3
  22. package/dist/v2/core/config/environment.js +7 -7
  23. package/dist/v2/core/funnelClient.d.ts +10 -0
  24. package/dist/v2/core/funnelClient.js +1 -1
  25. package/dist/v2/core/resources/apiClient.d.ts +18 -14
  26. package/dist/v2/core/resources/apiClient.js +151 -109
  27. package/dist/v2/core/resources/checkout.d.ts +1 -1
  28. package/dist/v2/core/resources/funnel.d.ts +1 -1
  29. package/dist/v2/core/resources/geo.d.ts +50 -0
  30. package/dist/v2/core/resources/geo.js +35 -0
  31. package/dist/v2/core/resources/index.d.ts +1 -1
  32. package/dist/v2/core/resources/index.js +1 -1
  33. package/dist/v2/core/resources/offers.js +4 -4
  34. package/dist/v2/core/resources/payments.d.ts +20 -1
  35. package/dist/v2/core/resources/payments.js +8 -0
  36. package/dist/v2/core/utils/currency.d.ts +3 -0
  37. package/dist/v2/core/utils/currency.js +40 -2
  38. package/dist/v2/core/utils/deviceInfo.d.ts +1 -0
  39. package/dist/v2/core/utils/deviceInfo.js +1 -0
  40. package/dist/v2/core/utils/previewMode.js +12 -0
  41. package/dist/v2/core/utils/previewModeIndicator.js +101 -101
  42. package/dist/v2/react/components/ApplePayButton.js +39 -16
  43. package/dist/v2/react/components/FunnelScriptInjector.js +167 -19
  44. package/dist/v2/react/components/StripeExpressButton.d.ts +8 -0
  45. package/dist/v2/react/components/StripeExpressButton.js +23 -3
  46. package/dist/v2/react/hooks/payment-actions/useAirwallexRadarAction.js +1 -0
  47. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.d.ts +15 -0
  48. package/dist/v2/react/hooks/payment-actions/useNgeniusThreedsAction.js +166 -0
  49. package/dist/v2/react/hooks/payment-actions/usePaymentActionHandler.js +12 -0
  50. package/dist/v2/react/hooks/payment-processing/usePaymentProcessors.js +1 -0
  51. package/dist/v2/react/hooks/useApiQuery.d.ts +1 -1
  52. package/dist/v2/react/hooks/useApiQuery.js +1 -1
  53. package/dist/v2/react/hooks/useCheckoutQuery.js +6 -2
  54. package/dist/v2/react/hooks/useISOData.js +25 -7
  55. package/dist/v2/react/hooks/usePaymentPolling.d.ts +1 -1
  56. package/dist/v2/react/hooks/usePreviewOffer.js +1 -1
  57. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +7 -0
  58. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +105 -9
  59. package/dist/v2/react/providers/TagadaProvider.js +6 -6
  60. package/dist/v2/standalone/apple-pay-service.d.ts +12 -0
  61. package/dist/v2/standalone/apple-pay-service.js +12 -0
  62. package/dist/v2/standalone/external-tracker.d.ts +1 -1
  63. package/dist/v2/standalone/google-pay-service.d.ts +9 -0
  64. package/dist/v2/standalone/google-pay-service.js +9 -0
  65. package/dist/v2/standalone/index.d.ts +8 -1
  66. package/dist/v2/standalone/index.js +7 -0
  67. package/dist/v2/standalone/payment-service.d.ts +18 -5
  68. package/dist/v2/standalone/payment-service.js +63 -9
  69. package/package.json +115 -114
@@ -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
@@ -279,6 +288,7 @@ export declare class PaymentsResource {
279
288
  error?: string;
280
289
  }>;
281
290
  saveRadarSession(data: {
291
+ paymentId?: string;
282
292
  orderId?: string;
283
293
  checkoutSessionId?: string;
284
294
  finixRadarSessionId?: string;
@@ -303,4 +313,13 @@ export declare class PaymentsResource {
303
313
  status: 'succeeded' | 'failed' | 'pending';
304
314
  paymentIntentId: string;
305
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>;
306
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
  }
@@ -7,6 +7,8 @@ export interface Currency {
7
7
  symbol: string;
8
8
  name: string;
9
9
  decimalPlaces: number;
10
+ /** True when the currency was explicitly set via URL param or persisted storage (not a SDK/store default) */
11
+ isExplicit: boolean;
10
12
  }
11
13
  /**
12
14
  * Format money amount from minor units (cents) to a formatted string
@@ -27,6 +29,7 @@ export declare class CurrencyUtils {
27
29
  * Get currency from context or fallback to default
28
30
  */
29
31
  static getCurrency(context: any, defaultCurrency?: string): Currency;
32
+ private static getCookieValue;
30
33
  /**
31
34
  * Get currency symbol
32
35
  */
@@ -47,9 +47,25 @@ export class CurrencyUtils {
47
47
  * Get currency from context or fallback to default
48
48
  */
49
49
  static getCurrency(context, defaultCurrency = 'USD') {
50
- // Handle case where context.currency might be a Currency object or string
51
50
  let currencyCode;
52
- if (typeof context?.currency === 'string') {
51
+ let isExplicit = false;
52
+ // 1. URL ?currency= param takes highest priority (set by CRM preview, storefront links, or currency selector)
53
+ const urlCurrency = typeof window !== 'undefined'
54
+ ? new URLSearchParams(window.location.search).get('currency')
55
+ : null;
56
+ // 2. Persisted tgd_currency from storage/cookie (survives navigation between funnel steps)
57
+ const storedCurrency = typeof window !== 'undefined'
58
+ ? (localStorage.getItem('tgd_currency') || CurrencyUtils.getCookieValue('tgd_currency'))
59
+ : null;
60
+ if (urlCurrency) {
61
+ currencyCode = urlCurrency.toUpperCase();
62
+ isExplicit = true;
63
+ }
64
+ else if (storedCurrency) {
65
+ currencyCode = storedCurrency.toUpperCase();
66
+ isExplicit = true;
67
+ }
68
+ else if (typeof context?.currency === 'string') {
53
69
  currencyCode = context.currency;
54
70
  }
55
71
  else if (context?.currency?.code) {
@@ -61,13 +77,35 @@ export class CurrencyUtils {
61
77
  else {
62
78
  currencyCode = defaultCurrency;
63
79
  }
80
+ // Validate against store's presentment currencies when available.
81
+ // This catches both explicit overrides (URL/storage) and fallback values that
82
+ // reference a currency the store doesn't support.
83
+ const presentment = context?.store?.presentmentCurrencies;
84
+ if (presentment?.length && !presentment.includes(currencyCode)) {
85
+ console.warn(`[CurrencyUtils] Currency "${currencyCode}" is not in store presentmentCurrencies [${presentment.join(', ')}]. Falling back to ${presentment[0]}.`);
86
+ currencyCode = presentment[0];
87
+ // Update persisted storage so subsequent renders don't keep requesting the unsupported currency
88
+ if (isExplicit && typeof window !== 'undefined') {
89
+ try {
90
+ localStorage.setItem('tgd_currency', currencyCode);
91
+ }
92
+ catch { }
93
+ }
94
+ }
64
95
  return {
65
96
  code: currencyCode,
66
97
  symbol: this.getCurrencySymbol(currencyCode),
67
98
  name: this.getCurrencyName(currencyCode),
68
99
  decimalPlaces: this.getDecimalPlaces(currencyCode),
100
+ isExplicit,
69
101
  };
70
102
  }
103
+ static getCookieValue(name) {
104
+ if (typeof document === 'undefined')
105
+ return null;
106
+ const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`));
107
+ return match ? decodeURIComponent(match[1]) : null;
108
+ }
71
109
  /**
72
110
  * Get currency symbol
73
111
  */
@@ -43,4 +43,5 @@ export declare function getUrlParams(): {
43
43
  utmSource?: string;
44
44
  utmMedium?: string;
45
45
  utmCampaign?: string;
46
+ gclid?: string;
46
47
  };
@@ -202,5 +202,6 @@ export function getUrlParams() {
202
202
  utmSource: params.get('utm_source') || undefined,
203
203
  utmMedium: params.get('utm_medium') || undefined,
204
204
  utmCampaign: params.get('utm_campaign') || undefined,
205
+ gclid: params.get('gclid') || undefined,
205
206
  };
206
207
  }
@@ -360,6 +360,18 @@ function persistSDKParamsFromURL() {
360
360
  if (urlBaseUrl) {
361
361
  setClientBaseUrl(urlBaseUrl);
362
362
  }
363
+ // Persist currency if in URL (survives navigation between funnel steps)
364
+ const urlCurrency = urlParams.get('currency');
365
+ if (urlCurrency) {
366
+ setInStorage(STORAGE_KEYS.CURRENCY, urlCurrency.toUpperCase());
367
+ setInCookie(STORAGE_KEYS.CURRENCY, urlCurrency.toUpperCase(), 86400);
368
+ }
369
+ // Persist locale if in URL
370
+ const urlLocale = urlParams.get('locale');
371
+ if (urlLocale) {
372
+ setInStorage(STORAGE_KEYS.LOCALE, urlLocale);
373
+ setInCookie(STORAGE_KEYS.LOCALE, urlLocale, 86400);
374
+ }
363
375
  }
364
376
  /**
365
377
  * Set funnel tracking mode in storage for persistence
@@ -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
@@ -24,6 +24,30 @@ const applePayContactToAddress = (contact) => {
24
24
  email: contact?.emailAddress || '',
25
25
  };
26
26
  };
27
+ const APPLE_PAY_SDK_URL = 'https://applepay.cdn-apple.com/jsapi/1.latest/apple-pay-sdk.js';
28
+ let applePaySdkPromise = null;
29
+ function loadApplePaySdk() {
30
+ if (applePaySdkPromise)
31
+ return applePaySdkPromise;
32
+ // Check if the script is already in the DOM (e.g. from a previous load)
33
+ if (document.querySelector(`script[src="${APPLE_PAY_SDK_URL}"]`)) {
34
+ applePaySdkPromise = Promise.resolve();
35
+ return applePaySdkPromise;
36
+ }
37
+ applePaySdkPromise = new Promise((resolve, reject) => {
38
+ const script = document.createElement('script');
39
+ script.src = APPLE_PAY_SDK_URL;
40
+ script.crossOrigin = 'anonymous';
41
+ script.async = true;
42
+ script.onload = () => resolve();
43
+ script.onerror = () => {
44
+ applePaySdkPromise = null;
45
+ reject(new Error('[ApplePay] Failed to load Apple Pay SDK'));
46
+ };
47
+ document.head.appendChild(script);
48
+ });
49
+ return applePaySdkPromise;
50
+ }
27
51
  export const ApplePayButton = ({ checkout, onSuccess, onError, onCancel }) => {
28
52
  const { applePayPaymentMethod, reComputeOrderSummary, shippingMethods, lineItems, handleAddExpressId, updateCheckoutSessionValues, updateCustomerEmail, setError: setContextError, } = useExpressPaymentMethods();
29
53
  const [processingPayment, setProcessingPayment] = useState(false);
@@ -38,25 +62,24 @@ export const ApplePayButton = ({ checkout, onSuccess, onError, onCancel }) => {
38
62
  if (!applePayPaymentMethod) {
39
63
  return null;
40
64
  }
41
- // Check Apple Pay availability (matches CMS pattern - useApplePayAvailable hook)
65
+ // Load SDK on demand, then check Apple Pay availability
42
66
  useEffect(() => {
43
- const addExpress = () => handleAddExpressId('apple_pay');
44
- try {
45
- // Apple Pay requires a secure context (HTTPS). On HTTP (like localhost),
46
- // calling canMakePayments() throws InvalidAccessError
47
- if (window?.ApplePaySession && ApplePaySession.canMakePayments()) {
48
- setIsApplePayAvailable(true);
49
- addExpress();
67
+ let cancelled = false;
68
+ const checkAvailability = () => {
69
+ try {
70
+ if (!cancelled && window?.ApplePaySession && ApplePaySession.canMakePayments()) {
71
+ setIsApplePayAvailable(true);
72
+ handleAddExpressId('apple_pay');
73
+ }
50
74
  }
51
- else {
52
- setIsApplePayAvailable(false);
75
+ catch (error) {
76
+ console.warn('[ApplePay] Apple Pay not available:', error);
53
77
  }
54
- }
55
- catch (error) {
56
- // Likely "Trying to start an Apple Pay session from an insecure document"
57
- console.warn('[ApplePay] Apple Pay not available:', error);
58
- setIsApplePayAvailable(false);
59
- }
78
+ };
79
+ loadApplePaySdk()
80
+ .then(checkAvailability)
81
+ .catch(() => { });
82
+ return () => { cancelled = true; };
60
83
  }, [handleAddExpressId]);
61
84
  // Helper to convert minor units to currency string
62
85
  const minorUnitsToCurrencyString = useCallback((amountMinor, currency) => {