@tagadapay/plugin-sdk 2.8.9 → 3.0.1

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 (54) hide show
  1. package/README.md +14 -14
  2. package/dist/index.js +1 -1
  3. package/dist/react/hooks/usePluginConfig.d.ts +1 -0
  4. package/dist/react/hooks/usePluginConfig.js +69 -18
  5. package/dist/react/providers/TagadaProvider.js +1 -4
  6. package/dist/v2/core/client.d.ts +22 -0
  7. package/dist/v2/core/client.js +90 -1
  8. package/dist/v2/core/config/environment.d.ts +24 -2
  9. package/dist/v2/core/config/environment.js +58 -25
  10. package/dist/v2/core/funnelClient.d.ts +84 -0
  11. package/dist/v2/core/funnelClient.js +252 -0
  12. package/dist/v2/core/index.d.ts +2 -0
  13. package/dist/v2/core/index.js +3 -0
  14. package/dist/v2/core/resources/apiClient.d.ts +5 -0
  15. package/dist/v2/core/resources/apiClient.js +47 -0
  16. package/dist/v2/core/resources/funnel.d.ts +8 -0
  17. package/dist/v2/core/resources/offers.d.ts +182 -8
  18. package/dist/v2/core/resources/offers.js +25 -0
  19. package/dist/v2/core/resources/products.d.ts +5 -0
  20. package/dist/v2/core/resources/products.js +15 -1
  21. package/dist/v2/core/types.d.ts +1 -0
  22. package/dist/v2/core/utils/funnelQueryKeys.d.ts +23 -0
  23. package/dist/v2/core/utils/funnelQueryKeys.js +23 -0
  24. package/dist/v2/core/utils/index.d.ts +2 -0
  25. package/dist/v2/core/utils/index.js +2 -0
  26. package/dist/v2/core/utils/pluginConfig.d.ts +1 -0
  27. package/dist/v2/core/utils/pluginConfig.js +84 -22
  28. package/dist/v2/core/utils/sessionStorage.d.ts +20 -0
  29. package/dist/v2/core/utils/sessionStorage.js +39 -0
  30. package/dist/v2/index.d.ts +3 -2
  31. package/dist/v2/index.js +1 -1
  32. package/dist/v2/react/components/ApplePayButton.js +1 -1
  33. package/dist/v2/react/hooks/__examples__/FunnelContextExample.d.ts +3 -0
  34. package/dist/v2/react/hooks/__examples__/FunnelContextExample.js +4 -3
  35. package/dist/v2/react/hooks/useClubOffers.d.ts +2 -2
  36. package/dist/v2/react/hooks/useFunnel.d.ts +27 -38
  37. package/dist/v2/react/hooks/useFunnel.js +22 -660
  38. package/dist/v2/react/hooks/useFunnelLegacy.d.ts +52 -0
  39. package/dist/v2/react/hooks/useFunnelLegacy.js +733 -0
  40. package/dist/v2/react/hooks/useOfferQuery.d.ts +109 -0
  41. package/dist/v2/react/hooks/useOfferQuery.js +483 -0
  42. package/dist/v2/react/hooks/useOffersQuery.d.ts +10 -58
  43. package/dist/v2/react/hooks/useOffersQuery.js +110 -8
  44. package/dist/v2/react/hooks/useProductsQuery.d.ts +1 -0
  45. package/dist/v2/react/hooks/useProductsQuery.js +10 -6
  46. package/dist/v2/react/index.d.ts +7 -4
  47. package/dist/v2/react/index.js +4 -2
  48. package/dist/v2/react/providers/TagadaProvider.d.ts +45 -2
  49. package/dist/v2/react/providers/TagadaProvider.js +116 -3
  50. package/dist/v2/standalone/index.d.ts +20 -0
  51. package/dist/v2/standalone/index.js +22 -0
  52. package/dist/v2/vue/index.d.ts +6 -0
  53. package/dist/v2/vue/index.js +10 -0
  54. package/package.json +6 -1
@@ -3,18 +3,26 @@
3
3
  * Handles offers with automatic caching
4
4
  */
5
5
  import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
6
- import { useCallback, useMemo, useRef, useState } from 'react';
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
7
7
  import { OffersResource } from '../../core/resources/offers';
8
8
  import { useTagadaContext } from '../providers/TagadaProvider';
9
9
  import { getGlobalApiClient } from './useApiQuery';
10
10
  import { usePluginConfig } from './usePluginConfig';
11
11
  export function useOffersQuery(options = {}) {
12
- const { offerIds = [], enabled = true, returnUrl, orderId: defaultOrderId } = options;
12
+ const { offerIds = [], enabled = true, returnUrl, orderId: defaultOrderId, activeOfferId, skipPreview = false } = options;
13
13
  const { storeId } = usePluginConfig();
14
14
  const { isSessionInitialized, session } = useTagadaContext();
15
15
  const _queryClient = useQueryClient();
16
+ // Version identifier for debugging
17
+ console.log('[useOffersQuery] Hook initialized - VERSION: 2.2-production-ready', {
18
+ activeOfferId,
19
+ skipPreview,
20
+ isSessionInitialized,
21
+ });
16
22
  // State for checkout sessions per offer (similar to postPurchases)
17
23
  const [checkoutSessions, setCheckoutSessions] = useState({});
24
+ const [isActiveSummaryLoading, setIsActiveSummaryLoading] = useState(false);
25
+ const lastPreviewedOfferRef = useRef(null);
18
26
  // Use ref to break dependency cycles in callbacks
19
27
  const checkoutSessionsRef = useRef(checkoutSessions);
20
28
  // Update ref on every render
@@ -30,6 +38,8 @@ export function useOffersQuery(options = {}) {
30
38
  }, []);
31
39
  // Helper function for fetching order summary (similar to postPurchases)
32
40
  const fetchOrderSummary = useCallback(async (offerId, sessionId) => {
41
+ if (!isSessionInitialized)
42
+ return null;
33
43
  try {
34
44
  // Set updating state
35
45
  setCheckoutSessions(prev => ({
@@ -77,7 +87,7 @@ export function useOffersQuery(options = {}) {
77
87
  }));
78
88
  throw error;
79
89
  }
80
- }, [offersResource]);
90
+ }, [offersResource, isSessionInitialized]);
81
91
  // Create query key based on options
82
92
  const queryKey = useMemo(() => ['offers', { storeId, offerIds }], [storeId, offerIds]);
83
93
  // Use TanStack Query for fetching offers
@@ -120,6 +130,9 @@ export function useOffersQuery(options = {}) {
120
130
  return await payWithCheckoutSessionAsync({ checkoutSessionId, orderId });
121
131
  }, [payWithCheckoutSessionAsync]);
122
132
  const initCheckoutSession = useCallback(async (offerId, orderId, customerId) => {
133
+ if (!isSessionInitialized) {
134
+ throw new Error('Cannot initialize checkout session: CMS session is not initialized');
135
+ }
123
136
  // Use customerId from session context if not provided
124
137
  const effectiveCustomerId = customerId || session?.customerId;
125
138
  if (!effectiveCustomerId) {
@@ -130,8 +143,11 @@ export function useOffersQuery(options = {}) {
130
143
  orderId,
131
144
  customerId: effectiveCustomerId
132
145
  });
133
- }, [initCheckoutSessionAsync, session?.customerId]);
146
+ }, [initCheckoutSessionAsync, session?.customerId, isSessionInitialized]);
134
147
  const payOffer = useCallback(async (offerId, orderId) => {
148
+ if (!isSessionInitialized) {
149
+ throw new Error('Cannot pay offer: CMS session is not initialized');
150
+ }
135
151
  const effectiveOrderId = orderId || defaultOrderId;
136
152
  const effectiveCustomerId = session?.customerId;
137
153
  if (!effectiveOrderId) {
@@ -144,18 +160,26 @@ export function useOffersQuery(options = {}) {
144
160
  const { checkoutSessionId } = await initCheckoutSession(offerId, effectiveOrderId, effectiveCustomerId);
145
161
  // 2. Pay
146
162
  await payWithCheckoutSession(checkoutSessionId, effectiveOrderId);
147
- }, [initCheckoutSession, payWithCheckoutSession, defaultOrderId, session?.customerId]);
163
+ }, [initCheckoutSession, payWithCheckoutSession, defaultOrderId, session?.customerId, isSessionInitialized]);
148
164
  const preview = useCallback(async (offerId) => {
165
+ console.log('[useOffersQuery] preview() called for offer:', offerId);
166
+ if (!isSessionInitialized) {
167
+ console.log('[useOffersQuery] preview() - session not initialized, returning null');
168
+ // Return null silently to avoid errors during auto-initialization phases
169
+ return null;
170
+ }
149
171
  const effectiveOrderId = defaultOrderId;
150
172
  const effectiveCustomerId = session?.customerId;
151
173
  // Use ref to check current state without creating dependency
152
174
  const currentSessions = checkoutSessionsRef.current;
153
175
  // If we already have a summary in state, return it
154
176
  if (currentSessions[offerId]?.orderSummary) {
177
+ console.log('[useOffersQuery] preview() - using cached summary for offer:', offerId);
155
178
  return currentSessions[offerId].orderSummary;
156
179
  }
157
180
  // Prevent duplicate initialization if already has a session and is updating
158
181
  if (currentSessions[offerId]?.checkoutSessionId && currentSessions[offerId]?.isUpdatingSummary) {
182
+ console.log('[useOffersQuery] preview() - already updating, skipping for offer:', offerId);
159
183
  return null;
160
184
  }
161
185
  // If we don't have orderId, fallback to static summary from offer object
@@ -189,7 +213,71 @@ export function useOffersQuery(options = {}) {
189
213
  console.error("Failed to preview offer", e);
190
214
  return null;
191
215
  }
192
- }, [offers, defaultOrderId, session?.customerId, initCheckoutSession, fetchOrderSummary]); // Removed checkoutSessions dependency
216
+ }, [offers, defaultOrderId, session?.customerId, initCheckoutSession, fetchOrderSummary, isSessionInitialized]); // Removed checkoutSessions dependency
217
+ // Auto-preview effect for activeOfferId
218
+ useEffect(() => {
219
+ console.log('[useOffersQuery v2.2] Auto-preview effect triggered:', {
220
+ activeOfferId,
221
+ skipPreview,
222
+ isSessionInitialized,
223
+ lastPreviewed: lastPreviewedOfferRef.current,
224
+ });
225
+ if (!activeOfferId || skipPreview || !isSessionInitialized) {
226
+ console.log('[useOffersQuery] Skipping auto-preview - conditions not met');
227
+ setIsActiveSummaryLoading(false); // Reset loading state if conditions not met
228
+ // Reset the ref when conditions are not met
229
+ if (!activeOfferId) {
230
+ lastPreviewedOfferRef.current = null;
231
+ }
232
+ return;
233
+ }
234
+ // Skip if we've already previewed this exact offer
235
+ if (lastPreviewedOfferRef.current === activeOfferId) {
236
+ console.log('[useOffersQuery] Skipping auto-preview - already previewed:', activeOfferId);
237
+ setIsActiveSummaryLoading(false); // Ensure loading is false
238
+ return;
239
+ }
240
+ console.log('[useOffersQuery] Starting auto-preview for offer:', activeOfferId);
241
+ let isMounted = true;
242
+ setIsActiveSummaryLoading(true); // Set loading immediately
243
+ // Debounce the preview call
244
+ const timer = setTimeout(() => {
245
+ if (!isMounted) {
246
+ console.log('[useOffersQuery] Component unmounted before preview call');
247
+ return;
248
+ }
249
+ console.log('[useOffersQuery] Calling preview for offer:', activeOfferId);
250
+ preview(activeOfferId)
251
+ .then(() => {
252
+ if (isMounted) {
253
+ console.log('[useOffersQuery] Preview successful for offer:', activeOfferId);
254
+ lastPreviewedOfferRef.current = activeOfferId; // Mark as previewed on success
255
+ }
256
+ })
257
+ .catch(err => {
258
+ console.error('[useOffersQuery] Failed to auto-preview offer:', activeOfferId, err);
259
+ // Don't mark as previewed on error, to avoid infinite retry loop
260
+ })
261
+ .finally(() => {
262
+ if (isMounted) {
263
+ console.log('[useOffersQuery] Preview finished for offer:', activeOfferId);
264
+ setIsActiveSummaryLoading(false);
265
+ }
266
+ });
267
+ }, 50);
268
+ return () => {
269
+ console.log('[useOffersQuery] Cleaning up auto-preview effect for offer:', activeOfferId);
270
+ isMounted = false;
271
+ clearTimeout(timer);
272
+ setIsActiveSummaryLoading(false); // Reset loading on unmount/cleanup
273
+ };
274
+ }, [activeOfferId, skipPreview, isSessionInitialized]); // FIXED: Removed 'preview' from dependencies to prevent infinite loop
275
+ const activeSummary = useMemo(() => {
276
+ if (!activeOfferId)
277
+ return null;
278
+ // Return dynamic summary if available, otherwise fallback to static summary
279
+ return checkoutSessions[activeOfferId]?.orderSummary || offers.find(o => o.id === activeOfferId)?.summaries?.[0] || null;
280
+ }, [activeOfferId, checkoutSessions, offers]);
193
281
  const getAvailableVariants = useCallback((offerId, productId) => {
194
282
  const sessionState = checkoutSessions[offerId]; // This hook needs to react to state changes
195
283
  if (!sessionState?.orderSummary?.options?.[productId])
@@ -208,6 +296,9 @@ export function useOffersQuery(options = {}) {
208
296
  return checkoutSessions[offerId]?.loadingVariants?.[productId] ?? false;
209
297
  }, [checkoutSessions]);
210
298
  const selectVariant = useCallback(async (offerId, productId, variantId) => {
299
+ if (!isSessionInitialized) {
300
+ throw new Error('Cannot select variant: CMS session is not initialized');
301
+ }
211
302
  // Use ref for initial check to avoid dependency but we might need latest state for logic
212
303
  // Actually for actions it's better to use ref or just dependency if action is not called in useEffect
213
304
  const currentSessions = checkoutSessionsRef.current;
@@ -286,12 +377,14 @@ export function useOffersQuery(options = {}) {
286
377
  }
287
378
  }));
288
379
  }
289
- }, [offersResource, fetchOrderSummary]); // Removed checkoutSessions dependency
290
- return {
380
+ }, [offersResource, fetchOrderSummary, isSessionInitialized]); // Removed checkoutSessions dependency
381
+ const result = {
291
382
  // Query data
292
383
  offers,
293
384
  isLoading,
294
385
  error,
386
+ activeSummary,
387
+ isActiveSummaryLoading,
295
388
  // Actions
296
389
  payOffer,
297
390
  preview,
@@ -299,4 +392,13 @@ export function useOffersQuery(options = {}) {
299
392
  selectVariant,
300
393
  isLoadingVariants,
301
394
  };
395
+ console.log('[useOffersQuery] Returning result:', {
396
+ offersCount: offers.length,
397
+ isLoading,
398
+ hasError: !!error,
399
+ activeOfferId,
400
+ hasActiveSummary: !!activeSummary,
401
+ isActiveSummaryLoading,
402
+ });
403
+ return result;
302
404
  }
@@ -5,6 +5,7 @@
5
5
  import { Product, ProductVariant } from '../../core/resources/products';
6
6
  export interface UseProductsQueryOptions {
7
7
  productIds?: string[];
8
+ variantIds?: string[];
8
9
  enabled?: boolean;
9
10
  includeVariants?: boolean;
10
11
  includePrices?: boolean;
@@ -8,7 +8,7 @@ import { ProductsResource } from '../../core/resources/products';
8
8
  import { getGlobalApiClient } from './useApiQuery';
9
9
  import { usePluginConfig } from './usePluginConfig';
10
10
  export function useProductsQuery(options = {}) {
11
- const { productIds = [], enabled = true, includeVariants = true, includePrices = true } = options;
11
+ const { productIds = [], variantIds = [], enabled = true, includeVariants = true, includePrices = true } = options;
12
12
  const { storeId } = usePluginConfig();
13
13
  // Create products resource client
14
14
  const productsResource = useMemo(() => {
@@ -20,7 +20,7 @@ export function useProductsQuery(options = {}) {
20
20
  }
21
21
  }, []);
22
22
  // Create query key based on options
23
- const queryKey = useMemo(() => ['products', { storeId, productIds, includeVariants, includePrices }], [storeId, productIds, includeVariants, includePrices]);
23
+ const queryKey = useMemo(() => ['products', { storeId, productIds, variantIds, includeVariants, includePrices }], [storeId, productIds, variantIds, includeVariants, includePrices]);
24
24
  // Use TanStack Query for fetching products
25
25
  const { data: products = [], isLoading, error, refetch } = useQuery({
26
26
  queryKey,
@@ -29,18 +29,22 @@ export function useProductsQuery(options = {}) {
29
29
  if (!storeId) {
30
30
  throw new Error('Store ID not found. Make sure the TagadaProvider is properly configured.');
31
31
  }
32
- const options = {
32
+ const baseOptions = {
33
33
  storeId,
34
34
  includeVariants,
35
35
  includePrices,
36
36
  };
37
- if (productIds.length > 0) {
37
+ if (variantIds.length > 0) {
38
+ // Fetch products by variant IDs (most efficient)
39
+ return await productsResource.getProductsByVariantIds(variantIds, baseOptions);
40
+ }
41
+ else if (productIds.length > 0) {
38
42
  // Fetch specific products
39
- return await productsResource.getProductsByIds(productIds, options);
43
+ return await productsResource.getProductsByIds(productIds, baseOptions);
40
44
  }
41
45
  else {
42
46
  // Fetch all products for the store
43
- return await productsResource.getProducts(options);
47
+ return await productsResource.getProducts(baseOptions);
44
48
  }
45
49
  },
46
50
  staleTime: 5 * 60 * 1000, // 5 minutes
@@ -25,7 +25,7 @@ export { queryKeys, useApiMutation, useApiQuery, useInvalidateQuery, usePreloadQ
25
25
  export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
26
26
  export { useCurrency } from './hooks/useCurrency';
27
27
  export { useDiscountsQuery as useDiscounts } from './hooks/useDiscountsQuery';
28
- export { useOffersQuery as useOffers } from './hooks/useOffersQuery';
28
+ export { useOfferQuery as useOffer } from './hooks/useOfferQuery';
29
29
  export { useOrderBumpQuery as useOrderBump } from './hooks/useOrderBumpQuery';
30
30
  export { useOrderQuery as useOrder } from './hooks/useOrderQuery';
31
31
  export { usePaymentQuery as usePayment } from './hooks/usePaymentQuery';
@@ -38,7 +38,8 @@ export { useThreeds } from './hooks/useThreeds';
38
38
  export { useThreedsModal } from './hooks/useThreedsModal';
39
39
  export { useTranslation } from './hooks/useTranslation';
40
40
  export { useVipOffersQuery as useVipOffers } from './hooks/useVipOffersQuery';
41
- export { useFunnel, useSimpleFunnel } from './hooks/useFunnel';
41
+ export { useFunnel } from './hooks/useFunnel';
42
+ export { useFunnel as useFunnelLegacy, useSimpleFunnel } from './hooks/useFunnelLegacy';
42
43
  export type { UseCheckoutTokenOptions, UseCheckoutTokenResult } from './hooks/useCheckoutToken';
43
44
  export type { ClubOffer, ClubOfferItem, ClubOfferLineItem, ClubOfferSummary, UseClubOffersOptions, UseClubOffersResult } from './hooks/useClubOffers';
44
45
  export type { UseCreditsOptions, UseCreditsResult } from './hooks/useCredits';
@@ -58,8 +59,10 @@ export type { TranslateFunction, UseTranslationOptions, UseTranslationResult } f
58
59
  export { FunnelActionType } from '../core/resources/funnel';
59
60
  export type { UseCheckoutQueryOptions as UseCheckoutOptions, UseCheckoutQueryResult as UseCheckoutResult } from './hooks/useCheckoutQuery';
60
61
  export type { UseDiscountsQueryOptions as UseDiscountsOptions, UseDiscountsQueryResult as UseDiscountsResult } from './hooks/useDiscountsQuery';
61
- export type { FunnelEvent, FunnelNavigationAction, FunnelNavigationResult, SimpleFunnelContext, UseFunnelOptions, UseFunnelResult } from './hooks/useFunnel';
62
- export type { UseOffersQueryOptions as UseOffersOptions, UseOffersQueryResult as UseOffersResult } from './hooks/useOffersQuery';
62
+ export type { FunnelAction, FunnelNavigationAction, FunnelNavigationResult, SimpleFunnelContext } from '../core/resources/funnel';
63
+ export type { FunnelContextValue } from './hooks/useFunnel';
64
+ export type { UseFunnelOptions as UseFunnelLegacyOptions, UseFunnelResult as UseFunnelLegacyResult } from './hooks/useFunnelLegacy';
65
+ export type { AvailableVariant, LineItemSelection, OfferLineItem, OfferPreviewSummary, UseOfferQueryOptions as UseOfferOptions, UseOfferQueryResult as UseOfferResult } from './hooks/useOfferQuery';
63
66
  export type { UseOrderBumpQueryOptions as UseOrderBumpOptions, UseOrderBumpQueryResult as UseOrderBumpResult } from './hooks/useOrderBumpQuery';
64
67
  export type { UseOrderQueryOptions as UseOrderOptions, UseOrderQueryResult as UseOrderResult } from './hooks/useOrderQuery';
65
68
  export type { PaymentHook as UsePaymentResult } from './hooks/usePaymentQuery';
@@ -29,7 +29,7 @@ export { queryKeys, useApiMutation, useApiQuery, useInvalidateQuery, usePreloadQ
29
29
  export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
30
30
  export { useCurrency } from './hooks/useCurrency';
31
31
  export { useDiscountsQuery as useDiscounts } from './hooks/useDiscountsQuery';
32
- export { useOffersQuery as useOffers } from './hooks/useOffersQuery';
32
+ export { useOfferQuery as useOffer } from './hooks/useOfferQuery';
33
33
  export { useOrderBumpQuery as useOrderBump } from './hooks/useOrderBumpQuery';
34
34
  export { useOrderQuery as useOrder } from './hooks/useOrderQuery';
35
35
  export { usePaymentQuery as usePayment } from './hooks/usePaymentQuery';
@@ -43,7 +43,9 @@ export { useThreedsModal } from './hooks/useThreedsModal';
43
43
  export { useTranslation } from './hooks/useTranslation';
44
44
  export { useVipOffersQuery as useVipOffers } from './hooks/useVipOffersQuery';
45
45
  // Funnel hooks
46
- export { useFunnel, useSimpleFunnel } from './hooks/useFunnel';
46
+ export { useFunnel } from './hooks/useFunnel';
47
+ // Legacy funnel hooks (deprecated - use TagadaProvider + useFunnel instead)
48
+ export { useFunnel as useFunnelLegacy, useSimpleFunnel } from './hooks/useFunnelLegacy';
47
49
  // TanStack Query types
48
50
  export { FunnelActionType } from '../core/resources/funnel';
49
51
  // Re-export utilities from main react
@@ -1,11 +1,27 @@
1
1
  import { ReactNode } from 'react';
2
- import { TagadaState } from '../../core/client';
2
+ import { TagadaClient, TagadaState, TagadaClientConfig } from '../../core/client';
3
3
  import { RawPluginConfig } from '../../core/utils/pluginConfig';
4
4
  import { Environment, EnvironmentConfig } from '../../core/types';
5
5
  import { ApiService } from '../../../react/services/apiService';
6
6
  import { formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits, convertCurrency } from '../../../react/utils/money';
7
+ import { FunnelAction, FunnelNavigationResult, SimpleFunnelContext } from '../../core/resources/funnel';
8
+ import { FunnelState } from '../../core/funnelClient';
7
9
  interface TagadaContextValue extends TagadaState {
10
+ client: TagadaClient;
8
11
  apiService: ApiService;
12
+ pendingRedirect: boolean;
13
+ funnel: FunnelState & {
14
+ currentStep: {
15
+ id: string;
16
+ } | null;
17
+ next: (event: FunnelAction) => Promise<FunnelNavigationResult>;
18
+ goToStep: (stepId: string) => Promise<FunnelNavigationResult>;
19
+ updateContext: (updates: Partial<SimpleFunnelContext>) => Promise<void>;
20
+ initializeSession: (entryStepId?: string) => Promise<void>;
21
+ endSession: () => Promise<void>;
22
+ retryInitialization: () => Promise<void>;
23
+ refetch: () => Promise<void>;
24
+ };
9
25
  debugCheckout: {
10
26
  isActive: boolean;
11
27
  data: any;
@@ -42,14 +58,41 @@ interface TagadaContextValue extends TagadaState {
42
58
  }
43
59
  interface TagadaProviderProps {
44
60
  children: ReactNode;
61
+ /**
62
+ * Optional environment override.
63
+ * ⚠️ Leave undefined for automatic runtime detection (recommended).
64
+ * Only set this if you need to force a specific environment for testing.
65
+ */
45
66
  environment?: Environment;
46
67
  customApiConfig?: Partial<EnvironmentConfig>;
47
68
  debugMode?: boolean;
48
69
  localConfig?: string;
49
70
  blockUntilSessionReady?: boolean;
50
71
  rawPluginConfig?: RawPluginConfig;
72
+ /**
73
+ * Optional feature flags to enable/disable subsystems (e.g. funnel).
74
+ * By default all features are enabled.
75
+ */
76
+ features?: TagadaClientConfig['features'];
77
+ /**
78
+ * Funnel ID to initialize. If not provided, will be detected from URL query param.
79
+ */
80
+ funnelId?: string;
81
+ /**
82
+ * Auto-initialize funnel session when auth and store are ready.
83
+ * Default: true
84
+ */
85
+ autoInitializeFunnel?: boolean;
86
+ /**
87
+ * Callback fired after funnel navigation
88
+ */
89
+ onNavigate?: (result: FunnelNavigationResult) => void | boolean;
90
+ /**
91
+ * Callback fired on funnel errors
92
+ */
93
+ onFunnelError?: (error: Error) => void;
51
94
  }
52
95
  export declare function TagadaProvider({ children, environment, customApiConfig, // Ignored for now in TagadaClient, or need to add support
53
- debugMode, localConfig, blockUntilSessionReady, rawPluginConfig, }: TagadaProviderProps): import("react/jsx-runtime").JSX.Element;
96
+ debugMode, localConfig, blockUntilSessionReady, rawPluginConfig, features, funnelId, autoInitializeFunnel, onNavigate, onFunnelError, }: TagadaProviderProps): import("react/jsx-runtime").JSX.Element;
54
97
  export declare function useTagadaContext(): TagadaContextValue;
55
98
  export {};
@@ -44,18 +44,27 @@ const InitializationLoader = () => (_jsxs("div", { style: {
44
44
  ` })] }));
45
45
  const TagadaContext = createContext(null);
46
46
  export function TagadaProvider({ children, environment, customApiConfig, // Ignored for now in TagadaClient, or need to add support
47
- debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, }) {
47
+ debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, features, funnelId, autoInitializeFunnel = true, onNavigate, onFunnelError, }) {
48
+ // Auto-detect debug mode from environment if not explicitly set
49
+ const effectiveDebugMode = debugMode !== undefined
50
+ ? debugMode
51
+ : (typeof import.meta !== 'undefined' && import.meta.env?.DEV) ||
52
+ (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development');
48
53
  // Initialize client
49
54
  const client = useMemo(() => {
50
55
  const config = {
51
56
  environment,
52
- debugMode,
57
+ debugMode: effectiveDebugMode,
53
58
  localConfig,
54
59
  rawPluginConfig,
55
60
  blockUntilSessionReady,
61
+ // For now we always enable funnel by default; callers can disable via features if needed
62
+ features: features ?? {
63
+ funnel: true,
64
+ },
56
65
  };
57
66
  return new TagadaClient(config);
58
- }, []); // Singleton behavior
67
+ }, []); // Singleton behavior on first render
59
68
  // State Sync
60
69
  const [state, setState] = useState(client.getState());
61
70
  useEffect(() => {
@@ -82,6 +91,32 @@ debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, }) {
82
91
  apiService.updateToken(state.token);
83
92
  apiService.updateConfig(state.environment);
84
93
  }, [state.token, state.environment, apiService]);
94
+ // Funnel State Management
95
+ const [funnelState, setFunnelState] = useState(() => client.funnel
96
+ ? client.funnel.getState()
97
+ : {
98
+ context: null,
99
+ isLoading: false,
100
+ isInitialized: false,
101
+ isNavigating: false,
102
+ error: null,
103
+ sessionError: null,
104
+ });
105
+ // Track pending redirect to prevent renders during navigation
106
+ const [pendingRedirect, setPendingRedirect] = useState(false);
107
+ // Subscribe to funnel state changes
108
+ useEffect(() => {
109
+ if (!client.funnel)
110
+ return;
111
+ // Keep funnel client in sync with latest config/environment
112
+ client.funnel.setConfig({
113
+ pluginConfig: state.pluginConfig,
114
+ environment: state.environment,
115
+ });
116
+ return client.funnel.subscribe(setFunnelState);
117
+ }, [client, state.pluginConfig, state.environment]);
118
+ // Note: Funnel auto-initialization is now handled by TagadaClient itself
119
+ // when the session is initialized. This happens automatically in the core layer.
85
120
  // Debug State (React specific)
86
121
  const [debugCheckout, setDebugCheckout] = useState({
87
122
  isActive: false,
@@ -160,9 +195,87 @@ debugMode, localConfig, blockUntilSessionReady = false, rawPluginConfig, }) {
160
195
  formatSimpleMoney,
161
196
  }), []);
162
197
  const [isDebugDrawerOpen, setIsDebugDrawerOpen] = useState(false);
198
+ // Funnel Methods
199
+ const funnelMethods = useMemo(() => {
200
+ if (!client.funnel) {
201
+ // Return no-op methods if funnel is disabled
202
+ const noopAsync = async () => {
203
+ throw new Error('Funnel feature is disabled');
204
+ };
205
+ return {
206
+ next: noopAsync,
207
+ goToStep: noopAsync,
208
+ updateContext: noopAsync,
209
+ initializeSession: noopAsync,
210
+ endSession: noopAsync,
211
+ retryInitialization: noopAsync,
212
+ refetch: noopAsync,
213
+ };
214
+ }
215
+ const next = async (event) => {
216
+ const result = await client.funnel.navigate(event);
217
+ // Call custom onNavigate handler if provided
218
+ let shouldAutoRedirect = true;
219
+ if (onNavigate) {
220
+ const handlerResult = onNavigate(result);
221
+ // If handler explicitly returns false, skip auto-redirect
222
+ if (handlerResult === false) {
223
+ shouldAutoRedirect = false;
224
+ }
225
+ }
226
+ // Default behavior: auto-redirect if result has a URL
227
+ if (shouldAutoRedirect && result?.url && typeof window !== 'undefined') {
228
+ console.log('🚀 [TagadaProvider] Auto-redirecting to:', result.url);
229
+ // Set pending redirect flag BEFORE navigation to prevent renders
230
+ setPendingRedirect(true);
231
+ window.location.href = result.url;
232
+ }
233
+ return result;
234
+ };
235
+ const goToStep = async (stepId) => {
236
+ return await next({
237
+ type: 'DIRECT_NAVIGATION',
238
+ data: { targetStepId: stepId },
239
+ });
240
+ };
241
+ // Helper to resolve accountId with fallbacks
242
+ const getAccountId = () => state.store?.accountId || state.pluginConfig?.accountId || state.session?.accountId || '';
243
+ return {
244
+ next,
245
+ goToStep,
246
+ updateContext: (updates) => client.funnel.updateContext(updates),
247
+ initializeSession: async (entryStepId) => {
248
+ const accountId = getAccountId();
249
+ if (!accountId || !state.auth.session || !state.store) {
250
+ throw new Error('Cannot initialize funnel: missing required data (accountId, session, or store)');
251
+ }
252
+ await client.funnel.initialize({ customerId: state.auth.session.customerId, sessionId: state.auth.session.sessionId }, { id: state.store.id, accountId }, funnelId, entryStepId);
253
+ },
254
+ endSession: () => client.funnel.endSession(),
255
+ retryInitialization: () => {
256
+ const accountId = getAccountId();
257
+ if (!accountId || !state.auth.session || !state.store) {
258
+ return Promise.reject(new Error('Cannot retry initialization: missing required data'));
259
+ }
260
+ return client
261
+ .funnel.autoInitialize({ customerId: state.auth.session.customerId, sessionId: state.auth.session.sessionId }, { id: state.store.id, accountId }, funnelId)
262
+ .then(() => { });
263
+ },
264
+ refetch: async () => {
265
+ await client.funnel.refreshSession();
266
+ },
267
+ };
268
+ }, [client, state.auth.session, state.store, funnelId, onNavigate]);
163
269
  const contextValue = {
270
+ client,
164
271
  ...state,
165
272
  apiService,
273
+ pendingRedirect,
274
+ funnel: {
275
+ ...funnelState,
276
+ currentStep: funnelState.context?.currentStepId ? { id: funnelState.context.currentStepId } : null,
277
+ ...funnelMethods,
278
+ },
166
279
  debugCheckout,
167
280
  updateCheckoutDebugData,
168
281
  debugFunnel,
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Standalone Adapter for TagadaPay SDK v2
3
+ *
4
+ * Provides a vanilla JS interface for using the SDK without a framework.
5
+ * Suitable for standard HTML/JS sites or other frameworks (Svelte, Angular, etc).
6
+ *
7
+ * This layer is a very thin wrapper over the Core SDK.
8
+ */
9
+ import { TagadaClient, TagadaClientConfig, TagadaState } from '../core/client';
10
+ import { ApiClient } from '../core/resources/apiClient';
11
+ /**
12
+ * Factory function to create a Tagada Client instance.
13
+ * Features (like funnel) can be toggled via the config.
14
+ */
15
+ export declare function createTagadaClient(config?: TagadaClientConfig): TagadaClient;
16
+ export { TagadaClient, ApiClient };
17
+ export type { TagadaClientConfig, TagadaState };
18
+ export { FunnelActionType } from '../core/resources/funnel';
19
+ export type { FunnelAction, FunnelNavigationResult, SimpleFunnelContext } from '../core/resources/funnel';
20
+ export * from '../core/utils';
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Standalone Adapter for TagadaPay SDK v2
3
+ *
4
+ * Provides a vanilla JS interface for using the SDK without a framework.
5
+ * Suitable for standard HTML/JS sites or other frameworks (Svelte, Angular, etc).
6
+ *
7
+ * This layer is a very thin wrapper over the Core SDK.
8
+ */
9
+ import { TagadaClient } from '../core/client';
10
+ import { ApiClient } from '../core/resources/apiClient';
11
+ /**
12
+ * Factory function to create a Tagada Client instance.
13
+ * Features (like funnel) can be toggled via the config.
14
+ */
15
+ export function createTagadaClient(config = {}) {
16
+ return new TagadaClient(config);
17
+ }
18
+ // Re-export Core Classes
19
+ export { TagadaClient, ApiClient };
20
+ export { FunnelActionType } from '../core/resources/funnel';
21
+ // Re-export Utilities
22
+ export * from '../core/utils';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Vue.js Adapter for TagadaPay SDK
3
+ *
4
+ * Provides Vue composables that wrap the Core SDK logic.
5
+ * Uses Vue's reactivity system to expose state from TagadaClient and FunnelClient.
6
+ */
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ /**
3
+ * Vue.js Adapter for TagadaPay SDK
4
+ *
5
+ * Provides Vue composables that wrap the Core SDK logic.
6
+ * Uses Vue's reactivity system to expose state from TagadaClient and FunnelClient.
7
+ */
8
+ // TODO: Implement Vue adapter
9
+ // export function useTagada() { ... }
10
+ // export function useFunnel() { ... }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagadapay/plugin-sdk",
3
- "version": "2.8.9",
3
+ "version": "3.0.1",
4
4
  "description": "Modern React SDK for building Tagada Pay plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,6 +19,11 @@
19
19
  "types": "./dist/v2/index.d.ts",
20
20
  "import": "./dist/v2/index.js",
21
21
  "require": "./dist/v2/index.js"
22
+ },
23
+ "./v2/standalone": {
24
+ "types": "./dist/v2/standalone/index.d.ts",
25
+ "import": "./dist/v2/standalone/index.js",
26
+ "require": "./dist/v2/standalone/index.js"
22
27
  }
23
28
  },
24
29
  "scripts": {