@tagadapay/plugin-sdk 2.7.14 β†’ 2.7.16

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.
@@ -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,25 @@ 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
147
160
  };
148
161
  const response = await funnelResource.navigate(requestBody);
149
162
  if (response.success && response.result) {
@@ -0,0 +1,22 @@
1
+ import { RawPluginConfig } from '../../core/utils/pluginConfig';
2
+ /**
3
+ * Hook to load local plugin config with minimal boilerplate
4
+ * Handles loading state and returns config when ready
5
+ *
6
+ * @example
7
+ * function MyApp() {
8
+ * const { config, isLoading } = useLocalPluginConfig();
9
+ *
10
+ * if (isLoading) return <div>Loading...</div>;
11
+ *
12
+ * return (
13
+ * <TagadaProvider rawPluginConfig={config}>
14
+ * <YourApp />
15
+ * </TagadaProvider>
16
+ * );
17
+ * }
18
+ */
19
+ export declare function useLocalPluginConfig(): {
20
+ config: RawPluginConfig | null;
21
+ isLoading: boolean;
22
+ };
@@ -0,0 +1,30 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { localConfigIfAny } from '../../core/utils/pluginConfig';
3
+ /**
4
+ * Hook to load local plugin config with minimal boilerplate
5
+ * Handles loading state and returns config when ready
6
+ *
7
+ * @example
8
+ * function MyApp() {
9
+ * const { config, isLoading } = useLocalPluginConfig();
10
+ *
11
+ * if (isLoading) return <div>Loading...</div>;
12
+ *
13
+ * return (
14
+ * <TagadaProvider rawPluginConfig={config}>
15
+ * <YourApp />
16
+ * </TagadaProvider>
17
+ * );
18
+ * }
19
+ */
20
+ export function useLocalPluginConfig() {
21
+ const [config, setConfig] = useState(null);
22
+ const [isLoading, setIsLoading] = useState(true);
23
+ useEffect(() => {
24
+ localConfigIfAny().then((loadedConfig) => {
25
+ setConfig(loadedConfig);
26
+ setIsLoading(false);
27
+ });
28
+ }, []);
29
+ return { config, isLoading };
30
+ }
@@ -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
+ }
@@ -18,8 +18,10 @@ export { useExpressPaymentMethods } from './hooks/useExpressPaymentMethods';
18
18
  export { useGeoLocation } from './hooks/useGeoLocation';
19
19
  export { useGoogleAutocomplete } from './hooks/useGoogleAutocomplete';
20
20
  export { getAvailableLanguages, useCountryOptions, useISOData, useLanguageImport, useRegionOptions } from './hooks/useISOData';
21
+ export { useLocalPluginConfig } from './hooks/useLocalPluginConfig';
21
22
  export { useLogin } from './hooks/useLogin';
22
23
  export { usePluginConfig } from './hooks/usePluginConfig';
24
+ export { useRemappableParams } from './hooks/useRemappableParams';
23
25
  export { queryKeys, useApiMutation, useApiQuery, useInvalidateQuery, usePreloadQuery } from './hooks/useApiQuery';
24
26
  export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
25
27
  export { useCurrency } from './hooks/useCurrency';
@@ -21,8 +21,10 @@ export { useExpressPaymentMethods } from './hooks/useExpressPaymentMethods';
21
21
  export { useGeoLocation } from './hooks/useGeoLocation';
22
22
  export { useGoogleAutocomplete } from './hooks/useGoogleAutocomplete';
23
23
  export { getAvailableLanguages, useCountryOptions, useISOData, useLanguageImport, useRegionOptions } from './hooks/useISOData';
24
+ export { useLocalPluginConfig } from './hooks/useLocalPluginConfig';
24
25
  export { useLogin } from './hooks/useLogin';
25
26
  export { usePluginConfig } from './hooks/usePluginConfig';
27
+ export { useRemappableParams } from './hooks/useRemappableParams';
26
28
  // TanStack Query hooks (recommended)
27
29
  export { queryKeys, useApiMutation, useApiQuery, useInvalidateQuery, usePreloadQuery } from './hooks/useApiQuery';
28
30
  export { useCheckoutQuery as useCheckout } from './hooks/useCheckoutQuery';
@@ -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.16",
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": {