@tagadapay/plugin-sdk 2.8.8 → 2.8.9

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 (41) hide show
  1. package/dist/react/config/environment.d.ts +1 -22
  2. package/dist/react/config/environment.js +1 -132
  3. package/dist/react/utils/deviceInfo.d.ts +1 -39
  4. package/dist/react/utils/deviceInfo.js +1 -163
  5. package/dist/react/utils/jwtDecoder.d.ts +1 -14
  6. package/dist/react/utils/jwtDecoder.js +1 -86
  7. package/dist/react/utils/tokenStorage.d.ts +1 -16
  8. package/dist/react/utils/tokenStorage.js +1 -53
  9. package/dist/v2/core/client.d.ts +92 -0
  10. package/dist/v2/core/client.js +386 -0
  11. package/dist/v2/core/config/environment.d.ts +22 -0
  12. package/dist/v2/core/config/environment.js +140 -0
  13. package/dist/v2/core/pathRemapping.js +61 -3
  14. package/dist/v2/core/resources/apiClient.d.ts +8 -0
  15. package/dist/v2/core/resources/apiClient.js +30 -9
  16. package/dist/v2/core/resources/funnel.d.ts +14 -0
  17. package/dist/v2/core/resources/payments.d.ts +23 -0
  18. package/dist/v2/core/types.d.ts +271 -0
  19. package/dist/v2/core/types.js +4 -0
  20. package/dist/v2/core/utils/deviceInfo.d.ts +39 -0
  21. package/dist/v2/core/utils/deviceInfo.js +162 -0
  22. package/dist/v2/core/utils/eventDispatcher.d.ts +10 -0
  23. package/dist/v2/core/utils/eventDispatcher.js +24 -0
  24. package/dist/v2/core/utils/jwtDecoder.d.ts +14 -0
  25. package/dist/v2/core/utils/jwtDecoder.js +85 -0
  26. package/dist/v2/core/utils/pluginConfig.js +6 -0
  27. package/dist/v2/core/utils/tokenStorage.d.ts +19 -0
  28. package/dist/v2/core/utils/tokenStorage.js +52 -0
  29. package/dist/v2/react/components/DebugDrawer.js +90 -1
  30. package/dist/v2/react/hooks/__examples__/FunnelContextExample.d.ts +12 -0
  31. package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +54 -0
  32. package/dist/v2/react/hooks/useFunnel.d.ts +1 -1
  33. package/dist/v2/react/hooks/useFunnel.js +209 -32
  34. package/dist/v2/react/hooks/useGoogleAutocomplete.js +26 -18
  35. package/dist/v2/react/hooks/useISOData.js +4 -2
  36. package/dist/v2/react/hooks/useOffersQuery.d.ts +24 -29
  37. package/dist/v2/react/hooks/useOffersQuery.js +164 -204
  38. package/dist/v2/react/hooks/usePaymentQuery.js +99 -6
  39. package/dist/v2/react/providers/TagadaProvider.d.ts +8 -21
  40. package/dist/v2/react/providers/TagadaProvider.js +79 -673
  41. package/package.json +1 -1
@@ -2,19 +2,23 @@
2
2
  * Offers Hook using TanStack Query
3
3
  * Handles offers with automatic caching
4
4
  */
5
- import { useMemo, useCallback, useState } from 'react';
6
- import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
5
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
6
+ import { useCallback, useMemo, useRef, useState } from 'react';
7
7
  import { OffersResource } from '../../core/resources/offers';
8
8
  import { useTagadaContext } from '../providers/TagadaProvider';
9
- import { usePluginConfig } from './usePluginConfig';
10
9
  import { getGlobalApiClient } from './useApiQuery';
10
+ import { usePluginConfig } from './usePluginConfig';
11
11
  export function useOffersQuery(options = {}) {
12
- const { offerIds = [], enabled = true, returnUrl } = options;
12
+ const { offerIds = [], enabled = true, returnUrl, orderId: defaultOrderId } = options;
13
13
  const { storeId } = usePluginConfig();
14
14
  const { isSessionInitialized, session } = useTagadaContext();
15
15
  const _queryClient = useQueryClient();
16
16
  // State for checkout sessions per offer (similar to postPurchases)
17
17
  const [checkoutSessions, setCheckoutSessions] = useState({});
18
+ // Use ref to break dependency cycles in callbacks
19
+ const checkoutSessionsRef = useRef(checkoutSessions);
20
+ // Update ref on every render
21
+ checkoutSessionsRef.current = checkoutSessions;
18
22
  // Create offers resource client
19
23
  const offersResource = useMemo(() => {
20
24
  try {
@@ -59,7 +63,9 @@ export function useOffersQuery(options = {}) {
59
63
  isUpdatingSummary: false,
60
64
  }
61
65
  }));
66
+ return orderSummary;
62
67
  }
68
+ return null;
63
69
  }
64
70
  catch (error) {
65
71
  setCheckoutSessions(prev => ({
@@ -75,7 +81,7 @@ export function useOffersQuery(options = {}) {
75
81
  // Create query key based on options
76
82
  const queryKey = useMemo(() => ['offers', { storeId, offerIds }], [storeId, offerIds]);
77
83
  // Use TanStack Query for fetching offers
78
- const { data: offers = [], isLoading, error, refetch } = useQuery({
84
+ const { data: offers = [], isLoading, error } = useQuery({
79
85
  queryKey,
80
86
  enabled: enabled && !!storeId && isSessionInitialized,
81
87
  queryFn: async () => {
@@ -94,32 +100,7 @@ export function useOffersQuery(options = {}) {
94
100
  staleTime: 5 * 60 * 1000, // 5 minutes
95
101
  refetchOnWindowFocus: false,
96
102
  });
97
- // Mutations for offer actions
98
- const createCheckoutSessionMutation = useMutation({
99
- mutationFn: async ({ offerId, returnUrl }) => {
100
- return await offersResource.createCheckoutSession(offerId, returnUrl);
101
- },
102
- onError: (_error) => {
103
- // Error handling removed
104
- },
105
- });
106
- const payOfferMutation = useMutation({
107
- mutationFn: async ({ offerId, orderId }) => {
108
- return await offersResource.payOffer(offerId, orderId);
109
- },
110
- onError: (_error) => {
111
- // Error handling removed
112
- },
113
- });
114
- const transformToCheckoutMutation = useMutation({
115
- mutationFn: async ({ offerId, returnUrl }) => {
116
- return await offersResource.transformToCheckout(offerId, returnUrl);
117
- },
118
- onError: (_error) => {
119
- // Error handling removed
120
- },
121
- });
122
- const payWithCheckoutSessionMutation = useMutation({
103
+ const { mutateAsync: payWithCheckoutSessionAsync } = useMutation({
123
104
  mutationFn: async ({ checkoutSessionId, orderId }) => {
124
105
  return await offersResource.payWithCheckoutSession(checkoutSessionId, orderId);
125
106
  },
@@ -127,7 +108,7 @@ export function useOffersQuery(options = {}) {
127
108
  // Error handling removed
128
109
  },
129
110
  });
130
- const initCheckoutSessionMutation = useMutation({
111
+ const { mutateAsync: initCheckoutSessionAsync } = useMutation({
131
112
  mutationFn: async ({ offerId, orderId, customerId }) => {
132
113
  return await offersResource.initCheckoutSession(offerId, orderId, customerId);
133
114
  },
@@ -135,108 +116,125 @@ export function useOffersQuery(options = {}) {
135
116
  // Error handling removed
136
117
  },
137
118
  });
138
- // Helper functions (matching V1 useOffers exactly)
139
- const getOffer = useCallback((offerId) => {
140
- return offers.find((offer) => offer.id === offerId);
141
- }, [offers]);
142
- const getTotalValue = useCallback(() => {
143
- return offers.reduce((total, offer) => {
144
- const firstSummary = offer.summaries[0];
145
- return total + (firstSummary?.totalAdjustedAmount || 0);
146
- }, 0);
147
- }, [offers]);
148
- const getTotalSavings = useCallback(() => {
149
- return offers.reduce((total, offer) => {
150
- const firstSummary = offer.summaries[0];
151
- return total + (firstSummary?.totalPromotionAmount || 0);
152
- }, 0);
153
- }, [offers]);
154
- // Action functions
155
- const createCheckoutSession = useCallback(async (offerId, options) => {
156
- return await createCheckoutSessionMutation.mutateAsync({
157
- offerId,
158
- returnUrl: options?.returnUrl || returnUrl,
159
- });
160
- }, [createCheckoutSessionMutation, returnUrl]);
161
- const payOffer = useCallback(async (offerId, orderId) => {
162
- return await payOfferMutation.mutateAsync({ offerId, orderId });
163
- }, [payOfferMutation]);
164
- const transformToCheckout = useCallback(async (offerId, options) => {
165
- return await transformToCheckoutMutation.mutateAsync({
166
- offerId,
167
- returnUrl: options?.returnUrl || returnUrl,
168
- });
169
- }, [transformToCheckoutMutation, returnUrl]);
170
119
  const payWithCheckoutSession = useCallback(async (checkoutSessionId, orderId) => {
171
- return await payWithCheckoutSessionMutation.mutateAsync({ checkoutSessionId, orderId });
172
- }, [payWithCheckoutSessionMutation]);
120
+ return await payWithCheckoutSessionAsync({ checkoutSessionId, orderId });
121
+ }, [payWithCheckoutSessionAsync]);
173
122
  const initCheckoutSession = useCallback(async (offerId, orderId, customerId) => {
174
123
  // Use customerId from session context if not provided
175
124
  const effectiveCustomerId = customerId || session?.customerId;
176
125
  if (!effectiveCustomerId) {
177
126
  throw new Error('Customer ID is required. Make sure the session is properly initialized.');
178
127
  }
179
- return await initCheckoutSessionMutation.mutateAsync({
128
+ return await initCheckoutSessionAsync({
180
129
  offerId,
181
130
  orderId,
182
131
  customerId: effectiveCustomerId
183
132
  });
184
- }, [initCheckoutSessionMutation, session?.customerId]);
185
- return {
186
- // Query data
187
- offers,
188
- isLoading,
189
- error,
190
- // Actions
191
- refetch: async () => {
192
- await refetch();
193
- },
194
- // Offer management
195
- createCheckoutSession,
196
- payOffer,
197
- transformToCheckout,
198
- // Helper functions
199
- getOffer,
200
- getTotalValue,
201
- getTotalSavings,
202
- // Payment functions
203
- payWithCheckoutSession,
204
- initCheckoutSession,
205
- // Checkout session management (similar to postPurchases)
206
- getCheckoutSessionState: (offerId) => {
207
- return checkoutSessions[offerId] || null;
208
- },
209
- initializeOfferCheckout: async (offerId) => {
210
- // Check if customer ID is available
211
- if (!session?.customerId) {
212
- throw new Error('Customer ID is required. Make sure the session is properly initialized.');
213
- }
214
- // Initialize checkout session using transformToCheckout
215
- const initResult = await offersResource.transformToCheckout(offerId, returnUrl);
216
- if (!initResult.checkoutUrl) {
217
- throw new Error('Failed to initialize checkout session');
133
+ }, [initCheckoutSessionAsync, session?.customerId]);
134
+ const payOffer = useCallback(async (offerId, orderId) => {
135
+ const effectiveOrderId = orderId || defaultOrderId;
136
+ const effectiveCustomerId = session?.customerId;
137
+ if (!effectiveOrderId) {
138
+ throw new Error('Order ID is required for payment. Please provide it in the hook options or the function call.');
139
+ }
140
+ if (!effectiveCustomerId) {
141
+ throw new Error('Customer ID is required. Make sure the session is properly initialized.');
142
+ }
143
+ // 1. Init session
144
+ const { checkoutSessionId } = await initCheckoutSession(offerId, effectiveOrderId, effectiveCustomerId);
145
+ // 2. Pay
146
+ await payWithCheckoutSession(checkoutSessionId, effectiveOrderId);
147
+ }, [initCheckoutSession, payWithCheckoutSession, defaultOrderId, session?.customerId]);
148
+ const preview = useCallback(async (offerId) => {
149
+ const effectiveOrderId = defaultOrderId;
150
+ const effectiveCustomerId = session?.customerId;
151
+ // Use ref to check current state without creating dependency
152
+ const currentSessions = checkoutSessionsRef.current;
153
+ // If we already have a summary in state, return it
154
+ if (currentSessions[offerId]?.orderSummary) {
155
+ return currentSessions[offerId].orderSummary;
156
+ }
157
+ // Prevent duplicate initialization if already has a session and is updating
158
+ if (currentSessions[offerId]?.checkoutSessionId && currentSessions[offerId]?.isUpdatingSummary) {
159
+ return null;
160
+ }
161
+ // If we don't have orderId, fallback to static summary from offer object
162
+ // as we can't initialize a checkout session properly without orderId (for upsells)
163
+ if (!effectiveOrderId || !effectiveCustomerId) {
164
+ const offer = offers.find(o => o.id === offerId);
165
+ return offer?.summaries?.[0] || null;
166
+ }
167
+ try {
168
+ // If we already have a session ID, reuse it instead of creating a new one
169
+ let sessionId = currentSessions[offerId]?.checkoutSessionId;
170
+ if (!sessionId) {
171
+ const { checkoutSessionId } = await initCheckoutSession(offerId, effectiveOrderId, effectiveCustomerId);
172
+ sessionId = checkoutSessionId;
173
+ // Update state with session ID
174
+ setCheckoutSessions(prev => ({
175
+ ...prev,
176
+ [offerId]: {
177
+ checkoutSessionId: sessionId,
178
+ orderSummary: null,
179
+ selectedVariants: {},
180
+ loadingVariants: {},
181
+ isUpdatingSummary: false,
182
+ }
183
+ }));
218
184
  }
219
- // Extract session ID from checkout URL or use a placeholder
220
- const sessionId = 'session_' + offerId; // This would need to be extracted from the actual response
221
- // Initialize session state
222
- setCheckoutSessions(prev => ({
223
- ...prev,
224
- [offerId]: {
225
- checkoutSessionId: sessionId,
226
- orderSummary: null,
227
- selectedVariants: {},
228
- loadingVariants: {},
229
- isUpdatingSummary: false,
185
+ // Fetch and return summary
186
+ return await fetchOrderSummary(offerId, sessionId);
187
+ }
188
+ catch (e) {
189
+ console.error("Failed to preview offer", e);
190
+ return null;
191
+ }
192
+ }, [offers, defaultOrderId, session?.customerId, initCheckoutSession, fetchOrderSummary]); // Removed checkoutSessions dependency
193
+ const getAvailableVariants = useCallback((offerId, productId) => {
194
+ const sessionState = checkoutSessions[offerId]; // This hook needs to react to state changes
195
+ if (!sessionState?.orderSummary?.options?.[productId])
196
+ return [];
197
+ return sessionState.orderSummary.options[productId].map((variant) => ({
198
+ variantId: variant.id,
199
+ variantName: variant.name,
200
+ variantSku: variant.sku,
201
+ variantDefault: variant.default,
202
+ variantExternalId: variant.externalVariantId,
203
+ priceId: variant.prices[0]?.id,
204
+ currencyOptions: variant.prices[0]?.currencyOptions,
205
+ }));
206
+ }, [checkoutSessions]);
207
+ const isLoadingVariants = useCallback((offerId, productId) => {
208
+ return checkoutSessions[offerId]?.loadingVariants?.[productId] ?? false;
209
+ }, [checkoutSessions]);
210
+ const selectVariant = useCallback(async (offerId, productId, variantId) => {
211
+ // Use ref for initial check to avoid dependency but we might need latest state for logic
212
+ // Actually for actions it's better to use ref or just dependency if action is not called in useEffect
213
+ const currentSessions = checkoutSessionsRef.current;
214
+ const sessionState = currentSessions[offerId];
215
+ if (!sessionState?.checkoutSessionId || !sessionState.orderSummary) {
216
+ throw new Error('Checkout session not initialized for this offer');
217
+ }
218
+ const sessionId = sessionState.checkoutSessionId;
219
+ // Set loading state for this specific variant
220
+ setCheckoutSessions(prev => ({
221
+ ...prev,
222
+ [offerId]: {
223
+ ...prev[offerId],
224
+ loadingVariants: {
225
+ ...prev[offerId].loadingVariants,
226
+ [productId]: true,
230
227
  }
231
- }));
232
- // Fetch order summary with variant options
233
- await fetchOrderSummary(offerId, sessionId);
234
- },
235
- getAvailableVariants: (offerId, productId) => {
236
- const sessionState = checkoutSessions[offerId];
237
- if (!sessionState?.orderSummary?.options?.[productId])
238
- return [];
239
- return sessionState.orderSummary.options[productId].map((variant) => ({
228
+ }
229
+ }));
230
+ try {
231
+ // We need to use the state from the ref to ensure we have the latest data without causing infinite loops
232
+ // if selectVariant was in a dependency array that triggered effects
233
+ const sessionState = checkoutSessionsRef.current[offerId];
234
+ if (!sessionState?.orderSummary?.options?.[productId]) {
235
+ throw new Error('No variants available for this product');
236
+ }
237
+ const availableVariants = sessionState.orderSummary.options[productId].map((variant) => ({
240
238
  variantId: variant.id,
241
239
  variantName: variant.name,
242
240
  variantSku: variant.sku,
@@ -245,98 +243,60 @@ export function useOffersQuery(options = {}) {
245
243
  priceId: variant.prices[0]?.id,
246
244
  currencyOptions: variant.prices[0]?.currencyOptions,
247
245
  }));
248
- },
249
- selectVariant: async (offerId, productId, variantId) => {
250
- const sessionState = checkoutSessions[offerId];
251
- if (!sessionState?.checkoutSessionId || !sessionState.orderSummary) {
252
- throw new Error('Checkout session not initialized for this offer');
246
+ const selectedVariant = availableVariants.find(v => v.variantId === variantId);
247
+ if (!selectedVariant) {
248
+ throw new Error('Selected variant not found');
249
+ }
250
+ // Find the current item to get its quantity
251
+ const currentItem = sessionState.orderSummary.items.find(item => item.productId === productId);
252
+ if (!currentItem) {
253
+ throw new Error('Current item not found');
253
254
  }
254
- const sessionId = sessionState.checkoutSessionId;
255
- // Set loading state for this specific variant
255
+ // Update selected variants state
256
+ setCheckoutSessions(prev => ({
257
+ ...prev,
258
+ [offerId]: {
259
+ ...prev[offerId],
260
+ selectedVariants: {
261
+ ...prev[offerId].selectedVariants,
262
+ [productId]: variantId,
263
+ }
264
+ }
265
+ }));
266
+ // Update line items on the server
267
+ await offersResource.updateLineItems(sessionId, [
268
+ {
269
+ variantId: selectedVariant.variantId,
270
+ quantity: currentItem.quantity,
271
+ },
272
+ ]);
273
+ // Refetch order summary after successful line item update
274
+ return await fetchOrderSummary(offerId, sessionId);
275
+ }
276
+ finally {
277
+ // Clear loading state for this specific variant
256
278
  setCheckoutSessions(prev => ({
257
279
  ...prev,
258
280
  [offerId]: {
259
281
  ...prev[offerId],
260
282
  loadingVariants: {
261
283
  ...prev[offerId].loadingVariants,
262
- [productId]: true,
284
+ [productId]: false,
263
285
  }
264
286
  }
265
287
  }));
266
- try {
267
- const sessionState = checkoutSessions[offerId];
268
- if (!sessionState?.orderSummary?.options?.[productId]) {
269
- throw new Error('No variants available for this product');
270
- }
271
- const availableVariants = sessionState.orderSummary.options[productId].map((variant) => ({
272
- variantId: variant.id,
273
- variantName: variant.name,
274
- variantSku: variant.sku,
275
- variantDefault: variant.default,
276
- variantExternalId: variant.externalVariantId,
277
- priceId: variant.prices[0]?.id,
278
- currencyOptions: variant.prices[0]?.currencyOptions,
279
- }));
280
- const selectedVariant = availableVariants.find(v => v.variantId === variantId);
281
- if (!selectedVariant) {
282
- throw new Error('Selected variant not found');
283
- }
284
- // Find the current item to get its quantity
285
- const currentItem = sessionState.orderSummary.items.find(item => item.productId === productId);
286
- if (!currentItem) {
287
- throw new Error('Current item not found');
288
- }
289
- // Update selected variants state
290
- setCheckoutSessions(prev => ({
291
- ...prev,
292
- [offerId]: {
293
- ...prev[offerId],
294
- selectedVariants: {
295
- ...prev[offerId].selectedVariants,
296
- [productId]: variantId,
297
- }
298
- }
299
- }));
300
- // Update line items on the server
301
- await offersResource.updateLineItems(sessionId, [
302
- {
303
- variantId: selectedVariant.variantId,
304
- quantity: currentItem.quantity,
305
- },
306
- ]);
307
- // Refetch order summary after successful line item update
308
- await fetchOrderSummary(offerId, sessionId);
309
- }
310
- finally {
311
- // Clear loading state for this specific variant
312
- setCheckoutSessions(prev => ({
313
- ...prev,
314
- [offerId]: {
315
- ...prev[offerId],
316
- loadingVariants: {
317
- ...prev[offerId].loadingVariants,
318
- [productId]: false,
319
- }
320
- }
321
- }));
322
- }
323
- },
324
- getOrderSummary: (offerId) => {
325
- return checkoutSessions[offerId]?.orderSummary ?? null;
326
- },
327
- isLoadingVariants: (offerId, productId) => {
328
- return checkoutSessions[offerId]?.loadingVariants?.[productId] ?? false;
329
- },
330
- isUpdatingOrderSummary: (offerId) => {
331
- return checkoutSessions[offerId]?.isUpdatingSummary || false;
332
- },
333
- confirmPurchase: async (offerId, _options) => {
334
- const sessionState = checkoutSessions[offerId];
335
- if (!sessionState?.checkoutSessionId) {
336
- throw new Error('Checkout session not initialized for this offer');
337
- }
338
- // Use the enhanced payWithCheckoutSession with proper metadata
339
- await offersResource.payWithCheckoutSession(sessionState.checkoutSessionId);
340
- },
288
+ }
289
+ }, [offersResource, fetchOrderSummary]); // Removed checkoutSessions dependency
290
+ return {
291
+ // Query data
292
+ offers,
293
+ isLoading,
294
+ error,
295
+ // Actions
296
+ payOffer,
297
+ preview,
298
+ getAvailableVariants,
299
+ selectVariant,
300
+ isLoadingVariants,
341
301
  };
342
302
  }
@@ -87,15 +87,28 @@ export function usePaymentQuery() {
87
87
  },
88
88
  onSuccess: (successPayment) => {
89
89
  setIsLoading(false);
90
- options.onSuccess?.({
90
+ const response = {
91
91
  paymentId: successPayment.id,
92
92
  payment: successPayment,
93
- });
93
+ // Extract order from payment if available (for funnel path resolution)
94
+ order: successPayment.order,
95
+ };
96
+ // Legacy callback (backwards compatibility)
97
+ options.onSuccess?.(response);
98
+ // Funnel-aligned callback (recommended)
99
+ options.onPaymentSuccess?.(response);
94
100
  },
95
101
  onFailure: (errorMsg) => {
96
102
  setError(errorMsg);
97
103
  setIsLoading(false);
104
+ // Legacy callback (backwards compatibility)
98
105
  options.onFailure?.(errorMsg);
106
+ // Funnel-aligned callback (recommended)
107
+ options.onPaymentFailed?.({
108
+ code: 'PAYMENT_FAILED',
109
+ message: errorMsg,
110
+ payment,
111
+ });
99
112
  },
100
113
  });
101
114
  }
@@ -105,22 +118,57 @@ export function usePaymentQuery() {
105
118
  const errorMsg = _error instanceof Error ? _error.message : 'Failed to start 3DS challenge';
106
119
  setError(errorMsg);
107
120
  setIsLoading(false);
121
+ // Legacy callback (backwards compatibility)
108
122
  options.onFailure?.(errorMsg);
123
+ // Funnel-aligned callback (recommended)
124
+ options.onPaymentFailed?.({
125
+ code: '3DS_CHALLENGE_FAILED',
126
+ message: errorMsg,
127
+ payment,
128
+ });
109
129
  }
110
130
  }
111
131
  break;
112
132
  case 'processor_auth':
113
133
  case 'redirect': {
114
- if (actionData.metadata?.redirect?.redirectUrl) {
134
+ // Only auto-redirect if explicitly enabled (disableAutoRedirect: false)
135
+ // Default behavior: disable redirects and let funnel orchestrator handle navigation
136
+ const shouldRedirect = options.disableAutoRedirect === false;
137
+ if (shouldRedirect && actionData.metadata?.redirect?.redirectUrl) {
115
138
  window.location.href = actionData.metadata.redirect.redirectUrl;
116
139
  }
140
+ else {
141
+ // If auto-redirect is disabled AND payment succeeded, call success callbacks
142
+ // This allows funnel orchestrator to handle navigation
143
+ if (payment.status === 'succeeded') {
144
+ setIsLoading(false);
145
+ const response = {
146
+ paymentId: payment.id,
147
+ payment,
148
+ // Extract order from payment if available (for funnel path resolution)
149
+ order: payment.order,
150
+ };
151
+ // Legacy callback (backwards compatibility)
152
+ options.onSuccess?.(response);
153
+ // Funnel-aligned callback (recommended)
154
+ options.onPaymentSuccess?.(response);
155
+ }
156
+ }
117
157
  break;
118
158
  }
119
159
  case 'error': {
120
160
  const errorMsg = actionData.message || 'Payment processing failed';
161
+ const errorCode = actionData.errorCode || 'PAYMENT_FAILED';
121
162
  setError(errorMsg);
122
163
  setIsLoading(false);
164
+ // Legacy callback (backwards compatibility)
123
165
  options.onFailure?.(errorMsg);
166
+ // Funnel-aligned callback (recommended)
167
+ options.onPaymentFailed?.({
168
+ code: errorCode,
169
+ message: errorMsg,
170
+ payment,
171
+ });
124
172
  break;
125
173
  }
126
174
  }
@@ -147,7 +195,15 @@ export function usePaymentQuery() {
147
195
  }
148
196
  else if (response.payment.status === 'succeeded') {
149
197
  setIsLoading(false);
150
- options.onSuccess?.(response);
198
+ // Ensure order is at response root (extract from payment if needed)
199
+ const successResponse = {
200
+ ...response,
201
+ order: response.order || response.payment.order,
202
+ };
203
+ // Legacy callback (backwards compatibility)
204
+ options.onSuccess?.(successResponse);
205
+ // Funnel-aligned callback (recommended)
206
+ options.onPaymentSuccess?.(successResponse);
151
207
  }
152
208
  else {
153
209
  // Start polling for payment status
@@ -157,15 +213,28 @@ export function usePaymentQuery() {
157
213
  },
158
214
  onSuccess: (payment) => {
159
215
  setIsLoading(false);
160
- options.onSuccess?.({
216
+ const successResponse = {
161
217
  paymentId: payment.id,
162
218
  payment,
163
- });
219
+ // Extract order from payment if available (for funnel path resolution)
220
+ order: payment.order,
221
+ };
222
+ // Legacy callback (backwards compatibility)
223
+ options.onSuccess?.(successResponse);
224
+ // Funnel-aligned callback (recommended)
225
+ options.onPaymentSuccess?.(successResponse);
164
226
  },
165
227
  onFailure: (errorMsg) => {
166
228
  setError(errorMsg);
167
229
  setIsLoading(false);
230
+ // Legacy callback (backwards compatibility)
168
231
  options.onFailure?.(errorMsg);
232
+ // Funnel-aligned callback (recommended)
233
+ options.onPaymentFailed?.({
234
+ code: 'PAYMENT_FAILED',
235
+ message: errorMsg,
236
+ payment: response.payment,
237
+ });
169
238
  },
170
239
  });
171
240
  }
@@ -175,7 +244,13 @@ export function usePaymentQuery() {
175
244
  const errorMsg = _error instanceof Error ? _error.message : 'Payment failed';
176
245
  setError(errorMsg);
177
246
  setIsLoading(false);
247
+ // Legacy callback (backwards compatibility)
178
248
  options.onFailure?.(errorMsg);
249
+ // Funnel-aligned callback (recommended)
250
+ options.onPaymentFailed?.({
251
+ code: 'PAYMENT_PROCESSING_ERROR',
252
+ message: errorMsg,
253
+ });
179
254
  throw _error;
180
255
  }
181
256
  }, [paymentsResource, handlePaymentAction, startPolling]);
@@ -205,7 +280,13 @@ export function usePaymentQuery() {
205
280
  setIsLoading(false);
206
281
  const errorMsg = _error instanceof Error ? _error.message : 'Payment failed';
207
282
  setError(errorMsg);
283
+ // Legacy callback (backwards compatibility)
208
284
  options.onFailure?.(errorMsg);
285
+ // Funnel-aligned callback (recommended)
286
+ options.onPaymentFailed?.({
287
+ code: 'CARD_PAYMENT_ERROR',
288
+ message: errorMsg,
289
+ });
209
290
  throw _error;
210
291
  }
211
292
  }, [createCardPaymentInstrument, createSession, processPaymentDirect]);
@@ -223,7 +304,13 @@ export function usePaymentQuery() {
223
304
  setIsLoading(false);
224
305
  const errorMsg = _error instanceof Error ? _error.message : 'Apple Pay payment failed';
225
306
  setError(errorMsg);
307
+ // Legacy callback (backwards compatibility)
226
308
  options.onFailure?.(errorMsg);
309
+ // Funnel-aligned callback (recommended)
310
+ options.onPaymentFailed?.({
311
+ code: 'APPLE_PAY_ERROR',
312
+ message: errorMsg,
313
+ });
227
314
  throw _error;
228
315
  }
229
316
  }, [createApplePayPaymentInstrument, processPaymentDirect]);
@@ -238,7 +325,13 @@ export function usePaymentQuery() {
238
325
  setIsLoading(false);
239
326
  const errorMsg = _error instanceof Error ? _error.message : 'Payment failed';
240
327
  setError(errorMsg);
328
+ // Legacy callback (backwards compatibility)
241
329
  options.onFailure?.(errorMsg);
330
+ // Funnel-aligned callback (recommended)
331
+ options.onPaymentFailed?.({
332
+ code: 'PAYMENT_INSTRUMENT_ERROR',
333
+ message: errorMsg,
334
+ });
242
335
  throw _error;
243
336
  }
244
337
  }, [processPaymentDirect]);