@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.
- package/README.md +475 -0
- package/dist/data/currencies.json +2410 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +37 -0
- package/dist/react/components/DebugDrawer.d.ts +7 -0
- package/dist/react/components/DebugDrawer.js +368 -0
- package/dist/react/components/OffersDemo.d.ts +1 -0
- package/dist/react/components/OffersDemo.js +50 -0
- package/dist/react/components/index.d.ts +1 -0
- package/dist/react/components/index.js +1 -0
- package/dist/react/config/environment.d.ts +22 -0
- package/dist/react/config/environment.js +132 -0
- package/dist/react/config/payment.d.ts +23 -0
- package/dist/react/config/payment.js +52 -0
- package/dist/react/hooks/useAuth.d.ts +4 -0
- package/dist/react/hooks/useAuth.js +12 -0
- package/dist/react/hooks/useCheckout.d.ts +262 -0
- package/dist/react/hooks/useCheckout.js +325 -0
- package/dist/react/hooks/useCurrency.d.ts +4 -0
- package/dist/react/hooks/useCurrency.js +640 -0
- package/dist/react/hooks/useCustomer.d.ts +7 -0
- package/dist/react/hooks/useCustomer.js +14 -0
- package/dist/react/hooks/useEnvironment.d.ts +7 -0
- package/dist/react/hooks/useEnvironment.js +18 -0
- package/dist/react/hooks/useLocale.d.ts +2 -0
- package/dist/react/hooks/useLocale.js +43 -0
- package/dist/react/hooks/useOffers.d.ts +99 -0
- package/dist/react/hooks/useOffers.js +115 -0
- package/dist/react/hooks/useOrder.d.ts +44 -0
- package/dist/react/hooks/useOrder.js +77 -0
- package/dist/react/hooks/usePayment.d.ts +60 -0
- package/dist/react/hooks/usePayment.js +343 -0
- package/dist/react/hooks/usePaymentPolling.d.ts +45 -0
- package/dist/react/hooks/usePaymentPolling.js +146 -0
- package/dist/react/hooks/useProducts.d.ts +95 -0
- package/dist/react/hooks/useProducts.js +120 -0
- package/dist/react/hooks/useSession.d.ts +10 -0
- package/dist/react/hooks/useSession.js +17 -0
- package/dist/react/hooks/useThreeds.d.ts +38 -0
- package/dist/react/hooks/useThreeds.js +162 -0
- package/dist/react/hooks/useThreedsModal.d.ts +16 -0
- package/dist/react/hooks/useThreedsModal.js +328 -0
- package/dist/react/index.d.ts +26 -0
- package/dist/react/index.js +27 -0
- package/dist/react/providers/TagadaProvider.d.ts +55 -0
- package/dist/react/providers/TagadaProvider.js +471 -0
- package/dist/react/services/apiService.d.ts +149 -0
- package/dist/react/services/apiService.js +168 -0
- package/dist/react/types.d.ts +151 -0
- package/dist/react/types.js +4 -0
- package/dist/react/utils/__tests__/urlUtils.test.d.ts +1 -0
- package/dist/react/utils/__tests__/urlUtils.test.js +189 -0
- package/dist/react/utils/deviceInfo.d.ts +39 -0
- package/dist/react/utils/deviceInfo.js +163 -0
- package/dist/react/utils/jwtDecoder.d.ts +14 -0
- package/dist/react/utils/jwtDecoder.js +86 -0
- package/dist/react/utils/money.d.ts +2273 -0
- package/dist/react/utils/money.js +104 -0
- package/dist/react/utils/tokenStorage.d.ts +16 -0
- package/dist/react/utils/tokenStorage.js +52 -0
- package/dist/react/utils/urlUtils.d.ts +239 -0
- package/dist/react/utils/urlUtils.js +449 -0
- 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;
|