@tagadapay/plugin-sdk 3.1.8 → 3.1.10

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 (50) hide show
  1. package/README.md +1129 -1129
  2. package/build-cdn.js +223 -113
  3. package/dist/external-tracker.js +135 -81
  4. package/dist/external-tracker.min.js +2 -2
  5. package/dist/external-tracker.min.js.map +4 -4
  6. package/dist/react/providers/TagadaProvider.js +5 -5
  7. package/dist/tagada-sdk.js +10164 -0
  8. package/dist/tagada-sdk.min.js +45 -0
  9. package/dist/tagada-sdk.min.js.map +7 -0
  10. package/dist/v2/core/funnelClient.d.ts +91 -4
  11. package/dist/v2/core/funnelClient.js +42 -3
  12. package/dist/v2/core/resources/funnel.d.ts +10 -0
  13. package/dist/v2/core/resources/payments.d.ts +21 -1
  14. package/dist/v2/core/resources/payments.js +34 -0
  15. package/dist/v2/core/utils/currency.d.ts +14 -0
  16. package/dist/v2/core/utils/currency.js +40 -0
  17. package/dist/v2/core/utils/index.d.ts +1 -0
  18. package/dist/v2/core/utils/index.js +2 -0
  19. package/dist/v2/core/utils/pluginConfig.d.ts +8 -0
  20. package/dist/v2/core/utils/pluginConfig.js +28 -0
  21. package/dist/v2/core/utils/previewMode.d.ts +4 -0
  22. package/dist/v2/core/utils/previewMode.js +28 -0
  23. package/dist/v2/core/utils/previewModeIndicator.js +101 -101
  24. package/dist/v2/index.d.ts +7 -6
  25. package/dist/v2/index.js +6 -6
  26. package/dist/v2/react/components/ApplePayButton.d.ts +1 -2
  27. package/dist/v2/react/components/ApplePayButton.js +57 -58
  28. package/dist/v2/react/components/FunnelScriptInjector.js +161 -172
  29. package/dist/v2/react/components/GooglePayButton.d.ts +2 -0
  30. package/dist/v2/react/components/GooglePayButton.js +80 -64
  31. package/dist/v2/react/hooks/useFunnel.d.ts +8 -2
  32. package/dist/v2/react/hooks/useFunnel.js +2 -2
  33. package/dist/v2/react/hooks/useGoogleAutocomplete.d.ts +10 -0
  34. package/dist/v2/react/hooks/useGoogleAutocomplete.js +48 -0
  35. package/dist/v2/react/hooks/useGooglePayCheckout.d.ts +21 -0
  36. package/dist/v2/react/hooks/useGooglePayCheckout.js +198 -0
  37. package/dist/v2/react/hooks/usePaymentPolling.d.ts +7 -1
  38. package/dist/v2/react/hooks/usePaymentQuery.d.ts +2 -0
  39. package/dist/v2/react/hooks/usePaymentQuery.js +435 -8
  40. package/dist/v2/react/hooks/usePixelTracking.d.ts +56 -0
  41. package/dist/v2/react/hooks/usePixelTracking.js +508 -0
  42. package/dist/v2/react/hooks/useStepConfig.d.ts +8 -6
  43. package/dist/v2/react/hooks/useStepConfig.js +3 -2
  44. package/dist/v2/react/index.d.ts +6 -2
  45. package/dist/v2/react/index.js +3 -1
  46. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.d.ts +1 -0
  47. package/dist/v2/react/providers/ExpressPaymentMethodsProvider.js +33 -13
  48. package/dist/v2/react/providers/TagadaProvider.js +22 -21
  49. package/dist/v2/standalone/index.js +1 -1
  50. package/package.json +112 -112
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { useEffect, useMemo, useRef } from 'react';
2
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
3
3
  import { getAssignedStepConfig } from '../../core';
4
4
  /**
5
5
  * FunnelScriptInjector - Handles injection of funnel scripts into the page.
@@ -26,158 +26,144 @@ export function FunnelScriptInjector({ context, isInitialized }) {
26
26
  }
27
27
  return scripts;
28
28
  }, [isInitialized]); // Re-compute when funnel initializes (local config should be loaded by then)
29
- useEffect(() => {
30
- // Only run in browser environment
31
- if (typeof document === 'undefined') {
29
+ // Set up Tagada on window object - must be available before any scripts run
30
+ // Memoized to prevent unnecessary recreations across re-renders
31
+ const setupTagada = useCallback(() => {
32
+ if (typeof window === 'undefined')
32
33
  return;
33
- }
34
- // Set up Tagada for funnel scripts (similar to HtmlScript.tsx)
35
- const setupTagada = () => {
36
- // @ts-expect-error - Adding utilities to window
37
- if (window.Tagada) {
38
- // Update pageType if context is available
39
- if (context?.currentStepId) {
40
- // @ts-expect-error - Updating window property
41
- window.Tagada.pageType = context.currentStepId;
42
- }
43
- // Update isInitialized
44
- // @ts-expect-error - Updating window property
45
- window.Tagada.isInitialized = isInitialized;
46
- // Update ressources
34
+ // @ts-expect-error - Adding utilities to window
35
+ if (window.Tagada) {
36
+ // Update properties if Tagada already exists
37
+ if (context?.currentStepId) {
47
38
  // @ts-expect-error - Updating window property
48
- window.Tagada.ressources = context?.resources || null;
49
- return; // Utils already exist, just update properties
39
+ window.Tagada.pageType = context.currentStepId;
50
40
  }
51
- // @ts-expect-error - Adding utilities to window
52
- window.Tagada = {
53
- // Wait for DOM to be ready
54
- ready: (callback) => {
55
- if (document.readyState === 'loading') {
56
- document.addEventListener('DOMContentLoaded', callback);
57
- }
58
- else {
41
+ // @ts-expect-error - Updating window property
42
+ window.Tagada.isInitialized = isInitialized;
43
+ // @ts-expect-error - Updating window property
44
+ window.Tagada.ressources = context?.resources || null;
45
+ return;
46
+ }
47
+ // @ts-expect-error - Adding utilities to window
48
+ window.Tagada = {
49
+ // Wait for DOM to be ready
50
+ ready: (callback) => {
51
+ if (document.readyState === 'loading') {
52
+ document.addEventListener('DOMContentLoaded', callback);
53
+ }
54
+ else {
55
+ callback();
56
+ }
57
+ },
58
+ // Wait for window to be fully loaded AND funnel to be initialized
59
+ loaded: (callback) => {
60
+ const checkBothConditions = () => {
61
+ const pageLoaded = document.readyState === 'complete';
62
+ // @ts-expect-error - Accessing window property
63
+ const funnelInitialized = window.Tagada?.isInitialized === true;
64
+ if (pageLoaded && funnelInitialized) {
59
65
  callback();
66
+ return true;
60
67
  }
61
- },
62
- // Wait for window to be fully loaded AND funnel to be initialized
63
- loaded: (callback) => {
64
- const checkBothConditions = () => {
65
- const pageLoaded = document.readyState === 'complete';
66
- // @ts-expect-error - Accessing window property
67
- const funnelInitialized = window.Tagada?.isInitialized === true;
68
- if (pageLoaded && funnelInitialized) {
69
- callback();
70
- return true;
71
- }
72
- return false;
73
- };
74
- // Check immediately
75
- if (checkBothConditions()) {
76
- return;
68
+ return false;
69
+ };
70
+ if (checkBothConditions()) {
71
+ return;
72
+ }
73
+ let loadListener = null;
74
+ let initCheckInterval = null;
75
+ let hasCalled = false;
76
+ const cleanup = () => {
77
+ if (loadListener) {
78
+ window.removeEventListener('load', loadListener);
79
+ loadListener = null;
77
80
  }
78
- // Set up listeners for both conditions
79
- let loadListener = null;
80
- let initCheckInterval = null;
81
- let hasCalled = false;
82
- const cleanup = () => {
83
- if (loadListener) {
84
- window.removeEventListener('load', loadListener);
85
- loadListener = null;
86
- }
87
- if (initCheckInterval) {
88
- clearInterval(initCheckInterval);
89
- initCheckInterval = null;
90
- }
91
- };
92
- // Listen for page load
93
- loadListener = () => {
94
- if (checkBothConditions() && !hasCalled) {
95
- hasCalled = true;
96
- cleanup();
97
- }
98
- };
99
- window.addEventListener('load', loadListener);
100
- // Poll for initialization status (in case page loads before initialization)
101
- initCheckInterval = setInterval(() => {
102
- if (checkBothConditions() && !hasCalled) {
103
- hasCalled = true;
104
- cleanup();
105
- }
106
- }, 100);
107
- // Timeout fallback (10 seconds max wait)
108
- setTimeout(() => {
109
- if (!hasCalled) {
110
- hasCalled = true;
111
- cleanup();
112
- // Call anyway if page is loaded (graceful degradation)
113
- if (document.readyState === 'complete') {
114
- callback();
115
- }
116
- }
117
- }, 10000);
118
- },
119
- // Execute with delay
120
- delay: (callback, ms = 1000) => {
121
- setTimeout(callback, ms);
122
- },
123
- // Retry until condition is met
124
- retry: (condition, callback, maxAttempts = 10, interval = 500) => {
125
- let attempts = 0;
126
- const check = () => {
127
- attempts++;
128
- if (condition() || attempts >= maxAttempts) {
81
+ if (initCheckInterval) {
82
+ clearInterval(initCheckInterval);
83
+ initCheckInterval = null;
84
+ }
85
+ };
86
+ loadListener = () => {
87
+ if (checkBothConditions() && !hasCalled) {
88
+ hasCalled = true;
89
+ cleanup();
90
+ }
91
+ };
92
+ window.addEventListener('load', loadListener);
93
+ initCheckInterval = setInterval(() => {
94
+ if (checkBothConditions() && !hasCalled) {
95
+ hasCalled = true;
96
+ cleanup();
97
+ }
98
+ }, 100);
99
+ setTimeout(() => {
100
+ if (!hasCalled) {
101
+ hasCalled = true;
102
+ cleanup();
103
+ if (document.readyState === 'complete') {
129
104
  callback();
130
105
  }
131
- else {
132
- setTimeout(check, interval);
133
- }
134
- };
135
- check();
136
- },
137
- // Wait for element to exist
138
- waitForElement: (selector, callback, timeout = 10000) => {
106
+ }
107
+ }, 10000);
108
+ },
109
+ delay: (callback, ms = 1000) => {
110
+ setTimeout(callback, ms);
111
+ },
112
+ retry: (condition, callback, maxAttempts = 10, interval = 500) => {
113
+ let attempts = 0;
114
+ const check = () => {
115
+ attempts++;
116
+ if (condition() || attempts >= maxAttempts) {
117
+ callback();
118
+ }
119
+ else {
120
+ setTimeout(check, interval);
121
+ }
122
+ };
123
+ check();
124
+ },
125
+ waitForElement: (selector, callback, timeout = 10000) => {
126
+ const element = document.querySelector(selector);
127
+ if (element) {
128
+ callback(element);
129
+ return;
130
+ }
131
+ const observer = new MutationObserver(() => {
139
132
  const element = document.querySelector(selector);
140
133
  if (element) {
141
- callback(element);
142
- return;
143
- }
144
- const observer = new MutationObserver(() => {
145
- const element = document.querySelector(selector);
146
- if (element) {
147
- observer.disconnect();
148
- callback(element);
149
- }
150
- });
151
- observer.observe(document.body, {
152
- childList: true,
153
- subtree: true,
154
- });
155
- // Timeout fallback
156
- setTimeout(() => {
157
134
  observer.disconnect();
158
- }, timeout);
159
- },
160
- // Page type helper (current step ID)
161
- pageType: context?.currentStepId || null,
162
- // Funnel initialization status
163
- isInitialized: isInitialized,
164
- // Expose resources directly (convenience access)
165
- ressources: context?.resources || null,
166
- // Expose funnel context data
167
- funnel: context
168
- ? {
169
- sessionId: context.sessionId,
170
- funnelId: context.funnelId,
171
- currentStepId: context.currentStepId,
172
- previousStepId: context.previousStepId,
173
- ressources: context.resources,
135
+ callback(element);
174
136
  }
175
- : null,
176
- // Expose stepConfig from HTML injection (NEW: supports A/B variants)
177
- stepConfig: getAssignedStepConfig() || null,
178
- };
137
+ });
138
+ observer.observe(document.body, {
139
+ childList: true,
140
+ subtree: true,
141
+ });
142
+ setTimeout(() => {
143
+ observer.disconnect();
144
+ }, timeout);
145
+ },
146
+ pageType: context?.currentStepId || null,
147
+ isInitialized: isInitialized,
148
+ ressources: context?.resources || null,
149
+ funnel: context
150
+ ? {
151
+ sessionId: context.sessionId,
152
+ funnelId: context.funnelId,
153
+ currentStepId: context.currentStepId,
154
+ previousStepId: context.previousStepId,
155
+ ressources: context.resources,
156
+ }
157
+ : null,
158
+ stepConfig: getAssignedStepConfig() || null,
179
159
  };
180
- // Set up utilities before injecting script
160
+ }, [context, isInitialized]);
161
+ useEffect(() => {
162
+ // Only run in browser environment
163
+ if (typeof document === 'undefined') {
164
+ return;
165
+ }
166
+ // Set up Tagada utilities before injecting legacy script
181
167
  setupTagada();
182
168
  const scriptContent = context?.script;
183
169
  const scriptId = 'tagada-funnel-script';
@@ -213,27 +199,27 @@ export function FunnelScriptInjector({ context, isInitialized }) {
213
199
  existingScript.remove();
214
200
  }
215
201
  // Wrap script content with error handling and context checks
216
- const wrappedScript = `
217
- (function() {
218
- try {
219
- // Check if we have basic DOM access
220
- if (typeof document === 'undefined') {
221
- console.error('[TagadaPay] Document not available');
222
- return;
223
- }
224
-
225
- // Check if we have Tagada
226
- if (!window.Tagada) {
227
- console.error('[TagadaPay] Tagada not available');
228
- return;
229
- }
230
-
231
- // Execute the original script
232
- ${scriptBody}
233
- } catch (error) {
234
- console.error('[TagadaPay] Script execution error:', error);
235
- }
236
- })();
202
+ const wrappedScript = `
203
+ (function() {
204
+ try {
205
+ // Check if we have basic DOM access
206
+ if (typeof document === 'undefined') {
207
+ console.error('[TagadaPay] Document not available');
208
+ return;
209
+ }
210
+
211
+ // Check if we have Tagada
212
+ if (!window.Tagada) {
213
+ console.error('[TagadaPay] Tagada not available');
214
+ return;
215
+ }
216
+
217
+ // Execute the original script
218
+ ${scriptBody}
219
+ } catch (error) {
220
+ console.error('[TagadaPay] Script execution error:', error);
221
+ }
222
+ })();
237
223
  `;
238
224
  // Create and inject new script element
239
225
  const scriptElement = document.createElement('script');
@@ -252,11 +238,14 @@ export function FunnelScriptInjector({ context, isInitialized }) {
252
238
  // This prevents React StrictMode from re-injecting the same script on the second run
253
239
  // The ref will be cleared when script content actually changes (next effect run)
254
240
  };
255
- }, [context?.script, context?.currentStepId, isInitialized]);
241
+ }, [context?.script, context?.currentStepId, isInitialized, setupTagada]);
256
242
  // Effect for NEW stepConfig.scripts (from HTML injection, supports A/B variants)
257
243
  useEffect(() => {
258
244
  if (typeof document === 'undefined')
259
245
  return;
246
+ // IMPORTANT: Set up Tagada BEFORE injecting any scripts
247
+ // This ensures Tagada.ready() is always available when scripts run
248
+ setupTagada();
260
249
  if (stepConfigScripts.length === 0)
261
250
  return;
262
251
  // Create a hash of current scripts to detect changes
@@ -284,15 +273,15 @@ export function FunnelScriptInjector({ context, isInitialized }) {
284
273
  if (!scriptBody)
285
274
  return;
286
275
  // Wrap script content with error handling
287
- const wrappedScript = `
288
- (function() {
289
- try {
290
- // Script: ${script.name}
291
- ${scriptBody}
292
- } catch (error) {
293
- console.error('[TagadaPay] StepConfig script "${script.name}" error:', error);
294
- }
295
- })();
276
+ const wrappedScript = `
277
+ (function() {
278
+ try {
279
+ // Script: ${script.name}
280
+ ${scriptBody}
281
+ } catch (error) {
282
+ console.error('[TagadaPay] StepConfig script "${script.name}" error:', error);
283
+ }
284
+ })();
296
285
  `;
297
286
  // Create script element
298
287
  const scriptElement = document.createElement('script');
@@ -340,7 +329,7 @@ export function FunnelScriptInjector({ context, isInitialized }) {
340
329
  return () => {
341
330
  document.querySelectorAll('[data-tagada-stepconfig-script]').forEach(el => el.remove());
342
331
  };
343
- }, [stepConfigScripts]);
332
+ }, [stepConfigScripts, setupTagada]);
344
333
  // This component doesn't render anything
345
334
  return null;
346
335
  }
@@ -14,6 +14,8 @@ export interface GooglePayButtonProps {
14
14
  size?: 'sm' | 'md' | 'lg';
15
15
  buttonColor?: 'default' | 'black' | 'white';
16
16
  buttonType?: 'buy' | 'plain' | 'donate' | 'pay';
17
+ /** Override shipping requirement. If undefined, auto-detects based on shippingMethods. */
18
+ requiresShipping?: boolean;
17
19
  }
18
20
  export declare const GooglePayButton: React.FC<GooglePayButtonProps>;
19
21
  export default GooglePayButton;
@@ -4,13 +4,32 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
4
4
  * Uses v2 useExpressPaymentMethods hook and follows clean architecture principles
5
5
  */
6
6
  import GooglePayButtonReact from '@google-pay/button-react';
7
- import { useCallback, useEffect, useState } from 'react';
7
+ import { useCallback, useEffect, useMemo, useState } from 'react';
8
8
  import { useExpressPaymentMethods } from '../hooks/useExpressPaymentMethods';
9
- const basistheoryPublicKey = (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_BASIS_THEORY_PUBLIC_API_KEY) || '';
10
- const basistheoryTenantId = (typeof process !== 'undefined' && process.env?.NEXT_PUBLIC_BASIS_THEORY_TENANT_ID) ||
11
- '0b283fa3-44a1-4535-adff-e99ad0a58a47';
12
- export const GooglePayButton = ({ className = '', disabled = false, onSuccess, onError, onCancel, checkout, size = 'lg', buttonColor = 'black', buttonType = 'plain', }) => {
9
+ import { usePaymentQuery } from '../hooks/usePaymentQuery';
10
+ import { useShippingRatesQuery } from '../hooks/useShippingRatesQuery';
11
+ import { getBasisTheoryKeys } from '../../../config/basisTheory';
12
+ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, onError, onCancel, checkout, size = 'lg', buttonColor = 'black', buttonType = 'plain', requiresShipping: requiresShippingProp, }) => {
13
13
  const { googlePayPaymentMethod, shippingMethods, lineItems, handleAddExpressId, updateCheckoutSessionValues, updateCustomerEmail, reComputeOrderSummary, setError: setContextError, } = useExpressPaymentMethods();
14
+ // Use payment query hook for processing
15
+ const { processGooglePayPayment } = usePaymentQuery();
16
+ // Use shipping rates hook for selecting shipping rates
17
+ const { selectRate } = useShippingRatesQuery({ checkout });
18
+ // Get Basis Theory credentials based on sandboxed flag from payment method config
19
+ // When sandboxed is true (even on production hostname), use test keys
20
+ const { basistheoryPublicKey, basistheoryTenantId } = useMemo(() => {
21
+ const useProductionKeys = googlePayPaymentMethod?.metadata?.sandboxed === false;
22
+ const keys = getBasisTheoryKeys(useProductionKeys);
23
+ console.log('[GooglePayButton] Using Basis Theory keys:', {
24
+ sandboxed: googlePayPaymentMethod?.metadata?.sandboxed,
25
+ useProductionKeys,
26
+ tenantId: keys.tenantId,
27
+ });
28
+ return {
29
+ basistheoryPublicKey: keys.apiKey,
30
+ basistheoryTenantId: keys.tenantId,
31
+ };
32
+ }, [googlePayPaymentMethod?.metadata?.sandboxed]);
14
33
  const [processingPayment, setProcessingPayment] = useState(false);
15
34
  const [pendingPaymentData, setPendingPaymentData] = useState(null);
16
35
  const [googlePayError, setGooglePayError] = useState(null);
@@ -65,46 +84,32 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
65
84
  throw error;
66
85
  }
67
86
  }, []);
68
- // Process Google Pay payment
69
- const processGooglePayPayment = useCallback(async (token) => {
70
- if (!checkout.checkoutSession.id) {
71
- throw new Error('Checkout session ID is not available');
72
- }
87
+ // Process Google Pay payment using SDK hook
88
+ const handleGooglePayPayment = useCallback(async (token) => {
73
89
  setProcessingPayment(true);
74
90
  try {
75
- // Process payment with backend API
76
- const response = await fetch('/api/v1/google-pay/process-payment', {
77
- method: 'POST',
78
- headers: {
79
- 'Content-Type': 'application/json',
91
+ const result = await processGooglePayPayment(checkout.checkoutSession.id, token, {
92
+ onPaymentSuccess: (response) => {
93
+ // Keep processing state true during navigation
94
+ },
95
+ onPaymentFailed: (err) => {
96
+ setProcessingPayment(false);
97
+ setGooglePayError(err.message);
98
+ setContextError(err.message);
99
+ throw new Error(err.message);
80
100
  },
81
- body: JSON.stringify({
82
- checkoutSessionId: checkout.checkoutSession.id,
83
- paymentToken: token,
84
- }),
85
101
  });
86
- if (!response.ok) {
87
- throw new Error('Failed to process Google Pay payment');
88
- }
89
- const paymentResult = await response.json();
90
- if (onSuccess) {
91
- onSuccess(paymentResult);
92
- }
102
+ return result;
93
103
  }
94
104
  catch (error) {
95
- console.error('Error processing Google Pay payment:', error);
105
+ console.error('Payment processing failed:', error);
106
+ setProcessingPayment(false);
96
107
  const errorMessage = error instanceof Error ? error.message : 'Google Pay payment failed';
97
108
  setGooglePayError(errorMessage);
98
109
  setContextError(errorMessage);
99
- if (onError) {
100
- onError(errorMessage);
101
- }
102
110
  throw error;
103
111
  }
104
- finally {
105
- setProcessingPayment(false);
106
- }
107
- }, [checkout.checkoutSession.id, onSuccess, onError, setContextError]);
112
+ }, [processGooglePayPayment, checkout.checkoutSession.id, setContextError]);
108
113
  // Process payment data
109
114
  const onGooglePaymentData = useCallback(async (paymentData) => {
110
115
  setProcessingPayment(true);
@@ -137,7 +142,11 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
137
142
  });
138
143
  }
139
144
  const payToken = await tokenizeGooglePayTokenWithBasisTheory(paymentData);
140
- await processGooglePayPayment(payToken);
145
+ const result = await handleGooglePayPayment(payToken);
146
+ // Call success callback
147
+ if (onSuccess) {
148
+ onSuccess(result);
149
+ }
141
150
  }
142
151
  catch (error) {
143
152
  console.error('Error processing Google Pay payment:', error);
@@ -154,7 +163,8 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
154
163
  updateCheckoutSessionValues,
155
164
  updateCustomerEmail,
156
165
  tokenizeGooglePayTokenWithBasisTheory,
157
- processGooglePayPayment,
166
+ handleGooglePayPayment,
167
+ onSuccess,
158
168
  onError,
159
169
  setContextError,
160
170
  ]);
@@ -200,8 +210,12 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
200
210
  });
201
211
  const newOrderSummary = await reComputeOrderSummary();
202
212
  if (newOrderSummary) {
213
+ // Use selected shipping rate ID if available, otherwise fall back to first one
214
+ const defaultSelectedId = newOrderSummary.selectedShippingRateId ||
215
+ newOrderSummary.shippingMethods[0]?.identifier ||
216
+ '';
203
217
  paymentDataRequestUpdate.newShippingOptionParameters = {
204
- defaultSelectedOptionId: newOrderSummary.shippingMethods[0]?.identifier || '',
218
+ defaultSelectedOptionId: defaultSelectedId,
205
219
  shippingOptions: newOrderSummary.shippingMethods.map((method) => ({
206
220
  id: method.identifier,
207
221
  label: method.label,
@@ -218,17 +232,8 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
218
232
  else if (intermediatePaymentData.callbackTrigger === 'SHIPPING_OPTION') {
219
233
  // Update shipping rate
220
234
  if (intermediatePaymentData.shippingOptionData?.id) {
221
- const response = await fetch('/api/v1/shipping-rates/select', {
222
- method: 'POST',
223
- headers: {
224
- 'Content-Type': 'application/json',
225
- },
226
- body: JSON.stringify({
227
- checkoutSessionId: checkout.checkoutSession.id,
228
- shippingRateId: intermediatePaymentData.shippingOptionData.id,
229
- }),
230
- });
231
- if (response.ok) {
235
+ try {
236
+ await selectRate(intermediatePaymentData.shippingOptionData.id);
232
237
  const newOrderSummary = await reComputeOrderSummary();
233
238
  if (newOrderSummary) {
234
239
  paymentDataRequestUpdate.newTransactionInfo = {
@@ -238,13 +243,17 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
238
243
  };
239
244
  }
240
245
  }
246
+ catch (error) {
247
+ console.error('Error selecting shipping rate:', error);
248
+ throw error;
249
+ }
241
250
  }
242
251
  }
243
252
  else if (intermediatePaymentData.callbackTrigger === 'OFFER') {
244
- console.log('OFFER callback triggered, no action needed');
253
+ // No action needed for OFFER callback
245
254
  }
246
255
  else if (intermediatePaymentData.callbackTrigger === 'INITIALIZE') {
247
- console.log('INITIALIZE callback triggered, no action needed');
256
+ // No action needed for INITIALIZE callback
248
257
  }
249
258
  resolve(paymentDataRequestUpdate);
250
259
  }
@@ -261,7 +270,7 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
261
270
  };
262
271
  void processCallback();
263
272
  });
264
- }, [updateCheckoutSessionValues, reComputeOrderSummary, checkout]);
273
+ }, [updateCheckoutSessionValues, reComputeOrderSummary, checkout, selectRate]);
265
274
  // Handle payment authorization
266
275
  const handleGooglePayAuthorized = useCallback((paymentData) => {
267
276
  setPendingPaymentData(paymentData);
@@ -295,6 +304,8 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
295
304
  gatewayMerchantId: basistheoryTenantId,
296
305
  },
297
306
  };
307
+ // Determine if shipping is required: use prop if provided, otherwise auto-detect from shippingMethods
308
+ const requiresShipping = requiresShippingProp ?? shippingMethods.length > 0;
298
309
  const paymentRequest = {
299
310
  apiVersion: 2,
300
311
  apiVersionMinor: 0,
@@ -313,22 +324,28 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
313
324
  merchantName: googlePayPaymentMethod?.metadata?.merchantName ||
314
325
  checkout.checkoutSession?.store?.name ||
315
326
  'Store',
327
+ // Use test merchant ID for sandbox, actual merchant ID for production
316
328
  merchantId: googlePayPaymentMethod?.metadata?.sandboxed
317
- ? '12345678901234567890'
329
+ ? '12345678901234567890' // Google's test merchant ID for sandbox
318
330
  : googlePayPaymentMethod?.metadata?.merchantId || '12345678901234567890',
319
331
  },
320
- shippingAddressRequired: true,
321
- shippingOptionRequired: true,
332
+ shippingAddressRequired: requiresShipping,
333
+ shippingOptionRequired: requiresShipping,
322
334
  emailRequired: true,
323
- callbackIntents: ['SHIPPING_OPTION', 'SHIPPING_ADDRESS', 'PAYMENT_AUTHORIZATION'],
324
- shippingOptionParameters: {
325
- defaultSelectedOptionId: shippingMethods.length > 0 ? shippingMethods[0].identifier : '',
326
- shippingOptions: shippingMethods.map((method) => ({
327
- id: method.identifier,
328
- label: method.label,
329
- description: method.amount + ': ' + method.detail || '',
330
- })),
331
- },
335
+ callbackIntents: requiresShipping
336
+ ? ['SHIPPING_OPTION', 'SHIPPING_ADDRESS', 'PAYMENT_AUTHORIZATION']
337
+ : ['PAYMENT_AUTHORIZATION'],
338
+ ...(requiresShipping && {
339
+ shippingOptionParameters: {
340
+ defaultSelectedOptionId: checkout.checkoutSession?.shippingRate?.id ||
341
+ (shippingMethods.length > 0 ? shippingMethods[0].identifier : ''),
342
+ shippingOptions: shippingMethods.map((method) => ({
343
+ id: method.identifier,
344
+ label: method.label,
345
+ description: method.amount + ': ' + method.detail || '',
346
+ })),
347
+ },
348
+ }),
332
349
  };
333
350
  const environment = googlePayPaymentMethod?.metadata?.sandboxed ? 'TEST' : 'PRODUCTION';
334
351
  // Button size classes
@@ -347,7 +364,6 @@ export const GooglePayButton = ({ className = '', disabled = false, onSuccess, o
347
364
  onError(errorMessage);
348
365
  }
349
366
  }, onCancel: () => {
350
- console.log('Google Pay payment cancelled');
351
367
  if (onCancel) {
352
368
  onCancel();
353
369
  }