@tagadapay/plugin-sdk 2.7.14 → 2.7.18

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.
@@ -2,6 +2,7 @@
2
2
  * Plugin Configuration Utility Functions
3
3
  * Pure functions for plugin configuration management
4
4
  */
5
+ import { resolveEnvValue } from './env';
5
6
  /**
6
7
  * Load local development configuration
7
8
  */
@@ -138,7 +139,6 @@ const loadProductionConfig = async () => {
138
139
  export const loadPluginConfig = async (configVariant = 'default', rawConfig) => {
139
140
  // If raw config is provided, use it directly
140
141
  if (rawConfig) {
141
- console.log('🛠️ Using raw plugin config:', rawConfig);
142
142
  return {
143
143
  storeId: rawConfig.storeId,
144
144
  accountId: rawConfig.accountId,
@@ -146,6 +146,12 @@ export const loadPluginConfig = async (configVariant = 'default', rawConfig) =>
146
146
  config: rawConfig.config ?? {},
147
147
  };
148
148
  }
149
+ else {
150
+ const rawConfig = await createRawPluginConfig();
151
+ if (rawConfig) {
152
+ return rawConfig;
153
+ }
154
+ }
149
155
  // Try local development config
150
156
  const localConfig = await loadLocalDevConfig(configVariant);
151
157
  if (localConfig) {
@@ -202,33 +208,17 @@ export async function loadLocalConfig(configName = 'default', defaultConfig) {
202
208
  * @param options - Configuration options including storeId, accountId, basePath, configName, or a direct config object
203
209
  * @returns A RawPluginConfig object or undefined if required fields are missing
204
210
  */
205
- export async function createRawPluginConfig(options) {
211
+ export async function createRawPluginConfig() {
206
212
  try {
207
- const resolveEnv = (key) => {
208
- const prefixes = ['', 'VITE_', 'REACT_APP_', 'NEXT_PUBLIC_'];
209
- for (const prefix of prefixes) {
210
- const envKey = prefix ? `${prefix}${key}` : key;
211
- if (typeof process !== 'undefined' && process?.env?.[envKey]) {
212
- return process.env[envKey];
213
- }
214
- if (typeof import.meta !== 'undefined' && import.meta?.env?.[envKey]) {
215
- return import.meta.env[envKey];
216
- }
217
- if (typeof window !== 'undefined' && window?.__TAGADA_ENV__?.[envKey]) {
218
- return window.__TAGADA_ENV__[envKey];
219
- }
220
- }
221
- return undefined;
222
- };
223
- const storeId = resolveEnv('TAGADA_STORE_ID');
224
- const accountId = resolveEnv('TAGADA_ACCOUNT_ID');
225
- const basePath = resolveEnv('TAGADA_BASE_PATH');
226
- const configName = resolveEnv('TAGADA_CONFIG_NAME');
227
- if (!storeId) {
213
+ const storeId = resolveEnvValue('TAGADA_STORE_ID');
214
+ const accountId = resolveEnvValue('TAGADA_ACCOUNT_ID');
215
+ const basePath = resolveEnvValue('TAGADA_BASE_PATH');
216
+ const configName = resolveEnvValue('TAGADA_CONFIG_NAME');
217
+ if (!storeId || !accountId) {
228
218
  console.warn('[createRawPluginConfig] No storeId provided');
229
219
  return undefined;
230
220
  }
231
- const resolvedConfig = await loadLocalConfig(configName, options?.config);
221
+ const resolvedConfig = await loadLocalConfig(configName);
232
222
  if (!resolvedConfig) {
233
223
  console.warn('[createRawPluginConfig] No config found');
234
224
  return undefined;
@@ -22,7 +22,7 @@ export type { ApplyDiscountResponse, Discount, DiscountCodeValidation, RemoveDis
22
22
  export type { ToggleOrderBumpResponse, VipOffer, VipPreviewResponse } from './core/resources/vipOffers';
23
23
  export type { StoreConfig } from './core/resources/storeConfig';
24
24
  export type { FunnelContextUpdateRequest, FunnelContextUpdateResponse, FunnelEvent, FunnelInitializeRequest, FunnelInitializeResponse, FunnelNavigateRequest, FunnelNavigateResponse, FunnelNavigationAction, FunnelNavigationResult, SimpleFunnelContext } from './core/resources/funnel';
25
- export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffers, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, useProducts, usePromotions, useRegionOptions, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
25
+ export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffers, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
26
26
  export type { TranslateFunction, TranslationText, UseTranslationOptions, UseTranslationResult } from './react/hooks/useTranslation';
27
27
  export type { ClubOffer, ClubOfferItem, ClubOfferLineItem, ClubOfferSummary, UseClubOffersOptions, UseClubOffersResult } from './react/hooks/useClubOffers';
28
28
  export type { UseLoginOptions, UseLoginResult } from './react/hooks/useLogin';
package/dist/v2/index.js CHANGED
@@ -14,4 +14,4 @@ export * from './core/utils/products';
14
14
  // Path remapping helpers (framework-agnostic)
15
15
  export * from './core/pathRemapping';
16
16
  // React exports (hooks and components only, types are exported above)
17
- export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffers, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, useProducts, usePromotions, useRegionOptions, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
17
+ export { ApplePayButton, ExpressPaymentMethodsProvider, formatMoney, getAvailableLanguages, GooglePayButton, queryKeys, TagadaProvider, useApiMutation, useApiQuery, useAuth, useCheckout, useCheckoutToken, useClubOffers, useCountryOptions, useCurrency, useCustomer, useCustomerInfos, useCustomerOrders, useCustomerSubscriptions, useDiscounts, useExpressPaymentMethods, useFunnel, useGeoLocation, useGoogleAutocomplete, useInvalidateQuery, useISOData, useLanguageImport, useLogin, useOffers, useOrder, useOrderBump, usePayment, usePluginConfig, usePostPurchases, usePreloadQuery, useProducts, usePromotions, useRegionOptions, useRemappableParams, useShippingRates, useSimpleFunnel, useStoreConfig, useTagadaContext, useThreeds, useThreedsModal, useTranslation, useVipOffers } from './react';
@@ -0,0 +1,19 @@
1
+ /**
2
+ * useApiClient Hook - Provides access to the authenticated API client
3
+ *
4
+ * This hook returns the ApiClient instance from the TagadaProvider context.
5
+ * The client is guaranteed to have the latest authentication token.
6
+ */
7
+ import type { ApiService } from '../../../react/services/apiService';
8
+ /**
9
+ * Hook to get the authenticated API service from context
10
+ *
11
+ * @returns The ApiService instance with the current authentication token
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const apiService = useApiClient();
16
+ * const response = await apiService.get('/api/endpoint');
17
+ * ```
18
+ */
19
+ export declare function useApiClient(): ApiService;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * useApiClient Hook - Provides access to the authenticated API client
3
+ *
4
+ * This hook returns the ApiClient instance from the TagadaProvider context.
5
+ * The client is guaranteed to have the latest authentication token.
6
+ */
7
+ import { useTagadaContext } from '../providers/TagadaProvider';
8
+ /**
9
+ * Hook to get the authenticated API service from context
10
+ *
11
+ * @returns The ApiService instance with the current authentication token
12
+ *
13
+ * @example
14
+ * ```typescript
15
+ * const apiService = useApiClient();
16
+ * const response = await apiService.get('/api/endpoint');
17
+ * ```
18
+ */
19
+ export function useApiClient() {
20
+ const { apiService } = useTagadaContext();
21
+ return apiService;
22
+ }
@@ -33,7 +33,6 @@ export function useCredits(options = {}) {
33
33
  if (!customerId || !storeId) {
34
34
  return null;
35
35
  }
36
- console.log('customerId', customerId, 'storeId', storeId);
37
36
  return await creditsResource.getCustomerCredits(customerId, storeId);
38
37
  },
39
38
  staleTime: 30 * 1000, // 30 seconds - fresher than most resources
@@ -42,8 +42,13 @@ export function useFunnel(options) {
42
42
  console.warn('🍪 Funnel: No session ID available for query');
43
43
  return null;
44
44
  }
45
+ // ✅ Automatically include currentUrl for session sync on page load/refresh
46
+ const currentUrl = typeof window !== 'undefined' ? window.location.href : undefined;
45
47
  console.log(`🍪 Funnel: Fetching session data for ID: ${context.sessionId}`);
46
- const response = await funnelResource.getSession(context.sessionId);
48
+ if (currentUrl) {
49
+ console.log(`🍪 Funnel: Including current URL for sync: ${currentUrl}`);
50
+ }
51
+ const response = await funnelResource.getSession(context.sessionId, currentUrl);
47
52
  console.log(`🍪 Funnel: Session fetch response:`, response);
48
53
  if (response.success && response.context) {
49
54
  return response.context;
@@ -133,17 +138,26 @@ export function useFunnel(options) {
133
138
  if (!context.sessionId) {
134
139
  throw new Error('Funnel session ID missing - session may be corrupted');
135
140
  }
141
+ // ✅ Automatically include currentUrl for URL-to-Step mapping
142
+ // User can override by providing event.currentUrl explicitly
143
+ const currentUrl = event.currentUrl || (typeof window !== 'undefined' ? window.location.href : undefined);
136
144
  console.log(`🍪 Funnel: Navigating with session ID: ${context.sessionId}`);
145
+ if (currentUrl) {
146
+ console.log(`🍪 Funnel: Current URL for sync: ${currentUrl}`);
147
+ }
137
148
  const requestBody = {
138
149
  sessionId: context.sessionId,
139
150
  event: {
140
151
  type: event.type,
141
152
  data: event.data,
142
- timestamp: event.timestamp || new Date().toISOString()
153
+ timestamp: event.timestamp || new Date().toISOString(),
154
+ currentUrl: event.currentUrl // Preserve user override if provided
143
155
  },
144
156
  contextUpdates: {
145
157
  lastActivityAt: Date.now()
146
- }
158
+ },
159
+ currentUrl, // ✅ Send to backend for URL→Step mapping
160
+ funnelId: effectiveFunnelId || options.funnelId // ✅ Send for session recovery
147
161
  };
148
162
  const response = await funnelResource.navigate(requestBody);
149
163
  if (response.success && response.result) {
@@ -156,19 +170,32 @@ export function useFunnel(options) {
156
170
  onSuccess: (result) => {
157
171
  if (!context)
158
172
  return;
173
+ // 🔄 Handle session recovery (if backend created a new session)
174
+ let recoveredSessionId;
175
+ if (result.sessionId && result.sessionId !== context.sessionId) {
176
+ console.warn(`🔄 Funnel: Session recovered! Old: ${context.sessionId}, New: ${result.sessionId}`);
177
+ recoveredSessionId = result.sessionId;
178
+ }
159
179
  // Update local context
160
180
  const newContext = {
161
181
  ...context,
182
+ sessionId: recoveredSessionId || context.sessionId, // ✅ Use recovered session ID if provided
162
183
  currentStepId: result.stepId,
163
184
  previousStepId: context.currentStepId,
164
185
  lastActivityAt: Date.now(),
165
186
  metadata: {
166
187
  ...context.metadata,
167
188
  lastEvent: 'navigation',
168
- lastTransition: new Date().toISOString()
189
+ lastTransition: new Date().toISOString(),
190
+ ...(recoveredSessionId ? { recovered: true, oldSessionId: context.sessionId } : {})
169
191
  }
170
192
  };
171
193
  setContext(newContext);
194
+ // Update cookie with new session ID if recovered
195
+ if (recoveredSessionId) {
196
+ document.cookie = `funnelSessionId=${recoveredSessionId}; path=/; max-age=86400; SameSite=Lax`;
197
+ console.log(`🍪 Funnel: Updated cookie with recovered session ID: ${recoveredSessionId}`);
198
+ }
172
199
  // Create typed navigation result
173
200
  const navigationResult = {
174
201
  stepId: result.stepId,
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Hook to extract URL parameters that works with both remapped and non-remapped paths
3
+ *
4
+ * This hook automatically detects if the current path is remapped and extracts
5
+ * parameters correctly in both cases, so your components don't need to know
6
+ * about path remapping at all.
7
+ *
8
+ * @param internalPath - The internal path pattern (e.g., "/hello-with-param/:myparam")
9
+ * @returns Parameters object with extracted values
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * function HelloWithParamPage() {
14
+ * // Works for both /hello-with-param/test AND /myremap/test
15
+ * const { myparam } = useRemappableParams<{ myparam: string }>('/hello-with-param/:myparam');
16
+ *
17
+ * return <div>Parameter: {myparam}</div>;
18
+ * }
19
+ * ```
20
+ */
21
+ export declare function useRemappableParams<T extends Record<string, string>>(internalPath: string): Partial<T>;
@@ -0,0 +1,104 @@
1
+ import { useParams } from 'react-router-dom';
2
+ import { getPathInfo } from '../../core/pathRemapping';
3
+ import { match } from 'path-to-regexp';
4
+ /**
5
+ * Hook to extract URL parameters that works with both remapped and non-remapped paths
6
+ *
7
+ * This hook automatically detects if the current path is remapped and extracts
8
+ * parameters correctly in both cases, so your components don't need to know
9
+ * about path remapping at all.
10
+ *
11
+ * @param internalPath - The internal path pattern (e.g., "/hello-with-param/:myparam")
12
+ * @returns Parameters object with extracted values
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * function HelloWithParamPage() {
17
+ * // Works for both /hello-with-param/test AND /myremap/test
18
+ * const { myparam } = useRemappableParams<{ myparam: string }>('/hello-with-param/:myparam');
19
+ *
20
+ * return <div>Parameter: {myparam}</div>;
21
+ * }
22
+ * ```
23
+ */
24
+ export function useRemappableParams(internalPath) {
25
+ const routerParams = useParams();
26
+ const pathInfo = getPathInfo();
27
+ // If not remapped, just return router params
28
+ if (!pathInfo.isRemapped) {
29
+ return routerParams;
30
+ }
31
+ // If remapped, extract params from the external URL
32
+ try {
33
+ // Get the external pattern from localStorage (for testing) or from meta tag
34
+ let externalPattern = null;
35
+ // Check localStorage for explicit remapping (development/testing)
36
+ if (typeof localStorage !== 'undefined') {
37
+ try {
38
+ const remapData = localStorage.getItem('tagadapay-remap');
39
+ if (remapData) {
40
+ const parsed = JSON.parse(remapData);
41
+ if (parsed.externalPath && parsed.internalPath === internalPath) {
42
+ externalPattern = parsed.externalPath;
43
+ }
44
+ }
45
+ }
46
+ catch (e) {
47
+ // Ignore parsing errors
48
+ }
49
+ }
50
+ // Check meta tag for production remapping
51
+ if (!externalPattern && typeof document !== 'undefined') {
52
+ const metaTag = document.querySelector('meta[name="tagadapay-path-remap-pattern"]');
53
+ if (metaTag) {
54
+ externalPattern = metaTag.getAttribute('content');
55
+ }
56
+ }
57
+ // If we have an external pattern, extract params from it
58
+ if (externalPattern) {
59
+ const matchFn = match(externalPattern, { decode: decodeURIComponent });
60
+ const result = matchFn(pathInfo.externalPath);
61
+ if (result && typeof result !== 'boolean') {
62
+ // We have extracted params from external URL
63
+ // Now we need to map them to internal param names
64
+ // Extract param names from both patterns
65
+ const externalParamNames = extractParamNames(externalPattern);
66
+ const internalParamNames = extractParamNames(internalPath);
67
+ // Map external params to internal params (by position)
68
+ const mappedParams = {};
69
+ externalParamNames.forEach((externalName, index) => {
70
+ const internalName = internalParamNames[index];
71
+ if (internalName && result.params[externalName] !== undefined) {
72
+ mappedParams[internalName] = result.params[externalName];
73
+ }
74
+ });
75
+ return mappedParams;
76
+ }
77
+ }
78
+ // Fallback: try to extract from internal path directly
79
+ const matchFn = match(internalPath, { decode: decodeURIComponent });
80
+ const result = matchFn(pathInfo.externalPath);
81
+ if (result && typeof result !== 'boolean') {
82
+ return result.params;
83
+ }
84
+ }
85
+ catch (error) {
86
+ console.error('[useRemappableParams] Failed to extract params:', error);
87
+ }
88
+ // Fallback to router params
89
+ return routerParams;
90
+ }
91
+ /**
92
+ * Extract parameter names from a path pattern
93
+ * @param pattern - Path pattern like "/hello/:param1/:param2"
94
+ * @returns Array of parameter names like ["param1", "param2"]
95
+ */
96
+ function extractParamNames(pattern) {
97
+ const paramRegex = /:([^/]+)/g;
98
+ const matches = [];
99
+ let match;
100
+ while ((match = paramRegex.exec(pattern)) !== null) {
101
+ matches.push(match[1]);
102
+ }
103
+ return matches;
104
+ }
@@ -20,6 +20,7 @@ export { useGoogleAutocomplete } from './hooks/useGoogleAutocomplete';
20
20
  export { getAvailableLanguages, useCountryOptions, useISOData, useLanguageImport, useRegionOptions } from './hooks/useISOData';
21
21
  export { useLogin } from './hooks/useLogin';
22
22
  export { usePluginConfig } from './hooks/usePluginConfig';
23
+ export { useRemappableParams } from './hooks/useRemappableParams';
23
24
  export { queryKeys, useApiMutation, useApiQuery, useInvalidateQuery, usePreloadQuery } from './hooks/useApiQuery';
24
25
  export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
25
26
  export { useCurrency } from './hooks/useCurrency';
@@ -23,6 +23,7 @@ export { useGoogleAutocomplete } from './hooks/useGoogleAutocomplete';
23
23
  export { getAvailableLanguages, useCountryOptions, useISOData, useLanguageImport, useRegionOptions } from './hooks/useISOData';
24
24
  export { useLogin } from './hooks/useLogin';
25
25
  export { usePluginConfig } from './hooks/usePluginConfig';
26
+ export { useRemappableParams } from './hooks/useRemappableParams';
26
27
  // TanStack Query hooks (recommended)
27
28
  export { queryKeys, useApiMutation, useApiQuery, useInvalidateQuery, usePreloadQuery } from './hooks/useApiQuery';
28
29
  export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
@@ -12,6 +12,7 @@ import { decodeJWTClient, isTokenExpired } from '../../../react/utils/jwtDecoder
12
12
  import { convertCurrency, formatMoney, formatMoneyWithoutSymbol, formatSimpleMoney, getCurrencyInfo, minorUnitsToMajorUnits, moneyStringOrNumberToMinorUnits, } from '../../../react/utils/money';
13
13
  import { clearClientToken, getClientToken, setClientToken } from '../../../react/utils/tokenStorage';
14
14
  import { ApiClient } from '../../core/resources/apiClient';
15
+ import { resolveEnvValue } from '../../core/utils/env';
15
16
  import { loadPluginConfig } from '../../core/utils/pluginConfig';
16
17
  import { default as DebugDrawer } from '../components/DebugDrawer';
17
18
  import { setGlobalApiClient } from '../hooks/useApiQuery';
@@ -47,6 +48,15 @@ const InitializationLoader = () => (_jsxs("div", { style: {
47
48
  100% { transform: rotate(360deg); }
48
49
  }
49
50
  ` })] }));
51
+ const isEnvironment = (value) => value === 'production' || value === 'development' || value === 'local';
52
+ const resolveEnvironmentFromVariables = () => {
53
+ const envValue = resolveEnvValue('TAGADA_ENV') ?? resolveEnvValue('TAGADA_ENVIRONMENT');
54
+ if (!envValue) {
55
+ return undefined;
56
+ }
57
+ const normalized = envValue.trim().toLowerCase();
58
+ return isEnvironment(normalized) ? normalized : undefined;
59
+ };
50
60
  const TagadaContext = createContext(null);
51
61
  // Global instance tracking for TagadaProvider
52
62
  let globalTagadaInstance = null;
@@ -154,7 +164,6 @@ rawPluginConfig, }) {
154
164
  }, [configVariant, rawPluginConfig]);
155
165
  // Extract store/account IDs from plugin config (only source now)
156
166
  const storeId = pluginConfig.storeId;
157
- const _accountId = pluginConfig.accountId;
158
167
  const [isLoading, setIsLoading] = useState(true);
159
168
  const [isInitialized, setIsInitialized] = useState(false);
160
169
  const [token, setToken] = useState(null);
@@ -163,12 +172,16 @@ rawPluginConfig, }) {
163
172
  const isInitializing = useRef(false);
164
173
  // Initialize environment configuration
165
174
  const [environmentConfig, _setEnvironmentConfig] = useState(() => {
166
- const detectedEnv = environment || detectEnvironment();
175
+ const envFromVariables = resolveEnvironmentFromVariables();
176
+ const detectedEnv = environment || envFromVariables || detectEnvironment();
167
177
  const config = getEnvironmentConfig(detectedEnv);
168
178
  // Log environment detection for debugging
169
179
  if (environment) {
170
180
  console.log(`[TagadaSDK] Using explicit environment: ${environment}`);
171
181
  }
182
+ else if (envFromVariables) {
183
+ console.log(`[TagadaSDK] Using environment from env variables: ${envFromVariables}`);
184
+ }
172
185
  else {
173
186
  console.log(`[TagadaSDK] Auto-detected environment: ${detectedEnv} (${typeof window !== 'undefined' ? window.location.hostname : 'SSR'})`);
174
187
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Utility for waiting until session is initialized
3
+ * Optimized with smart polling and fast path
4
+ */
5
+ export interface SessionWaiterOptions {
6
+ checkReady: () => boolean;
7
+ label?: string;
8
+ timeoutMs?: number;
9
+ pollIntervalMs?: number;
10
+ debug?: boolean;
11
+ }
12
+ export interface SessionReadyCheckOptions {
13
+ isSessionInitialized: boolean;
14
+ token?: string | null;
15
+ debug?: boolean;
16
+ }
17
+ /**
18
+ * Check if session is fully ready (initialized + token available)
19
+ * Checks token from context (synchronous and reliable source of truth)
20
+ */
21
+ export declare function isSessionFullyReady(options: SessionReadyCheckOptions): boolean;
22
+ /**
23
+ * Wait for a condition to be true with smart polling
24
+ * Fast path: returns immediately if condition is already true
25
+ */
26
+ export declare function waitForCondition(options: SessionWaiterOptions): Promise<void>;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Utility for waiting until session is initialized
3
+ * Optimized with smart polling and fast path
4
+ */
5
+ /**
6
+ * Check if session is fully ready (initialized + token available)
7
+ * Checks token from context (synchronous and reliable source of truth)
8
+ */
9
+ export function isSessionFullyReady(options) {
10
+ const { isSessionInitialized, token, debug = false } = options;
11
+ if (!isSessionInitialized) {
12
+ if (debug)
13
+ console.log('🔍 [SessionWaiter] Session not initialized yet');
14
+ return false;
15
+ }
16
+ const hasToken = !!token;
17
+ if (debug) {
18
+ if (hasToken) {
19
+ console.log(`✅ [SessionWaiter] Session fully ready - Token: ${token.substring(0, 8)}...`);
20
+ }
21
+ else {
22
+ console.log('⚠️ [SessionWaiter] Session initialized but NO TOKEN in context');
23
+ }
24
+ }
25
+ return hasToken;
26
+ }
27
+ /**
28
+ * Wait for a condition to be true with smart polling
29
+ * Fast path: returns immediately if condition is already true
30
+ */
31
+ export async function waitForCondition(options) {
32
+ const { checkReady, label = 'Condition', timeoutMs = 10000, pollIntervalMs = 50, debug = false } = options;
33
+ const startTime = Date.now();
34
+ // Fast path: if already ready, return immediately
35
+ if (checkReady()) {
36
+ console.log(`⚡ [${label}] Already ready (fast path)`);
37
+ return Promise.resolve();
38
+ }
39
+ console.log(`⏳ [${label}] Waiting... (timeout: ${timeoutMs}ms, poll interval: ${pollIntervalMs}ms)`);
40
+ // Smart polling
41
+ return new Promise((resolve, reject) => {
42
+ const maxAttempts = Math.floor(timeoutMs / pollIntervalMs);
43
+ let attempts = 0;
44
+ let lastLogTime = startTime;
45
+ const check = () => {
46
+ attempts++;
47
+ const elapsed = Date.now() - startTime;
48
+ if (debug) {
49
+ console.log(`🔍 [${label}] Check #${attempts} (${elapsed}ms elapsed)`);
50
+ }
51
+ // Log every 1 second to show we're still waiting
52
+ if (elapsed - (lastLogTime - startTime) >= 1000) {
53
+ console.log(`⏳ [${label}] Still waiting... (${elapsed}ms elapsed, ${attempts} checks)`);
54
+ lastLogTime = Date.now();
55
+ }
56
+ if (checkReady()) {
57
+ console.log(`✅ [${label}] Ready! (took ${elapsed}ms, ${attempts} checks)`);
58
+ resolve();
59
+ return;
60
+ }
61
+ if (attempts >= maxAttempts) {
62
+ console.error(`❌ [${label}] Timeout after ${timeoutMs}ms (${attempts} checks)`);
63
+ reject(new Error(`${label} timeout after ${timeoutMs}ms. Please refresh the page and try again.`));
64
+ return;
65
+ }
66
+ // Continue polling
67
+ setTimeout(check, pollIntervalMs);
68
+ };
69
+ // Start checking
70
+ check();
71
+ });
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagadapay/plugin-sdk",
3
- "version": "2.7.14",
3
+ "version": "2.7.18",
4
4
  "description": "Modern React SDK for building Tagada Pay plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -25,7 +25,9 @@
25
25
  "build": "tsc",
26
26
  "clean": "rm -rf dist",
27
27
  "lint": "echo \"No linting configured\"",
28
- "test": "echo \"No tests yet\" && exit 0",
28
+ "test": "jest --no-watchman",
29
+ "test:watch": "jest --watch --no-watchman",
30
+ "test:coverage": "jest --coverage --no-watchman",
29
31
  "dev": "tsc --watch",
30
32
  "prepublishOnly": "npm run clean && npm run build",
31
33
  "publish:patch": "npm version patch && npm publish",
@@ -60,6 +62,7 @@
60
62
  "@basis-theory/basis-theory-react": "^1.32.5",
61
63
  "@basis-theory/web-threeds": "^1.0.1",
62
64
  "@google-pay/button-react": "^3.0.10",
65
+ "@tagadapay/plugin-sdk": "link:",
63
66
  "@tanstack/react-query": "^5.90.2",
64
67
  "axios": "^1.10.0",
65
68
  "iso3166-2-db": "^2.3.11",
@@ -68,9 +71,12 @@
68
71
  "swr": "^2.3.6"
69
72
  },
70
73
  "devDependencies": {
74
+ "@types/jest": "^29.5.0",
71
75
  "@types/node": "^18.0.0",
72
76
  "@types/react": "^19",
73
77
  "@types/react-dom": "^19",
78
+ "jest": "^29.5.0",
79
+ "ts-jest": "^29.1.0",
74
80
  "typescript": "^5.0.0"
75
81
  },
76
82
  "peerDependencies": {