@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.
- package/dist/react/hooks/usePluginConfig.js +15 -7
- package/dist/v2/core/index.d.ts +1 -0
- package/dist/v2/core/index.js +2 -0
- package/dist/v2/core/pathRemapping.d.ts +156 -0
- package/dist/v2/core/pathRemapping.js +440 -0
- package/dist/v2/core/resources/credits.d.ts +42 -0
- package/dist/v2/core/resources/credits.js +21 -0
- package/dist/v2/core/resources/index.d.ts +1 -0
- package/dist/v2/core/resources/index.js +1 -0
- package/dist/v2/core/utils/pluginConfig.d.ts +14 -0
- package/dist/v2/core/utils/pluginConfig.js +96 -29
- package/dist/v2/index.d.ts +1 -0
- package/dist/v2/index.js +2 -0
- package/dist/v2/react/hooks/useCredits.d.ts +67 -0
- package/dist/v2/react/hooks/useCredits.js +80 -0
- package/dist/v2/react/hooks/useTranslation.d.ts +40 -3
- package/dist/v2/react/hooks/useTranslation.js +127 -15
- package/dist/v2/react/index.d.ts +2 -0
- package/dist/v2/react/index.js +1 -0
- package/package.json +2 -1
|
@@ -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
|
+
}
|
|
@@ -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
|
|
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
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
|
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
|
|
118
|
+
console.warn('⚠️ Plugin config: Account ID not found in meta tags');
|
|
136
119
|
}
|
|
137
|
-
const result = { storeId, accountId, basePath
|
|
138
|
-
console.log('
|
|
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
|
package/dist/v2/index.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
|
|
58
|
-
|
|
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
|
};
|
package/dist/v2/react/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/v2/react/index.js
CHANGED
|
@@ -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.
|
|
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
|
},
|