@tagadapay/plugin-sdk 2.7.0 → 2.7.3

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,21 @@
1
+ /**
2
+ * Credits Resource Client
3
+ * Handles credits-related API operations
4
+ */
5
+ export class CreditsResource {
6
+ constructor(apiClient) {
7
+ this.apiClient = apiClient;
8
+ }
9
+ /**
10
+ * Get customer credit balance and transaction history
11
+ */
12
+ async getCustomerCredits(customerId, storeId) {
13
+ return this.apiClient.get(`/api/v1/credits/balance/${customerId}/${storeId}`);
14
+ }
15
+ /**
16
+ * Redeem a product using credits
17
+ */
18
+ async redeemProduct(data) {
19
+ return this.apiClient.post('/api/v1/credits/redeem', data);
20
+ }
21
+ }
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export * from './apiClient';
6
6
  export * from './checkout';
7
+ export * from './credits';
7
8
  export * from './customer';
8
9
  export * from './discounts';
9
10
  export * from './funnel';
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export * from './apiClient';
6
6
  export * from './checkout';
7
+ export * from './credits';
7
8
  export * from './customer';
8
9
  export * from './discounts';
9
10
  export * from './funnel';
@@ -27,6 +27,20 @@ export type RawPluginConfig<TConfig = Record<string, any>> = {
27
27
  * Handles local dev, production, and raw config
28
28
  */
29
29
  export declare const loadPluginConfig: (configVariant?: string, rawConfig?: RawPluginConfig) => Promise<PluginConfig>;
30
+ /**
31
+ * Helper to load local config file for development (from /config directory)
32
+ * Returns the config object that can be passed to TagadaProvider
33
+ */
34
+ export declare function loadLocalConfig(configName?: string): Promise<Record<string, unknown> | null>;
35
+ export interface CreateRawPluginConfigOptions {
36
+ config?: any;
37
+ }
38
+ /**
39
+ * Creates a RawPluginConfig object from provided options
40
+ * @param options - Configuration options including storeId, accountId, basePath, configName, or a direct config object
41
+ * @returns A RawPluginConfig object or undefined if required fields are missing
42
+ */
43
+ export declare function createRawPluginConfig(options?: CreateRawPluginConfigOptions): Promise<RawPluginConfig | undefined>;
30
44
  export declare class PluginConfigUtils {
31
45
  /**
32
46
  * Get plugin configuration from various sources
@@ -88,33 +88,16 @@ const getMetaContent = (name) => {
88
88
  return metaTag?.getAttribute('content') || undefined;
89
89
  };
90
90
  /**
91
- * Load production config from headers and meta tags
91
+ * Load production config from meta tags
92
+ * Meta tags are injected by the plugin middleware during HTML serving
93
+ * This avoids making an additional HEAD request (~300ms savings)
92
94
  */
93
95
  const loadProductionConfig = async () => {
94
96
  try {
95
- // Try to get headers first
96
- let storeId;
97
- let accountId;
98
- let basePath;
99
- try {
100
- const response = await fetch(window.location.href, { method: 'HEAD' });
101
- storeId = response.headers.get('X-Plugin-Store-Id') || undefined;
102
- accountId = response.headers.get('X-Plugin-Account-Id') || undefined;
103
- basePath = response.headers.get('X-Plugin-Base-Path') || undefined;
104
- }
105
- catch {
106
- // Headers fetch failed, will fallback to meta tags
107
- }
108
- // Fallback to meta tags if headers are not available
109
- if (!storeId) {
110
- storeId = getMetaContent('x-plugin-store-id');
111
- }
112
- if (!accountId) {
113
- accountId = getMetaContent('x-plugin-account-id');
114
- }
115
- if (!basePath) {
116
- basePath = getMetaContent('x-plugin-base-path') || '/';
117
- }
97
+ // Read store/account info from meta tags (injected by middleware)
98
+ const storeId = getMetaContent('x-plugin-store-id');
99
+ const accountId = getMetaContent('x-plugin-account-id');
100
+ const basePath = getMetaContent('x-plugin-base-path') || '/';
118
101
  // Get deployment config from meta tags
119
102
  let config = {};
120
103
  try {
@@ -129,17 +112,22 @@ const loadProductionConfig = async () => {
129
112
  }
130
113
  // Final validation and warnings
131
114
  if (!storeId) {
132
- console.warn('⚠️ Plugin config: Store ID not found in headers or meta tags');
115
+ console.warn('⚠️ Plugin config: Store ID not found in meta tags');
133
116
  }
134
117
  if (!accountId) {
135
- console.warn('⚠️ Plugin config: Account ID not found in headers or meta tags');
118
+ console.warn('⚠️ Plugin config: Account ID not found in meta tags');
136
119
  }
137
- const result = { storeId, accountId, basePath: basePath || '/', config };
138
- console.log('🏭 Using production plugin config:', result);
120
+ const result = { storeId, accountId, basePath, config };
121
+ console.log('🚀 Plugin config loaded from meta tags (no HEAD request needed):', {
122
+ storeId,
123
+ accountId,
124
+ basePath,
125
+ configKeys: Object.keys(config)
126
+ });
139
127
  return result;
140
128
  }
141
129
  catch (error) {
142
- console.warn('Failed to load production config, using defaults:', error);
130
+ console.warn('⚠️ Failed to load production config from meta tags, using defaults:', error);
143
131
  return { basePath: '/', config: {} };
144
132
  }
145
133
  };
@@ -166,6 +154,85 @@ export const loadPluginConfig = async (configVariant = 'default', rawConfig) =>
166
154
  // Fall back to production config
167
155
  return loadProductionConfig();
168
156
  };
157
+ /**
158
+ * Helper to load local config file for development (from /config directory)
159
+ * Returns the config object that can be passed to TagadaProvider
160
+ */
161
+ export async function loadLocalConfig(configName = 'default') {
162
+ try {
163
+ // Only load in localhost
164
+ const isLocalhost = typeof window !== 'undefined' &&
165
+ (window.location.hostname === 'localhost' ||
166
+ window.location.hostname.includes('.localhost') ||
167
+ window.location.hostname.includes('127.0.0.1'));
168
+ if (!isLocalhost) {
169
+ return null;
170
+ }
171
+ // Try .tgd.json first, then .json, then .config.json
172
+ const possiblePaths = [
173
+ `/config/${configName}.tgd.json`,
174
+ `/config/${configName}.json`,
175
+ `/config/${configName}.config.json`,
176
+ ];
177
+ for (const path of possiblePaths) {
178
+ try {
179
+ const response = await fetch(path);
180
+ if (response.ok) {
181
+ const config = await response.json();
182
+ console.log(`✅ [loadLocalConfig] Loaded config from ${path}:`, {
183
+ configName: config.configName,
184
+ hasBranding: !!config.branding,
185
+ });
186
+ return config;
187
+ }
188
+ }
189
+ catch {
190
+ continue;
191
+ }
192
+ }
193
+ console.warn(`⚠️ [loadLocalConfig] No config found for '${configName}'`);
194
+ return null;
195
+ }
196
+ catch (error) {
197
+ console.error('[loadLocalConfig] Error loading config:', error);
198
+ return null;
199
+ }
200
+ }
201
+ /**
202
+ * Creates a RawPluginConfig object from provided options
203
+ * @param options - Configuration options including storeId, accountId, basePath, configName, or a direct config object
204
+ * @returns A RawPluginConfig object or undefined if required fields are missing
205
+ */
206
+ export async function createRawPluginConfig(options) {
207
+ try {
208
+ const storeId = process.env.TAGADA_STORE_ID;
209
+ const accountId = process.env.TAGADA_ACCOUNT_ID;
210
+ const basePath = process.env.TAGADA_BASE_PATH;
211
+ const configName = process.env.TAGADA_CONFIG_NAME;
212
+ if (storeId) {
213
+ console.warn('[createRawPluginConfig] No storeId provided');
214
+ return undefined;
215
+ }
216
+ const resolvedConfig = options?.config ?? await loadLocalConfig(configName);
217
+ const rawConfig = {
218
+ storeId: storeId,
219
+ accountId: accountId,
220
+ basePath: basePath || '/',
221
+ config: resolvedConfig,
222
+ };
223
+ console.log('[createRawPluginConfig] Created raw plugin config:', {
224
+ hasStoreId: !!rawConfig.storeId,
225
+ hasAccountId: !!rawConfig.accountId,
226
+ basePath: rawConfig.basePath,
227
+ hasConfig: !!rawConfig.config,
228
+ });
229
+ return rawConfig;
230
+ }
231
+ catch (error) {
232
+ console.error('[createRawPluginConfig] Error creating config:', error);
233
+ return undefined;
234
+ }
235
+ }
169
236
  export class PluginConfigUtils {
170
237
  /**
171
238
  * Get plugin configuration from various sources
@@ -10,6 +10,7 @@ export * from './core/isoData';
10
10
  export * from './core/utils/currency';
11
11
  export * from './core/utils/pluginConfig';
12
12
  export * from './core/utils/products';
13
+ export * from './core/pathRemapping';
13
14
  export type { CheckoutData, CheckoutInitParams, CheckoutLineItem, CheckoutSession, Promotion } from './core/resources/checkout';
14
15
  export type { Order, OrderLineItem } from './core/utils/order';
15
16
  export type { PostPurchaseOffer, PostPurchaseOfferItem, PostPurchaseOfferSummary } from './core/resources/postPurchases';
package/dist/v2/index.js CHANGED
@@ -11,5 +11,7 @@ export * from './core/isoData';
11
11
  export * from './core/utils/currency';
12
12
  export * from './core/utils/pluginConfig';
13
13
  export * from './core/utils/products';
14
+ // Path remapping helpers (framework-agnostic)
15
+ export * from './core/pathRemapping';
14
16
  // React exports (hooks and components only, types are exported above)
15
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';
@@ -0,0 +1,67 @@
1
+ import { CreditTransaction, RedeemProductResponse } from '../../core/resources/credits';
2
+ export interface UseCreditsOptions {
3
+ /**
4
+ * Customer ID to fetch credits for
5
+ */
6
+ customerId?: string | null;
7
+ /**
8
+ * Whether to fetch credits automatically on mount
9
+ * @default true
10
+ */
11
+ enabled?: boolean;
12
+ }
13
+ export interface UseCreditsResult {
14
+ /**
15
+ * Customer credit balance
16
+ */
17
+ balance: number;
18
+ /**
19
+ * Lifetime credits earned
20
+ */
21
+ lifetimeEarned: number;
22
+ /**
23
+ * Lifetime credits spent
24
+ */
25
+ lifetimeSpent: number;
26
+ /**
27
+ * Credit transaction history
28
+ */
29
+ transactions: CreditTransaction[];
30
+ /**
31
+ * Loading state
32
+ */
33
+ isLoading: boolean;
34
+ /**
35
+ * Error state
36
+ */
37
+ error: Error | null;
38
+ /**
39
+ * Refetch credit balance and history
40
+ */
41
+ refetch: () => Promise<void>;
42
+ /**
43
+ * Redeem a product using credits
44
+ */
45
+ redeemProduct: (data: {
46
+ productId?: string;
47
+ variantId: string;
48
+ quantity?: number;
49
+ }) => Promise<RedeemProductResponse>;
50
+ /**
51
+ * Check if customer has enough credits for a purchase
52
+ */
53
+ hasEnoughCredits: (amount: number) => boolean;
54
+ /**
55
+ * Get transaction by ID
56
+ */
57
+ getTransaction: (transactionId: string) => CreditTransaction | undefined;
58
+ /**
59
+ * Redemption mutation state
60
+ */
61
+ isRedeeming: boolean;
62
+ /**
63
+ * Redemption error state
64
+ */
65
+ redeemError: Error | null;
66
+ }
67
+ export declare function useCredits(options?: UseCreditsOptions): UseCreditsResult;
@@ -0,0 +1,80 @@
1
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
2
+ import { useCallback, useMemo } from 'react';
3
+ import { CreditsResource } from '../../core/resources/credits';
4
+ import { useTagadaContext } from '../providers/TagadaProvider';
5
+ import { getGlobalApiClient } from './useApiQuery';
6
+ import { usePluginConfig } from './usePluginConfig';
7
+ export function useCredits(options = {}) {
8
+ const { customerId = null, enabled = true } = options;
9
+ const { storeId } = usePluginConfig();
10
+ const { isSessionInitialized } = useTagadaContext();
11
+ const queryClient = useQueryClient();
12
+ // Create credits resource client
13
+ const creditsResource = useMemo(() => {
14
+ try {
15
+ return new CreditsResource(getGlobalApiClient());
16
+ }
17
+ catch (error) {
18
+ throw new Error('Failed to initialize credits resource: ' +
19
+ (error instanceof Error ? error.message : 'Unknown error'));
20
+ }
21
+ }, []);
22
+ // Determine if query should be enabled
23
+ const isEnabled = useMemo(() => {
24
+ return Boolean(enabled && customerId && storeId && isSessionInitialized);
25
+ }, [enabled, customerId, storeId, isSessionInitialized]);
26
+ // Create query key
27
+ const queryKey = useMemo(() => ['customer-credits', { customerId, storeId }], [customerId, storeId]);
28
+ // Fetch customer credits using TanStack Query
29
+ const { data = null, isLoading, error, refetch, } = useQuery({
30
+ queryKey,
31
+ enabled: isEnabled,
32
+ queryFn: async () => {
33
+ if (!customerId || !storeId) {
34
+ return null;
35
+ }
36
+ return await creditsResource.getCustomerCredits(customerId, storeId);
37
+ },
38
+ staleTime: 30 * 1000, // 30 seconds - fresher than most resources
39
+ refetchOnWindowFocus: true, // Refetch on window focus for up-to-date balance
40
+ });
41
+ // Redeem product mutation
42
+ const redeemMutation = useMutation({
43
+ mutationFn: async (redeemData) => {
44
+ return await creditsResource.redeemProduct(redeemData);
45
+ },
46
+ onSuccess: () => {
47
+ // Invalidate credits query to refetch updated balance
48
+ void queryClient.invalidateQueries({ queryKey });
49
+ },
50
+ onError: (error) => {
51
+ console.error('[SDK] Failed to redeem product with credits:', error);
52
+ },
53
+ });
54
+ // Helper functions
55
+ const hasEnoughCredits = useCallback((amount) => {
56
+ return (data?.balance ?? 0) >= amount;
57
+ }, [data?.balance]);
58
+ const getTransaction = useCallback((transactionId) => {
59
+ return data?.transactions.find((tx) => tx.id === transactionId);
60
+ }, [data?.transactions]);
61
+ const redeemProduct = useCallback(async (redeemData) => {
62
+ return await redeemMutation.mutateAsync(redeemData);
63
+ }, [redeemMutation]);
64
+ return {
65
+ balance: data?.balance ?? 0,
66
+ lifetimeEarned: data?.lifetimeEarned ?? 0,
67
+ lifetimeSpent: data?.lifetimeSpent ?? 0,
68
+ transactions: data?.transactions ?? [],
69
+ isLoading,
70
+ error,
71
+ refetch: async () => {
72
+ await refetch();
73
+ },
74
+ redeemProduct,
75
+ hasEnoughCredits,
76
+ getTransaction,
77
+ isRedeeming: redeemMutation.isPending,
78
+ redeemError: redeemMutation.error,
79
+ };
80
+ }
@@ -1,3 +1,4 @@
1
+ import { ReactNode } from 'react';
1
2
  import { LanguageCode } from '../../../data/languages';
2
3
  export type TranslationText = {
3
4
  [key in LanguageCode | string]?: string;
@@ -5,14 +6,22 @@ export type TranslationText = {
5
6
  export interface UseTranslationOptions {
6
7
  defaultLanguage?: string;
7
8
  }
8
- export type TranslateFunction = (text: TranslationText | string | undefined, fallback?: string, languageCode?: string) => string;
9
+ export type TranslationParams = {
10
+ [key: string]: string | number | ((children: ReactNode) => ReactNode);
11
+ };
12
+ export interface TranslateFunction {
13
+ (text: TranslationText | string | undefined, fallback?: string): string;
14
+ (text: TranslationText | string | undefined, fallback: string | undefined, params: undefined, languageCode: string): string;
15
+ (text: TranslationText | string | undefined, fallback?: string, params?: TranslationParams, languageCode?: string): string | ReactNode;
16
+ }
9
17
  export interface UseTranslationResult {
10
18
  /**
11
19
  * Translate function that handles multi-language text objects
12
20
  * @param text - The text to translate (can be a string or MultiLangText object)
13
21
  * @param fallback - Optional fallback text if translation is not found
22
+ * @param params - Optional parameters for string interpolation (e.g., {count: 10} for 'User has {count} points') or tag wrapping with functions (e.g., {i: (val) => <b>{val}</b>} for '<i>text</i>')
14
23
  * @param languageCode - Optional language code to override the current locale
15
- * @returns The translated string
24
+ * @returns The translated string or ReactNode (when using function params for tag wrapping)
16
25
  */
17
26
  t: TranslateFunction;
18
27
  /**
@@ -39,8 +48,36 @@ export interface UseTranslationResult {
39
48
  * // With fallback
40
49
  * const withFallback = t(multiLangText, 'Default text');
41
50
  *
51
+ * // With params interpolation
52
+ * const withParams = t('User has {count} points', '', { count: 10 });
53
+ * // Result: 'User has 10 points'
54
+ *
55
+ * // Escaped brackets are treated as literal text
56
+ * const escaped = t('Use \\{count\\} as placeholder', '', { count: 10 });
57
+ * // Result: 'Use {count} as placeholder'
58
+ *
59
+ * // With function params for tag wrapping
60
+ * const withTags = t('This is <i>formatted</i>', '', {
61
+ * i: (value) => <b>{value}</b>
62
+ * });
63
+ * // Result: ReactNode with <b>formatted</b>
64
+ *
65
+ * // Escaped tags are treated as literal text
66
+ * const escapedTags = t('This is \\<i>not formatted\\</i>', '', {
67
+ * i: (value) => <b>{value}</b>
68
+ * });
69
+ * // Result: 'This is <i>not formatted</i>'
70
+ *
71
+ * // Mix of placeholders and tags
72
+ * const mixed = t('User {name} has <b>{count}</b> points', '', {
73
+ * name: 'John',
74
+ * count: 10,
75
+ * b: (value) => <strong>{value}</strong>
76
+ * });
77
+ * // Result: ReactNode with 'User John has <strong>10</strong> points'
78
+ *
42
79
  * // Override locale
43
- * const inFrench = t(multiLangText, '', 'fr');
80
+ * const inFrench = t(multiLangText, '', undefined, 'fr');
44
81
  * ```
45
82
  */
46
83
  export declare const useTranslation: (options?: UseTranslationOptions) => UseTranslationResult;
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo } from 'react';
1
+ import React, { useCallback, useMemo } from 'react';
2
2
  /**
3
3
  * Hook for translating multi-language text objects
4
4
  *
@@ -18,8 +18,36 @@ import { useCallback, useMemo } from 'react';
18
18
  * // With fallback
19
19
  * const withFallback = t(multiLangText, 'Default text');
20
20
  *
21
+ * // With params interpolation
22
+ * const withParams = t('User has {count} points', '', { count: 10 });
23
+ * // Result: 'User has 10 points'
24
+ *
25
+ * // Escaped brackets are treated as literal text
26
+ * const escaped = t('Use \\{count\\} as placeholder', '', { count: 10 });
27
+ * // Result: 'Use {count} as placeholder'
28
+ *
29
+ * // With function params for tag wrapping
30
+ * const withTags = t('This is <i>formatted</i>', '', {
31
+ * i: (value) => <b>{value}</b>
32
+ * });
33
+ * // Result: ReactNode with <b>formatted</b>
34
+ *
35
+ * // Escaped tags are treated as literal text
36
+ * const escapedTags = t('This is \\<i>not formatted\\</i>', '', {
37
+ * i: (value) => <b>{value}</b>
38
+ * });
39
+ * // Result: 'This is <i>not formatted</i>'
40
+ *
41
+ * // Mix of placeholders and tags
42
+ * const mixed = t('User {name} has <b>{count}</b> points', '', {
43
+ * name: 'John',
44
+ * count: 10,
45
+ * b: (value) => <strong>{value}</strong>
46
+ * });
47
+ * // Result: ReactNode with 'User John has <strong>10</strong> points'
48
+ *
21
49
  * // Override locale
22
- * const inFrench = t(multiLangText, '', 'fr');
50
+ * const inFrench = t(multiLangText, '', undefined, 'fr');
23
51
  * ```
24
52
  */
25
53
  export const useTranslation = (options = {}) => {
@@ -40,22 +68,106 @@ export const useTranslation = (options = {}) => {
40
68
  }
41
69
  return defaultLanguage;
42
70
  }, [defaultLanguage]);
43
- const t = useCallback((text, fallback, languageCode) => {
71
+ const t = useCallback((text, fallback, params, languageCode) => {
72
+ // Helper function to interpolate params into the translated string
73
+ const interpolate = (str, params) => {
74
+ if (!params) {
75
+ // Even without params, we need to unescape any escaped characters
76
+ return str.replace(/\\([{}<>])/g, '$1');
77
+ }
78
+ // First, process tag-based interpolation for functions
79
+ // Match tags like <i>content</i> but not \<i>content</i>
80
+ const tagRegex = /(?<!\\)<(\w+)>(.*?)(?<!\\)<\/\1>/g;
81
+ const hasTags = tagRegex.test(str);
82
+ if (hasTags) {
83
+ // Reset regex lastIndex for reuse
84
+ tagRegex.lastIndex = 0;
85
+ const parts = [];
86
+ let lastIndex = 0;
87
+ let match;
88
+ let hasReactNodes = false;
89
+ while ((match = tagRegex.exec(str)) !== null) {
90
+ const [fullMatch, tagName, content] = match;
91
+ const param = params[tagName];
92
+ // Add text before the match
93
+ if (match.index > lastIndex) {
94
+ parts.push(str.substring(lastIndex, match.index));
95
+ }
96
+ // If param is a function, call it with the content
97
+ if (typeof param === 'function') {
98
+ parts.push(param(content));
99
+ hasReactNodes = true;
100
+ }
101
+ else {
102
+ // If not a function, keep the original tag
103
+ parts.push(fullMatch);
104
+ }
105
+ lastIndex = match.index + fullMatch.length;
106
+ }
107
+ // Add remaining text after last match
108
+ if (lastIndex < str.length) {
109
+ parts.push(str.substring(lastIndex));
110
+ }
111
+ // If we have React nodes, process placeholder interpolation for each string part
112
+ if (hasReactNodes) {
113
+ const processedParts = parts.map((part, index) => {
114
+ if (typeof part === 'string') {
115
+ // Process {key} placeholders in string parts
116
+ let processed = Object.entries(params).reduce((acc, [key, value]) => {
117
+ if (typeof value !== 'function') {
118
+ const regex = new RegExp(`(?<!\\\\)\\{${key}\\}`, 'g');
119
+ return acc.replace(regex, String(value));
120
+ }
121
+ return acc;
122
+ }, part);
123
+ // Unescape escaped characters
124
+ processed = processed.replace(/\\([{}<>])/g, '$1');
125
+ return processed;
126
+ }
127
+ return part;
128
+ });
129
+ // If only one part and it's a string, return as string
130
+ if (processedParts.length === 1 && typeof processedParts[0] === 'string') {
131
+ return processedParts[0];
132
+ }
133
+ // Return array of React nodes
134
+ return processedParts.map((part, index) => typeof part === 'string' ? part : React.createElement('span', { key: index }, part));
135
+ }
136
+ // If no React nodes, continue with normal string processing
137
+ str = parts.join('');
138
+ }
139
+ // Process {key} placeholders for string/number values
140
+ let result = Object.entries(params).reduce((acc, [key, value]) => {
141
+ if (typeof value !== 'function') {
142
+ const regex = new RegExp(`(?<!\\\\)\\{${key}\\}`, 'g');
143
+ return acc.replace(regex, String(value));
144
+ }
145
+ return acc;
146
+ }, str);
147
+ // Remove escape characters from escaped characters (\{ -> {, \} -> }, \< -> <, \> -> >)
148
+ result = result.replace(/\\([{}<>])/g, '$1');
149
+ return result;
150
+ };
44
151
  if (!text)
45
- return fallback || '';
152
+ return interpolate(fallback || '', params);
46
153
  if (typeof text === 'string')
47
- return text;
154
+ return interpolate(text, params);
48
155
  const targetLocale = languageCode || locale;
49
- if (text[targetLocale])
50
- return text[targetLocale];
51
- if (fallback)
52
- return fallback;
53
- if (text[defaultLanguage])
54
- return text[defaultLanguage];
55
- const firstAvailable = Object.values(text).find((value) => value !== undefined && value !== '');
56
- if (firstAvailable)
57
- return firstAvailable;
58
- return '';
156
+ let translatedText = '';
157
+ if (text[targetLocale]) {
158
+ translatedText = text[targetLocale];
159
+ }
160
+ else if (fallback) {
161
+ translatedText = fallback;
162
+ }
163
+ else if (text[defaultLanguage]) {
164
+ translatedText = text[defaultLanguage];
165
+ }
166
+ else {
167
+ const firstAvailable = Object.values(text).find((value) => value !== undefined && value !== '');
168
+ translatedText = firstAvailable || '';
169
+ }
170
+ return interpolate(translatedText, params);
59
171
  }, [locale, defaultLanguage]);
60
172
  return { t, locale };
61
173
  };
@@ -9,6 +9,7 @@ export { GooglePayButton } from './components/GooglePayButton';
9
9
  export { useAuth } from './hooks/useAuth';
10
10
  export { useCheckoutToken } from './hooks/useCheckoutToken';
11
11
  export { useClubOffers } from './hooks/useClubOffers';
12
+ export { useCredits } from './hooks/useCredits';
12
13
  export { useCustomer } from './hooks/useCustomer';
13
14
  export { useCustomerInfos } from './hooks/useCustomerInfos';
14
15
  export { useCustomerOrders } from './hooks/useCustomerOrders';
@@ -39,6 +40,7 @@ export { useVipOffersQuery as useVipOffers } from './hooks/useVipOffersQuery';
39
40
  export { useFunnel, useSimpleFunnel } from './hooks/useFunnel';
40
41
  export type { UseCheckoutTokenOptions, UseCheckoutTokenResult } from './hooks/useCheckoutToken';
41
42
  export type { ClubOffer, ClubOfferItem, ClubOfferLineItem, ClubOfferSummary, UseClubOffersOptions, UseClubOffersResult } from './hooks/useClubOffers';
43
+ export type { UseCreditsOptions, UseCreditsResult } from './hooks/useCredits';
42
44
  export type { UseCustomerResult } from './hooks/useCustomer';
43
45
  export type { UseCustomerInfosOptions, UseCustomerInfosResult } from './hooks/useCustomerInfos';
44
46
  export type { UseCustomerOrdersOptions, UseCustomerOrdersResult } from './hooks/useCustomerOrders';
@@ -12,6 +12,7 @@ export { GooglePayButton } from './components/GooglePayButton';
12
12
  export { useAuth } from './hooks/useAuth';
13
13
  export { useCheckoutToken } from './hooks/useCheckoutToken';
14
14
  export { useClubOffers } from './hooks/useClubOffers';
15
+ export { useCredits } from './hooks/useCredits';
15
16
  export { useCustomer } from './hooks/useCustomer';
16
17
  export { useCustomerInfos } from './hooks/useCustomerInfos';
17
18
  export { useCustomerOrders } from './hooks/useCustomerOrders';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tagadapay/plugin-sdk",
3
- "version": "2.7.0",
3
+ "version": "2.7.3",
4
4
  "description": "Modern React SDK for building Tagada Pay plugins",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -63,6 +63,7 @@
63
63
  "@tanstack/react-query": "^5.90.2",
64
64
  "axios": "^1.10.0",
65
65
  "iso3166-2-db": "^2.3.11",
66
+ "path-to-regexp": "^8.2.0",
66
67
  "react-intl": "^7.1.11",
67
68
  "swr": "^2.3.6"
68
69
  },