@tagadapay/plugin-sdk 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +475 -0
  2. package/dist/data/currencies.json +2410 -0
  3. package/dist/index.d.ts +33 -0
  4. package/dist/index.js +37 -0
  5. package/dist/react/components/DebugDrawer.d.ts +7 -0
  6. package/dist/react/components/DebugDrawer.js +368 -0
  7. package/dist/react/components/OffersDemo.d.ts +1 -0
  8. package/dist/react/components/OffersDemo.js +50 -0
  9. package/dist/react/components/index.d.ts +1 -0
  10. package/dist/react/components/index.js +1 -0
  11. package/dist/react/config/environment.d.ts +22 -0
  12. package/dist/react/config/environment.js +132 -0
  13. package/dist/react/config/payment.d.ts +23 -0
  14. package/dist/react/config/payment.js +52 -0
  15. package/dist/react/hooks/useAuth.d.ts +4 -0
  16. package/dist/react/hooks/useAuth.js +12 -0
  17. package/dist/react/hooks/useCheckout.d.ts +262 -0
  18. package/dist/react/hooks/useCheckout.js +325 -0
  19. package/dist/react/hooks/useCurrency.d.ts +4 -0
  20. package/dist/react/hooks/useCurrency.js +640 -0
  21. package/dist/react/hooks/useCustomer.d.ts +7 -0
  22. package/dist/react/hooks/useCustomer.js +14 -0
  23. package/dist/react/hooks/useEnvironment.d.ts +7 -0
  24. package/dist/react/hooks/useEnvironment.js +18 -0
  25. package/dist/react/hooks/useLocale.d.ts +2 -0
  26. package/dist/react/hooks/useLocale.js +43 -0
  27. package/dist/react/hooks/useOffers.d.ts +99 -0
  28. package/dist/react/hooks/useOffers.js +115 -0
  29. package/dist/react/hooks/useOrder.d.ts +44 -0
  30. package/dist/react/hooks/useOrder.js +77 -0
  31. package/dist/react/hooks/usePayment.d.ts +60 -0
  32. package/dist/react/hooks/usePayment.js +343 -0
  33. package/dist/react/hooks/usePaymentPolling.d.ts +45 -0
  34. package/dist/react/hooks/usePaymentPolling.js +146 -0
  35. package/dist/react/hooks/useProducts.d.ts +95 -0
  36. package/dist/react/hooks/useProducts.js +120 -0
  37. package/dist/react/hooks/useSession.d.ts +10 -0
  38. package/dist/react/hooks/useSession.js +17 -0
  39. package/dist/react/hooks/useThreeds.d.ts +38 -0
  40. package/dist/react/hooks/useThreeds.js +162 -0
  41. package/dist/react/hooks/useThreedsModal.d.ts +16 -0
  42. package/dist/react/hooks/useThreedsModal.js +328 -0
  43. package/dist/react/index.d.ts +26 -0
  44. package/dist/react/index.js +27 -0
  45. package/dist/react/providers/TagadaProvider.d.ts +55 -0
  46. package/dist/react/providers/TagadaProvider.js +471 -0
  47. package/dist/react/services/apiService.d.ts +149 -0
  48. package/dist/react/services/apiService.js +168 -0
  49. package/dist/react/types.d.ts +151 -0
  50. package/dist/react/types.js +4 -0
  51. package/dist/react/utils/__tests__/urlUtils.test.d.ts +1 -0
  52. package/dist/react/utils/__tests__/urlUtils.test.js +189 -0
  53. package/dist/react/utils/deviceInfo.d.ts +39 -0
  54. package/dist/react/utils/deviceInfo.js +163 -0
  55. package/dist/react/utils/jwtDecoder.d.ts +14 -0
  56. package/dist/react/utils/jwtDecoder.js +86 -0
  57. package/dist/react/utils/money.d.ts +2273 -0
  58. package/dist/react/utils/money.js +104 -0
  59. package/dist/react/utils/tokenStorage.d.ts +16 -0
  60. package/dist/react/utils/tokenStorage.js +52 -0
  61. package/dist/react/utils/urlUtils.d.ts +239 -0
  62. package/dist/react/utils/urlUtils.js +449 -0
  63. package/package.json +64 -0
@@ -0,0 +1,343 @@
1
+ import { useState, useCallback, useEffect, useRef } from 'react';
2
+ import { useTagadaContext } from '../providers/TagadaProvider';
3
+ import { getBasisTheoryApiKey } from '../config/payment';
4
+ import { usePaymentPolling } from './usePaymentPolling';
5
+ import { useThreeds } from './useThreeds';
6
+ // Helper function to format expiry date
7
+ const getCardMonthAndYear = (expiryDate) => {
8
+ const [month, year] = expiryDate.split('/');
9
+ const currentYear = new Date().getFullYear();
10
+ const century = Math.floor(currentYear / 100) * 100;
11
+ const fullYear = Number(year) + century;
12
+ return {
13
+ expiration_month: Number(month),
14
+ expiration_year: fullYear,
15
+ };
16
+ };
17
+ export function usePayment() {
18
+ const { apiService, environment } = useTagadaContext();
19
+ const [isLoading, setIsLoading] = useState(false);
20
+ const [error, setError] = useState(null);
21
+ const [currentPaymentId, setCurrentPaymentId] = useState(null);
22
+ const { startPolling, stopPolling } = usePaymentPolling();
23
+ const { createSession, startChallenge } = useThreeds();
24
+ // Track challenge in progress to prevent multiple challenges
25
+ const challengeInProgressRef = useRef(false);
26
+ // Initialize BasisTheory dynamically
27
+ const [basisTheory, setBasisTheory] = useState(null);
28
+ useEffect(() => {
29
+ let isMounted = true;
30
+ const loadBasisTheory = async () => {
31
+ try {
32
+ // Get API key from embedded configuration (with env override support)
33
+ const apiKey = getBasisTheoryApiKey(environment?.environment || 'local');
34
+ if (!apiKey) {
35
+ console.warn('BasisTheory API key not configured');
36
+ return;
37
+ }
38
+ const { BasisTheory } = await import('@basis-theory/basis-theory-js');
39
+ if (isMounted) {
40
+ const bt = await new BasisTheory().init(apiKey, {
41
+ elements: false,
42
+ });
43
+ setBasisTheory(bt);
44
+ console.log('✅ BasisTheory initialized successfully');
45
+ }
46
+ }
47
+ catch (err) {
48
+ console.error('Failed to load BasisTheory:', err);
49
+ if (isMounted) {
50
+ setError('Failed to initialize payment processor');
51
+ }
52
+ }
53
+ };
54
+ void loadBasisTheory();
55
+ return () => {
56
+ isMounted = false;
57
+ };
58
+ }, [environment]);
59
+ // Clean up polling when component unmounts
60
+ useEffect(() => {
61
+ return () => {
62
+ stopPolling();
63
+ };
64
+ }, [stopPolling]);
65
+ // Handle payment actions (3DS, redirects, etc.)
66
+ const handlePaymentAction = useCallback(async (payment, options = {}) => {
67
+ if (payment.requireAction === 'none')
68
+ return;
69
+ if (payment?.requireActionData?.processed)
70
+ return;
71
+ const actionData = payment.requireActionData;
72
+ if (!actionData)
73
+ return;
74
+ // Mark action as processed
75
+ try {
76
+ await apiService.fetch('/api/v1/payments/require-action/processed', {
77
+ method: 'POST',
78
+ body: {
79
+ paymentId: payment.id,
80
+ },
81
+ });
82
+ }
83
+ catch (error) {
84
+ console.error('Error setting payment action as processed', error);
85
+ }
86
+ console.log('Processing payment action:', actionData.type);
87
+ switch (actionData.type) {
88
+ case 'threeds_auth':
89
+ if (actionData.metadata?.threedsSession && !challengeInProgressRef.current) {
90
+ try {
91
+ challengeInProgressRef.current = true;
92
+ console.log('Starting 3DS challenge...');
93
+ await startChallenge({
94
+ sessionId: actionData.metadata.threedsSession.externalSessionId,
95
+ acsChallengeUrl: actionData.metadata.threedsSession.acsChallengeUrl,
96
+ acsTransactionId: actionData.metadata.threedsSession.acsTransID,
97
+ threeDSVersion: actionData.metadata.threedsSession.messageVersion,
98
+ }, { provider: options.threedsProvider || 'basis_theory' });
99
+ challengeInProgressRef.current = false;
100
+ console.log('3DS challenge completed');
101
+ // Start polling after challenge completion
102
+ if (payment.id) {
103
+ startPolling(payment.id, {
104
+ onRequireAction: (updatedPayment) => {
105
+ void handlePaymentAction(updatedPayment, options);
106
+ },
107
+ onSuccess: (successPayment) => {
108
+ setIsLoading(false);
109
+ options.onSuccess?.(successPayment);
110
+ },
111
+ onFailure: (errorMsg) => {
112
+ console.error('Payment failed:', errorMsg);
113
+ setError(errorMsg);
114
+ setIsLoading(false);
115
+ options.onFailure?.(errorMsg);
116
+ },
117
+ });
118
+ }
119
+ }
120
+ catch (error) {
121
+ challengeInProgressRef.current = false;
122
+ console.error('Error starting 3DS challenge:', error);
123
+ const errorMsg = error instanceof Error ? error.message : 'Failed to start 3DS challenge';
124
+ setError(errorMsg);
125
+ setIsLoading(false);
126
+ options.onFailure?.(errorMsg);
127
+ }
128
+ }
129
+ break;
130
+ case 'processor_auth':
131
+ case 'redirect': {
132
+ if (actionData.metadata?.redirect?.redirectUrl) {
133
+ window.location.href = actionData.metadata.redirect.redirectUrl;
134
+ }
135
+ break;
136
+ }
137
+ case 'error': {
138
+ const errorMsg = actionData.message || 'Payment processing failed';
139
+ setError(errorMsg);
140
+ setIsLoading(false);
141
+ options.onFailure?.(errorMsg);
142
+ break;
143
+ }
144
+ }
145
+ options.onRequireAction?.(payment);
146
+ }, [apiService, startChallenge, startPolling]);
147
+ // Create card payment instrument
148
+ const createCardPaymentInstrument = useCallback(async (cardData) => {
149
+ if (!basisTheory) {
150
+ throw new Error('Payment processor not initialized');
151
+ }
152
+ try {
153
+ console.log('Creating card payment instrument');
154
+ // Create token using BasisTheory
155
+ const btResponse = await basisTheory.tokens.create({
156
+ type: 'card',
157
+ data: {
158
+ cvc: cardData.cvc,
159
+ number: Number(cardData.cardNumber.replace(/\s+/g, '')),
160
+ ...getCardMonthAndYear(cardData.expiryDate),
161
+ },
162
+ metadata: {
163
+ nonSensitiveField: 'nonSensitiveValue',
164
+ },
165
+ });
166
+ // Create payment instrument through API
167
+ const paymentInstrumentData = {
168
+ type: btResponse.type,
169
+ card: {
170
+ maskedCardNumber: String(btResponse?.data?.number || ''),
171
+ expirationMonth: Number(btResponse?.data?.expiration_month || 0),
172
+ expirationYear: Number(btResponse?.data?.expiration_year || 0),
173
+ },
174
+ token: btResponse.id,
175
+ };
176
+ const response = await apiService.fetch('/api/v1/payment/create-payment-instrument', {
177
+ method: 'POST',
178
+ body: { paymentInstrumentData },
179
+ });
180
+ console.log('Payment instrument created:', response);
181
+ return response;
182
+ }
183
+ catch (error) {
184
+ console.error('Error creating card payment instrument:', error);
185
+ throw error;
186
+ }
187
+ }, [basisTheory, apiService]);
188
+ // Create Apple Pay payment instrument
189
+ const createApplePayPaymentInstrument = useCallback(async (applePayToken) => {
190
+ if (!applePayToken.id) {
191
+ throw new Error('Apple Pay token is missing');
192
+ }
193
+ try {
194
+ const paymentInstrumentData = {
195
+ type: 'apple_pay',
196
+ token: applePayToken.id,
197
+ dpanType: applePayToken.type,
198
+ card: {
199
+ bin: applePayToken.card.bin,
200
+ last4: applePayToken.card.last4,
201
+ expirationMonth: applePayToken.card.expiration_month,
202
+ expirationYear: applePayToken.card.expiration_year,
203
+ brand: applePayToken.card.brand,
204
+ },
205
+ };
206
+ const response = await apiService.fetch('/api/v1/payment/create-payment-instrument', {
207
+ method: 'POST',
208
+ body: { paymentInstrumentData },
209
+ });
210
+ return response;
211
+ }
212
+ catch (error) {
213
+ console.error('Error creating Apple Pay payment instrument:', error);
214
+ throw error;
215
+ }
216
+ }, [apiService]);
217
+ // Process payment directly with checkout session
218
+ const processPaymentDirect = useCallback(async (checkoutSessionId, paymentInstrumentId, threedsSessionId, options = {}) => {
219
+ try {
220
+ // Create order and process payment in one call
221
+ const response = await apiService.fetch(`/api/v1/checkout-sessions/${checkoutSessionId}/pay`, {
222
+ method: 'POST',
223
+ body: {
224
+ paymentInstrumentId,
225
+ threedsSessionId,
226
+ },
227
+ });
228
+ console.log('Payment response:', response);
229
+ setCurrentPaymentId(response.payment?.id);
230
+ if (response.payment.requireAction !== 'none') {
231
+ await handlePaymentAction(response.payment, options);
232
+ }
233
+ else if (response.payment.status === 'succeeded') {
234
+ setIsLoading(false);
235
+ options.onSuccess?.(response.payment);
236
+ }
237
+ else {
238
+ // Start polling for payment status
239
+ startPolling(response.payment?.id, {
240
+ onRequireAction: (payment) => {
241
+ void handlePaymentAction(payment, options);
242
+ },
243
+ onSuccess: (payment) => {
244
+ setIsLoading(false);
245
+ options.onSuccess?.(payment);
246
+ },
247
+ onFailure: (errorMsg) => {
248
+ console.error('Payment failed:', errorMsg);
249
+ setError(errorMsg);
250
+ setIsLoading(false);
251
+ options.onFailure?.(errorMsg);
252
+ },
253
+ });
254
+ }
255
+ return response;
256
+ }
257
+ catch (error) {
258
+ const errorMsg = error instanceof Error ? error.message : 'Payment failed';
259
+ setError(errorMsg);
260
+ setIsLoading(false);
261
+ options.onFailure?.(errorMsg);
262
+ throw error;
263
+ }
264
+ }, [apiService, handlePaymentAction, startPolling]);
265
+ // Process card payment (simplified)
266
+ const processCardPayment = useCallback(async (checkoutSessionId, cardData, options = {}) => {
267
+ setIsLoading(true);
268
+ setError(null);
269
+ try {
270
+ // 1. Create payment instrument
271
+ const paymentInstrument = await createCardPaymentInstrument(cardData);
272
+ // 2. Create 3DS session if enabled
273
+ let threedsSessionId;
274
+ if (options.enableThreeds !== false) {
275
+ try {
276
+ const threedsSession = await createSession(paymentInstrument, {
277
+ provider: options.threedsProvider || 'basis_theory',
278
+ });
279
+ threedsSessionId = threedsSession.id;
280
+ }
281
+ catch (error) {
282
+ console.warn('Failed to create 3DS session, proceeding without 3DS:', error);
283
+ }
284
+ }
285
+ // 3. Process payment directly
286
+ return await processPaymentDirect(checkoutSessionId, paymentInstrument.id, threedsSessionId, options);
287
+ }
288
+ catch (error) {
289
+ setIsLoading(false);
290
+ const errorMsg = error instanceof Error ? error.message : 'Payment failed';
291
+ setError(errorMsg);
292
+ options.onFailure?.(errorMsg);
293
+ throw error;
294
+ }
295
+ }, [createCardPaymentInstrument, createSession, processPaymentDirect]);
296
+ // Process Apple Pay payment (simplified)
297
+ const processApplePayPayment = useCallback(async (checkoutSessionId, applePayToken, options = {}) => {
298
+ setIsLoading(true);
299
+ setError(null);
300
+ try {
301
+ // 1. Create payment instrument
302
+ const paymentInstrument = await createApplePayPaymentInstrument(applePayToken);
303
+ // 2. Process payment directly (Apple Pay typically doesn't require 3DS)
304
+ return await processPaymentDirect(checkoutSessionId, paymentInstrument.id, undefined, options);
305
+ }
306
+ catch (error) {
307
+ setIsLoading(false);
308
+ const errorMsg = error instanceof Error ? error.message : 'Apple Pay payment failed';
309
+ setError(errorMsg);
310
+ options.onFailure?.(errorMsg);
311
+ throw error;
312
+ }
313
+ }, [createApplePayPaymentInstrument, processPaymentDirect]);
314
+ // Process payment with existing instrument (simplified)
315
+ const processPaymentWithInstrument = useCallback(async (checkoutSessionId, paymentInstrumentId, options = {}) => {
316
+ setIsLoading(true);
317
+ setError(null);
318
+ try {
319
+ return await processPaymentDirect(checkoutSessionId, paymentInstrumentId, undefined, options);
320
+ }
321
+ catch (error) {
322
+ setIsLoading(false);
323
+ const errorMsg = error instanceof Error ? error.message : 'Payment failed';
324
+ setError(errorMsg);
325
+ options.onFailure?.(errorMsg);
326
+ throw error;
327
+ }
328
+ }, [processPaymentDirect]);
329
+ const clearError = useCallback(() => {
330
+ setError(null);
331
+ }, []);
332
+ return {
333
+ processCardPayment,
334
+ processApplePayPayment,
335
+ processPaymentWithInstrument,
336
+ createCardPaymentInstrument,
337
+ createApplePayPaymentInstrument,
338
+ isLoading,
339
+ error,
340
+ clearError,
341
+ currentPaymentId,
342
+ };
343
+ }
@@ -0,0 +1,45 @@
1
+ export interface Payment {
2
+ id: string;
3
+ status: string;
4
+ subStatus: string;
5
+ requireAction: 'none' | 'redirect' | 'error';
6
+ requireActionData?: {
7
+ type: 'redirect' | 'threeds_auth' | 'processor_auth' | 'error';
8
+ url?: string;
9
+ processed: boolean;
10
+ processorId?: string;
11
+ metadata?: {
12
+ type: 'redirect';
13
+ redirect?: {
14
+ redirectUrl: string;
15
+ returnUrl: string;
16
+ };
17
+ threedsSession?: {
18
+ externalSessionId: string;
19
+ acsChallengeUrl: string;
20
+ acsTransID: string;
21
+ messageVersion: string;
22
+ };
23
+ };
24
+ redirectUrl?: string;
25
+ resumeToken?: string;
26
+ message?: string;
27
+ errorCode?: string;
28
+ };
29
+ }
30
+ export interface PollingOptions {
31
+ onRequireAction?: (payment: Payment, stop: () => void) => void;
32
+ onSuccess?: (payment: Payment) => void;
33
+ onFailure?: (error: string) => void;
34
+ maxAttempts?: number;
35
+ pollInterval?: number;
36
+ }
37
+ export interface PaymentPollingHook {
38
+ startPolling: (paymentId: string, options?: PollingOptions) => {
39
+ stop: () => void;
40
+ isPolling: () => boolean;
41
+ };
42
+ stopPolling: () => void;
43
+ isPolling: () => boolean;
44
+ }
45
+ export declare function usePaymentPolling(): PaymentPollingHook;
@@ -0,0 +1,146 @@
1
+ import { useCallback, useRef, useEffect } from 'react';
2
+ import { useTagadaContext } from '../providers/TagadaProvider';
3
+ export function usePaymentPolling() {
4
+ const { apiService } = useTagadaContext();
5
+ const pollIntervalRef = useRef(null);
6
+ const attemptsRef = useRef(0);
7
+ const isPollingRef = useRef(false);
8
+ const isMountedRef = useRef(true);
9
+ const currentPaymentIdRef = useRef(null);
10
+ // Track mounted state and cleanup
11
+ useEffect(() => {
12
+ isMountedRef.current = true;
13
+ return () => {
14
+ isMountedRef.current = false;
15
+ stopPolling();
16
+ };
17
+ }, []);
18
+ const stopPolling = useCallback(() => {
19
+ if (pollIntervalRef.current) {
20
+ clearInterval(pollIntervalRef.current);
21
+ pollIntervalRef.current = null;
22
+ }
23
+ isPollingRef.current = false;
24
+ currentPaymentIdRef.current = null;
25
+ if (isMountedRef.current) {
26
+ console.log('Stopped polling payment status');
27
+ }
28
+ }, []);
29
+ const startPolling = useCallback((paymentId, options = {}) => {
30
+ if (!paymentId) {
31
+ console.error('Cannot poll payment status: paymentId is missing');
32
+ return { stop: stopPolling, isPolling: () => false };
33
+ }
34
+ // Prevent multiple polling sessions for the same payment
35
+ if (currentPaymentIdRef.current === paymentId && isPollingRef.current) {
36
+ console.log('Already polling for payment:', paymentId);
37
+ return { stop: stopPolling, isPolling: () => isPollingRef.current };
38
+ }
39
+ // Clean up any existing polling
40
+ stopPolling();
41
+ // Don't start polling if component is unmounted
42
+ if (!isMountedRef.current) {
43
+ console.log('Component unmounted, skipping polling start');
44
+ return { stop: stopPolling, isPolling: () => false };
45
+ }
46
+ // Reset attempts counter and set current payment
47
+ attemptsRef.current = 0;
48
+ isPollingRef.current = true;
49
+ currentPaymentIdRef.current = paymentId;
50
+ const { onRequireAction, onSuccess, onFailure, maxAttempts = 20, pollInterval = 1500 } = options;
51
+ console.log('Starting to poll payment status for:', paymentId);
52
+ const checkPaymentStatus = async () => {
53
+ // Stop if component was unmounted or polling was stopped
54
+ if (!isMountedRef.current || !isPollingRef.current) {
55
+ return;
56
+ }
57
+ try {
58
+ attemptsRef.current++;
59
+ console.log(`Polling attempt ${attemptsRef.current}/${maxAttempts} for payment ${paymentId}`);
60
+ const payment = await apiService.fetch(`/api/v1/payments/${paymentId}`, {
61
+ method: 'GET',
62
+ });
63
+ // Check again after async operation
64
+ if (!isMountedRef.current || !isPollingRef.current) {
65
+ return;
66
+ }
67
+ // Type guard and validation
68
+ if (!payment?.id) {
69
+ console.warn('Invalid payment response received');
70
+ return;
71
+ }
72
+ console.log('Payment status update:', payment);
73
+ // Check if payment requires action
74
+ if (payment.requireAction !== 'none' && payment.requireActionData) {
75
+ console.log('Payment requires new action, handling...');
76
+ stopPolling();
77
+ if (isMountedRef.current && onRequireAction) {
78
+ onRequireAction(payment, stopPolling);
79
+ }
80
+ return;
81
+ }
82
+ // Check for successful payment
83
+ if (payment.status === 'succeeded' ||
84
+ (payment.status === 'pending' && payment.subStatus === 'authorized')) {
85
+ console.log('Payment succeeded, stopping polling');
86
+ stopPolling();
87
+ if (isMountedRef.current && onSuccess) {
88
+ onSuccess(payment);
89
+ }
90
+ return;
91
+ }
92
+ // Check for failed payment (non-succeeded and not pending)
93
+ if (payment.status !== 'succeeded' && payment.status !== 'pending') {
94
+ console.log('Payment failed, stopping polling');
95
+ stopPolling();
96
+ if (isMountedRef.current && onFailure) {
97
+ onFailure(payment.status || 'Payment failed');
98
+ }
99
+ return;
100
+ }
101
+ // Stop after max attempts
102
+ if (attemptsRef.current >= maxAttempts) {
103
+ console.log('Reached maximum polling attempts');
104
+ stopPolling();
105
+ if (isMountedRef.current && onFailure) {
106
+ onFailure('Payment verification timeout');
107
+ }
108
+ }
109
+ }
110
+ catch (error) {
111
+ console.error('Error checking payment status:', error);
112
+ // Stop polling on repeated errors to prevent infinite loops
113
+ if (attemptsRef.current >= 3) {
114
+ console.log('Multiple errors encountered, stopping polling');
115
+ stopPolling();
116
+ if (isMountedRef.current && onFailure) {
117
+ onFailure('Payment verification failed due to network errors');
118
+ }
119
+ }
120
+ // Continue polling for occasional errors
121
+ }
122
+ };
123
+ // Start polling immediately, but check if still mounted
124
+ if (isMountedRef.current && isPollingRef.current) {
125
+ void checkPaymentStatus();
126
+ pollIntervalRef.current = setInterval(() => {
127
+ if (isMountedRef.current && isPollingRef.current) {
128
+ void checkPaymentStatus();
129
+ }
130
+ else {
131
+ stopPolling();
132
+ }
133
+ }, pollInterval);
134
+ }
135
+ // Return control object
136
+ return {
137
+ stop: stopPolling,
138
+ isPolling: () => isPollingRef.current && isMountedRef.current,
139
+ };
140
+ }, [apiService, stopPolling]);
141
+ return {
142
+ startPolling,
143
+ stopPolling,
144
+ isPolling: () => isPollingRef.current && isMountedRef.current,
145
+ };
146
+ }
@@ -0,0 +1,95 @@
1
+ export interface ProductPrice {
2
+ id: string;
3
+ amount: number;
4
+ currency: string;
5
+ recurring: boolean;
6
+ interval?: string;
7
+ intervalCount?: number;
8
+ default?: boolean;
9
+ currencyOptions: Record<string, {
10
+ amount: number;
11
+ currency: string;
12
+ }>;
13
+ }
14
+ export interface ProductVariant {
15
+ id: string;
16
+ name: string;
17
+ sku?: string;
18
+ weight?: number;
19
+ imageUrl?: string;
20
+ default?: boolean;
21
+ prices: ProductPrice[];
22
+ }
23
+ export interface Product {
24
+ id: string;
25
+ name: string;
26
+ description?: string;
27
+ image?: string;
28
+ variants: ProductVariant[];
29
+ defaultPrice?: ProductPrice;
30
+ }
31
+ export interface UseProductsOptions {
32
+ /**
33
+ * Array of product IDs to fetch. If empty, fetches all products for the store
34
+ */
35
+ productIds?: string[];
36
+ /**
37
+ * Whether to fetch products automatically on mount
38
+ * @default true
39
+ */
40
+ enabled?: boolean;
41
+ /**
42
+ * Whether to include variants in the response
43
+ * @default true
44
+ */
45
+ includeVariants?: boolean;
46
+ /**
47
+ * Whether to include prices in the response
48
+ * @default true
49
+ */
50
+ includePrices?: boolean;
51
+ }
52
+ export interface UseProductsResult {
53
+ /**
54
+ * Array of fetched products
55
+ */
56
+ products: Product[];
57
+ /**
58
+ * Loading state
59
+ */
60
+ isLoading: boolean;
61
+ /**
62
+ * Error state
63
+ */
64
+ error: Error | null;
65
+ /**
66
+ * Refetch products
67
+ */
68
+ refetch: () => Promise<void>;
69
+ /**
70
+ * Get product by ID from the loaded products
71
+ */
72
+ getProduct: (productId: string) => Product | undefined;
73
+ /**
74
+ * Get variant by ID from all loaded products
75
+ */
76
+ getVariant: (variantId: string) => {
77
+ product: Product;
78
+ variant: ProductVariant;
79
+ } | undefined;
80
+ /**
81
+ * Get all variants from all products as a flat array
82
+ */
83
+ getAllVariants: () => {
84
+ product: Product;
85
+ variant: ProductVariant;
86
+ }[];
87
+ /**
88
+ * Filter variants by criteria
89
+ */
90
+ filterVariants: (predicate: (variant: ProductVariant, product: Product) => boolean) => {
91
+ product: Product;
92
+ variant: ProductVariant;
93
+ }[];
94
+ }
95
+ export declare function useProducts(options?: UseProductsOptions): UseProductsResult;