@tagadapay/plugin-sdk 2.7.20 → 2.7.22

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.
@@ -3,16 +3,54 @@
3
3
  * Handles post-purchase offers with automatic caching
4
4
  */
5
5
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
6
- import { useCallback, useEffect, useMemo, useState } from 'react';
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
7
  import { PostPurchasesResource } from '../../core/resources/postPurchases';
8
8
  import { useTagadaContext } from '../providers/TagadaProvider';
9
- import { getGlobalApiClient } from './useApiQuery';
9
+ import { getGlobalApiClient, getGlobalApiClientOrNull } from './useApiQuery';
10
10
  export function usePostPurchasesQuery(options) {
11
+ console.log('usePostPurchasesQuery');
11
12
  const { orderId, enabled = true, autoInitializeCheckout = false } = options;
12
13
  const queryClient = useQueryClient();
13
- const { session } = useTagadaContext();
14
+ const { session, isSessionInitialized, apiService } = useTagadaContext();
15
+ // Track if token is available on API service
16
+ const [hasToken, setHasToken] = useState(() => apiService.getCurrentToken() !== null);
17
+ // Update token availability when session is initialized
18
+ useEffect(() => {
19
+ if (isSessionInitialized) {
20
+ const checkToken = () => {
21
+ const token = apiService.getCurrentToken();
22
+ const tokenAvailable = token !== null;
23
+ setHasToken(tokenAvailable);
24
+ // Also ensure API client has the token
25
+ const apiClient = getGlobalApiClientOrNull();
26
+ if (apiClient && token) {
27
+ apiClient.updateToken(token);
28
+ }
29
+ return tokenAvailable;
30
+ };
31
+ // Check immediately
32
+ if (checkToken()) {
33
+ return; // Token is available, no need to poll
34
+ }
35
+ // Poll for token if not immediately available (handles async token setting)
36
+ const interval = setInterval(() => {
37
+ if (checkToken()) {
38
+ clearInterval(interval);
39
+ }
40
+ }, 100); // Check every 100ms
41
+ return () => clearInterval(interval);
42
+ }
43
+ }, [isSessionInitialized, apiService]);
14
44
  // State for checkout sessions per offer
15
45
  const [checkoutSessions, setCheckoutSessions] = useState({});
46
+ // Ref to track checkout sessions for synchronous access
47
+ const checkoutSessionsRef = useRef({});
48
+ // Ref to track offers being initialized to prevent duplicate initialization
49
+ const initializingOffers = useRef(new Set());
50
+ // Keep ref in sync with state
51
+ useEffect(() => {
52
+ checkoutSessionsRef.current = checkoutSessions;
53
+ }, [checkoutSessions]);
16
54
  // Create post purchases resource client
17
55
  const postPurchasesResource = useMemo(() => {
18
56
  try {
@@ -70,28 +108,102 @@ export function usePostPurchasesQuery(options) {
70
108
  throw error;
71
109
  }
72
110
  }, [postPurchasesResource]);
111
+ // Initialize checkout session for an offer
112
+ const initializeOfferCheckout = useCallback(async (offerId) => {
113
+ try {
114
+ // Check if customer ID is available
115
+ if (!session?.customerId) {
116
+ throw new Error('Customer ID is required. Make sure the session is properly initialized.');
117
+ }
118
+ // Check if already initializing to avoid race conditions
119
+ if (initializingOffers.current.has(offerId)) {
120
+ return; // Already initializing, skip
121
+ }
122
+ // Check if already initialized using ref for synchronous access
123
+ if (checkoutSessionsRef.current[offerId]?.checkoutSessionId) {
124
+ return; // Already initialized, exit early
125
+ }
126
+ // Mark as initializing
127
+ initializingOffers.current.add(offerId);
128
+ try {
129
+ // Initialize checkout session
130
+ const initResult = await postPurchasesResource.initCheckoutSession(offerId, orderId, session.customerId);
131
+ if (!initResult.checkoutSessionId) {
132
+ throw new Error('Failed to initialize checkout session');
133
+ }
134
+ const sessionId = initResult.checkoutSessionId;
135
+ // Initialize session state
136
+ setCheckoutSessions(prev => {
137
+ // Double-check we're not overwriting an existing session
138
+ if (prev[offerId]?.checkoutSessionId) {
139
+ return prev;
140
+ }
141
+ return {
142
+ ...prev,
143
+ [offerId]: {
144
+ checkoutSessionId: sessionId,
145
+ orderSummary: null,
146
+ selectedVariants: {},
147
+ loadingVariants: {},
148
+ isUpdatingSummary: false,
149
+ }
150
+ };
151
+ });
152
+ // Fetch order summary with variant options
153
+ await fetchOrderSummary(offerId, sessionId);
154
+ }
155
+ finally {
156
+ // Remove from initializing set
157
+ initializingOffers.current.delete(offerId);
158
+ }
159
+ }
160
+ catch (error) {
161
+ // Remove from initializing set on error
162
+ initializingOffers.current.delete(offerId);
163
+ console.error(`[SDK] Failed to initialize checkout for offer ${offerId}:`, error);
164
+ throw error;
165
+ }
166
+ }, [session?.customerId, orderId, postPurchasesResource, fetchOrderSummary]);
73
167
  // Main post-purchase offers query
74
168
  const { data: offers = [], isLoading, error, refetch, } = useQuery({
75
169
  queryKey: ['post-purchase-offers', orderId],
76
- queryFn: () => postPurchasesResource.getPostPurchaseOffers(orderId),
77
- enabled: enabled && !!orderId,
170
+ queryFn: async () => {
171
+ // Double-check token is available before making the request
172
+ const token = apiService.getCurrentToken();
173
+ if (!token) {
174
+ throw new Error('No authentication token available. Please wait for session initialization.');
175
+ }
176
+ // Ensure API client has the token
177
+ const apiClient = getGlobalApiClientOrNull();
178
+ if (apiClient && token) {
179
+ apiClient.updateToken(token);
180
+ }
181
+ return postPurchasesResource.getPostPurchaseOffers(orderId);
182
+ },
183
+ enabled: enabled && !!orderId && isSessionInitialized && hasToken, // Wait for CMS token/session to be ready AND token to be set on API client
78
184
  staleTime: 30000, // 30 seconds
79
185
  refetchOnWindowFocus: false,
186
+ retry: (failureCount, error) => {
187
+ // Retry if token is not available yet (might be a timing issue)
188
+ if (error instanceof Error && error.message.includes('No authentication token')) {
189
+ return failureCount < 3; // Retry up to 3 times
190
+ }
191
+ return false; // Don't retry other errors
192
+ },
80
193
  });
81
194
  // Auto-initialize checkout sessions if enabled
82
195
  useEffect(() => {
83
- if (autoInitializeCheckout && offers.length > 0) {
196
+ if (autoInitializeCheckout && offers.length > 0 && session?.customerId && isSessionInitialized) {
84
197
  // Initialize checkout sessions for all offers
85
198
  offers.forEach((offer) => {
86
- try {
87
- // This is a placeholder - in a full implementation, you'd call initializeOfferCheckout
88
- }
89
- catch (_error) {
90
- // Error handling removed
91
- }
199
+ // The initializeOfferCheckout function will check if already initialized or initializing
200
+ void initializeOfferCheckout(offer.id).catch((error) => {
201
+ // Log errors but don't throw - auto-initialization failures shouldn't break the UI
202
+ console.error(`[SDK] Failed to auto-initialize checkout for offer ${offer.id}:`, error);
203
+ });
92
204
  });
93
205
  }
94
- }, [autoInitializeCheckout, offers]);
206
+ }, [autoInitializeCheckout, offers, session?.customerId, isSessionInitialized, initializeOfferCheckout]);
95
207
  // Accept offer mutation
96
208
  const acceptMutation = useMutation({
97
209
  mutationFn: ({ offerId, items }) => {
@@ -201,40 +313,14 @@ export function usePostPurchasesQuery(options) {
201
313
  getCheckoutSessionState: (offerId) => {
202
314
  return checkoutSessions[offerId] || null;
203
315
  },
204
- initializeOfferCheckout: async (offerId) => {
205
- try {
206
- // Check if customer ID is available
207
- if (!session?.customerId) {
208
- throw new Error('Customer ID is required. Make sure the session is properly initialized.');
209
- }
210
- // Initialize checkout session
211
- const initResult = await postPurchasesResource.initCheckoutSession(offerId, orderId, session.customerId);
212
- if (!initResult.checkoutSessionId) {
213
- throw new Error('Failed to initialize checkout session');
214
- }
215
- const sessionId = initResult.checkoutSessionId;
216
- // Initialize session state
217
- setCheckoutSessions(prev => ({
218
- ...prev,
219
- [offerId]: {
220
- checkoutSessionId: sessionId,
221
- orderSummary: null,
222
- selectedVariants: {},
223
- loadingVariants: {},
224
- isUpdatingSummary: false,
225
- }
226
- }));
227
- // Fetch order summary with variant options
228
- await fetchOrderSummary(offerId, sessionId);
229
- }
230
- catch (_error) {
231
- throw _error;
232
- }
233
- },
316
+ initializeOfferCheckout,
234
317
  getAvailableVariants: (offerId, productId) => {
235
318
  const sessionState = checkoutSessions[offerId];
319
+ console.log('sessionState', sessionState);
236
320
  if (!sessionState?.orderSummary?.options?.[productId])
237
321
  return [];
322
+ console.log('getAvailableVariants', offerId, productId);
323
+ console.log('sessionState.orderSummary.options', sessionState.orderSummary.options);
238
324
  return sessionState.orderSummary.options[productId].map((variant) => ({
239
325
  variantId: variant.id,
240
326
  variantName: variant.name,
@@ -55,6 +55,7 @@ export type { ExtractedAddress, GooglePlaceDetails, GooglePrediction, UseGoogleA
55
55
  export type { ISOCountry, ISORegion, UseISODataResult } from './hooks/useISOData';
56
56
  export type { UsePluginConfigOptions, UsePluginConfigResult } from './hooks/usePluginConfig';
57
57
  export type { TranslateFunction, UseTranslationOptions, UseTranslationResult } from './hooks/useTranslation';
58
+ export { FunnelEventType } from '../core/resources/funnel';
58
59
  export type { UseCheckoutQueryOptions as UseCheckoutOptions, UseCheckoutQueryResult as UseCheckoutResult } from './hooks/useCheckoutQuery';
59
60
  export type { UseDiscountsQueryOptions as UseDiscountsOptions, UseDiscountsQueryResult as UseDiscountsResult } from './hooks/useDiscountsQuery';
60
61
  export type { FunnelEvent, FunnelNavigationAction, FunnelNavigationResult, SimpleFunnelContext, UseFunnelOptions, UseFunnelResult } from './hooks/useFunnel';
@@ -44,5 +44,7 @@ export { useTranslation } from './hooks/useTranslation';
44
44
  export { useVipOffersQuery as useVipOffers } from './hooks/useVipOffersQuery';
45
45
  // Funnel hooks
46
46
  export { useFunnel, useSimpleFunnel } from './hooks/useFunnel';
47
+ // TanStack Query types
48
+ export { FunnelEventType } from '../core/resources/funnel';
47
49
  // Re-export utilities from main react
48
50
  export { formatMoney } from '../../react/utils/money';
@@ -26,6 +26,14 @@ interface TagadaContextValue {
26
26
  lastUpdated: Date | null;
27
27
  };
28
28
  updateCheckoutDebugData: (data: any, error?: Error | null, isLoading?: boolean) => void;
29
+ debugFunnel: {
30
+ isActive: boolean;
31
+ data: any;
32
+ error: Error | null;
33
+ isLoading: boolean;
34
+ lastUpdated: Date | null;
35
+ };
36
+ updateFunnelDebugData: (data: any, error?: Error | null, isLoading?: boolean) => void;
29
37
  refreshCoordinator: {
30
38
  registerCheckoutRefresh: (refreshFn: () => Promise<void>, name?: string) => void;
31
39
  registerOrderBumpRefresh: (refreshFn: () => Promise<void>) => void;
@@ -295,6 +295,13 @@ rawPluginConfig, }) {
295
295
  isLoading: false,
296
296
  lastUpdated: null,
297
297
  });
298
+ const [debugFunnel, setDebugFunnel] = useState({
299
+ isActive: false,
300
+ data: null,
301
+ error: null,
302
+ isLoading: false,
303
+ lastUpdated: null,
304
+ });
298
305
  // Initialize auth state
299
306
  const [auth, setAuth] = useState({
300
307
  isAuthenticated: false,
@@ -480,6 +487,65 @@ rawPluginConfig, }) {
480
487
  setIsLoading(false);
481
488
  }
482
489
  }, [apiService, hasAttemptedAnonymousToken, initializeSession, finalDebugMode, isActiveInstance, instanceId]);
490
+ // Initialize token from storage or create anonymous token (extracted to stable callback)
491
+ const initializeToken = useCallback(async () => {
492
+ if (!isActiveInstance) {
493
+ console.log(`🚫 [TagadaProvider] Instance ${instanceId} is not active, skipping token initialization`);
494
+ return;
495
+ }
496
+ try {
497
+ console.debug('[SDK] Initializing token...');
498
+ setIsLoading(true);
499
+ // Check for existing token
500
+ const existingToken = getClientToken();
501
+ let tokenToUse = null;
502
+ // Check URL params for token
503
+ const urlParams = new URLSearchParams(window.location.search);
504
+ const queryToken = urlParams.get('token');
505
+ if (queryToken) {
506
+ console.debug('[SDK] Found token in URL params');
507
+ tokenToUse = queryToken;
508
+ setClientToken(queryToken);
509
+ }
510
+ else if (existingToken && !isTokenExpired(existingToken)) {
511
+ console.debug('[SDK] Using existing token from storage');
512
+ tokenToUse = existingToken;
513
+ }
514
+ else {
515
+ console.debug('[SDK] No valid token found');
516
+ // Determine storeId for anonymous token
517
+ const targetStoreId = storeId || 'default-store';
518
+ await createAnonymousToken(targetStoreId);
519
+ return;
520
+ }
521
+ if (tokenToUse) {
522
+ setToken(tokenToUse);
523
+ // Update the API service with the token
524
+ apiService.updateToken(tokenToUse);
525
+ // IMPORTANT: Immediately sync token to API client
526
+ apiClient.updateToken(tokenToUse);
527
+ console.log('[SDK] Token immediately synced to ApiClient:', tokenToUse.substring(0, 8) + '...');
528
+ // Decode token to get session data
529
+ const decodedSession = decodeJWTClient(tokenToUse);
530
+ if (decodedSession) {
531
+ setSession(decodedSession);
532
+ // Initialize session with API call
533
+ await initializeSession(decodedSession);
534
+ }
535
+ else {
536
+ console.error('[SDK] Failed to decode token');
537
+ setIsInitialized(true);
538
+ setIsSessionInitialized(false); // Session failed to initialize
539
+ setIsLoading(false);
540
+ }
541
+ }
542
+ }
543
+ catch (error) {
544
+ console.error('[SDK] Error initializing token:', error);
545
+ setIsInitialized(true);
546
+ setIsLoading(false);
547
+ }
548
+ }, [apiService, apiClient, storeId, createAnonymousToken, initializeSession, isActiveInstance, instanceId]);
483
549
  // Initialize token from storage or create anonymous token
484
550
  // This runs in the background after phases 1 & 2 complete, but doesn't block rendering
485
551
  useEffect(() => {
@@ -500,62 +566,23 @@ rawPluginConfig, }) {
500
566
  return;
501
567
  }
502
568
  isInitializing.current = true;
503
- const initializeToken = async () => {
504
- try {
505
- console.debug('[SDK] Initializing token...');
506
- setIsLoading(true);
507
- // Check for existing token
508
- const existingToken = getClientToken();
509
- let tokenToUse = null;
510
- // Check URL params for token
511
- const urlParams = new URLSearchParams(window.location.search);
512
- const queryToken = urlParams.get('token');
513
- if (queryToken) {
514
- console.debug('[SDK] Found token in URL params');
515
- tokenToUse = queryToken;
516
- setClientToken(queryToken);
517
- }
518
- else if (existingToken && !isTokenExpired(existingToken)) {
519
- console.debug('[SDK] Using existing token from storage');
520
- tokenToUse = existingToken;
521
- }
522
- else {
523
- console.debug('[SDK] No valid token found');
524
- // Determine storeId for anonymous token
525
- const targetStoreId = storeId || 'default-store';
526
- await createAnonymousToken(targetStoreId);
527
- return;
528
- }
529
- if (tokenToUse) {
530
- setToken(tokenToUse);
531
- // Update the API service with the token
532
- apiService.updateToken(tokenToUse);
533
- // IMPORTANT: Immediately sync token to API client
534
- apiClient.updateToken(tokenToUse);
535
- console.log('[SDK] Token immediately synced to ApiClient:', tokenToUse.substring(0, 8) + '...');
536
- // Decode token to get session data
537
- const decodedSession = decodeJWTClient(tokenToUse);
538
- if (decodedSession) {
539
- setSession(decodedSession);
540
- // Initialize session with API call
541
- await initializeSession(decodedSession);
542
- }
543
- else {
544
- console.error('[SDK] Failed to decode token');
545
- setIsInitialized(true);
546
- setIsSessionInitialized(false); // Session failed to initialize
547
- setIsLoading(false);
548
- }
549
- }
550
- }
551
- catch (error) {
552
- console.error('[SDK] Error initializing token:', error);
553
- setIsInitialized(true);
554
- setIsLoading(false);
555
- }
556
- };
557
569
  void initializeToken();
558
- }, [storeId, createAnonymousToken, initializeSession, configLoading, isActiveInstance, instanceId]);
570
+ }, [storeId, initializeToken, configLoading, isActiveInstance, instanceId]);
571
+ // Listen for storage changes (e.g., token updated in another tab)
572
+ useEffect(() => {
573
+ if (!isActiveInstance) {
574
+ return;
575
+ }
576
+ function onStorage() {
577
+ // Re-run initialization when token may have changed in another tab
578
+ isInitializing.current = false;
579
+ void initializeToken();
580
+ }
581
+ window.addEventListener('storage', onStorage);
582
+ return () => {
583
+ window.removeEventListener('storage', onStorage);
584
+ };
585
+ }, [initializeToken, isActiveInstance]);
559
586
  // Update auth state when customer/session changes
560
587
  useEffect(() => {
561
588
  setAuth({
@@ -671,6 +698,16 @@ rawPluginConfig, }) {
671
698
  pluginConfigLoading: configLoading,
672
699
  debugCheckout,
673
700
  updateCheckoutDebugData: memoizedUpdateDebugData,
701
+ debugFunnel,
702
+ updateFunnelDebugData: (data, error, isLoading) => {
703
+ setDebugFunnel({
704
+ isActive: true,
705
+ data,
706
+ error: error ?? null,
707
+ isLoading: isLoading ?? false,
708
+ lastUpdated: new Date(),
709
+ });
710
+ },
674
711
  refreshCoordinator,
675
712
  money: memoizedMoneyUtils,
676
713
  }), [
@@ -685,6 +722,8 @@ rawPluginConfig, }) {
685
722
  isLoading,
686
723
  isInitialized,
687
724
  isSessionInitialized,
725
+ debugCheckout,
726
+ debugFunnel,
688
727
  finalDebugMode,
689
728
  pluginConfig,
690
729
  configLoading,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagadapay/plugin-sdk",
3
- "version": "2.7.20",
3
+ "version": "2.7.22",
4
4
  "description": "Modern React SDK for building Tagada Pay plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",