xcel-paygate-sdk 1.0.0
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/LICENSE +21 -0
- package/README.md +225 -0
- package/dist/api/client.d.ts +19 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/client.js +215 -0
- package/dist/components/XcelPaymentFlow.d.ts +50 -0
- package/dist/components/XcelPaymentFlow.d.ts.map +1 -0
- package/dist/components/XcelPaymentFlow.js +125 -0
- package/dist/components/XcelPaymentScreen.d.ts +67 -0
- package/dist/components/XcelPaymentScreen.d.ts.map +1 -0
- package/dist/components/XcelPaymentScreen.js +356 -0
- package/dist/components/XcelPaymentWebView.d.ts +76 -0
- package/dist/components/XcelPaymentWebView.d.ts.map +1 -0
- package/dist/components/XcelPaymentWebView.js +413 -0
- package/dist/components/index.d.ts +12 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +14 -0
- package/dist/context/XcelPayGateProvider.d.ts +56 -0
- package/dist/context/XcelPayGateProvider.d.ts.map +1 -0
- package/dist/context/XcelPayGateProvider.js +107 -0
- package/dist/hooks/use-payment-completion.d.ts +75 -0
- package/dist/hooks/use-payment-completion.d.ts.map +1 -0
- package/dist/hooks/use-payment-completion.js +181 -0
- package/dist/hooks/use-xcel-paygate.d.ts +96 -0
- package/dist/hooks/use-xcel-paygate.d.ts.map +1 -0
- package/dist/hooks/use-xcel-paygate.js +279 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/services/checkout.d.ts +32 -0
- package/dist/services/checkout.d.ts.map +1 -0
- package/dist/services/checkout.js +137 -0
- package/dist/services/xcel-wallet.d.ts +23 -0
- package/dist/services/xcel-wallet.d.ts.map +1 -0
- package/dist/services/xcel-wallet.js +107 -0
- package/dist/types/index.d.ts +373 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/payment-completion.d.ts +87 -0
- package/dist/utils/payment-completion.d.ts.map +1 -0
- package/dist/utils/payment-completion.js +110 -0
- package/dist/utils/payment-helpers.d.ts +55 -0
- package/dist/utils/payment-helpers.d.ts.map +1 -0
- package/dist/utils/payment-helpers.js +261 -0
- package/package.json +51 -0
- package/src/api/client.ts +326 -0
- package/src/components/XcelPaymentFlow.tsx +154 -0
- package/src/components/XcelPaymentScreen.tsx +477 -0
- package/src/components/XcelPaymentWebView.tsx +533 -0
- package/src/components/index.ts +14 -0
- package/src/context/XcelPayGateProvider.tsx +98 -0
- package/src/hooks/use-payment-completion.ts +225 -0
- package/src/hooks/use-xcel-paygate.ts +363 -0
- package/src/index.ts +70 -0
- package/src/services/checkout.ts +165 -0
- package/src/services/xcel-wallet.ts +175 -0
- package/src/types/index.ts +407 -0
- package/src/utils/payment-completion.ts +144 -0
- package/src/utils/payment-helpers.ts +287 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XcelPaymentWebView - WebView Payment Handler Component
|
|
3
|
+
*
|
|
4
|
+
* Handles payment flow in a WebView with automatic status detection.
|
|
5
|
+
* Drop this component in your navigation stack and it handles everything!
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```tsx
|
|
9
|
+
* import { XcelPaymentWebView } from '@xcelapp/paygate-sdk';
|
|
10
|
+
*
|
|
11
|
+
* export default function PaymentWebViewScreen({ route }) {
|
|
12
|
+
* const { paymentLink, paymentCode } = route.params;
|
|
13
|
+
*
|
|
14
|
+
* return (
|
|
15
|
+
* <XcelPaymentWebView
|
|
16
|
+
* paymentLink={paymentLink}
|
|
17
|
+
* paymentCode={paymentCode}
|
|
18
|
+
* onSuccess={(result) => {
|
|
19
|
+
* console.log('Payment successful!', result);
|
|
20
|
+
* navigation.navigate('Receipt', { result });
|
|
21
|
+
* }}
|
|
22
|
+
* onFailure={(result) => {
|
|
23
|
+
* console.log('Payment failed', result);
|
|
24
|
+
* navigation.goBack();
|
|
25
|
+
* }}
|
|
26
|
+
* />
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import React, { useRef, useState, useEffect, useCallback } from 'react';
|
|
33
|
+
import {
|
|
34
|
+
StyleSheet,
|
|
35
|
+
ActivityIndicator,
|
|
36
|
+
View,
|
|
37
|
+
Pressable,
|
|
38
|
+
Text,
|
|
39
|
+
ViewStyle,
|
|
40
|
+
TextStyle,
|
|
41
|
+
} from 'react-native';
|
|
42
|
+
|
|
43
|
+
export interface PaymentResult {
|
|
44
|
+
status: 'SUCCESS' | 'FAILED' | 'PENDING';
|
|
45
|
+
paymentCode?: string;
|
|
46
|
+
transactionId?: string;
|
|
47
|
+
url?: string;
|
|
48
|
+
bodyText?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface XcelPaymentWebViewProps {
|
|
52
|
+
/** Payment link URL */
|
|
53
|
+
paymentLink: string;
|
|
54
|
+
|
|
55
|
+
/** Payment code for tracking */
|
|
56
|
+
paymentCode?: string;
|
|
57
|
+
|
|
58
|
+
/** Additional payment details */
|
|
59
|
+
amount?: string;
|
|
60
|
+
currency?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
|
|
63
|
+
/** Called when payment succeeds */
|
|
64
|
+
onSuccess?: (result: PaymentResult) => void;
|
|
65
|
+
|
|
66
|
+
/** Called when payment fails */
|
|
67
|
+
onFailure?: (result: PaymentResult) => void;
|
|
68
|
+
|
|
69
|
+
/** Called when payment is pending */
|
|
70
|
+
onPending?: (result: PaymentResult) => void;
|
|
71
|
+
|
|
72
|
+
/** Called when user closes/cancels */
|
|
73
|
+
onCancel?: () => void;
|
|
74
|
+
|
|
75
|
+
/** Custom header component */
|
|
76
|
+
renderHeader?: () => React.ReactNode;
|
|
77
|
+
|
|
78
|
+
/** Custom loading component */
|
|
79
|
+
renderLoading?: () => React.ReactNode;
|
|
80
|
+
|
|
81
|
+
/** Custom styles */
|
|
82
|
+
styles?: {
|
|
83
|
+
container?: ViewStyle;
|
|
84
|
+
header?: ViewStyle;
|
|
85
|
+
backButton?: ViewStyle;
|
|
86
|
+
backButtonText?: TextStyle;
|
|
87
|
+
webview?: ViewStyle;
|
|
88
|
+
[key: string]: ViewStyle | TextStyle | undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Success detection timeout (ms) */
|
|
92
|
+
successTimeout?: number;
|
|
93
|
+
|
|
94
|
+
/** Show back button */
|
|
95
|
+
showBackButton?: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function XcelPaymentWebView({
|
|
99
|
+
paymentLink,
|
|
100
|
+
paymentCode,
|
|
101
|
+
amount,
|
|
102
|
+
currency,
|
|
103
|
+
description,
|
|
104
|
+
onSuccess,
|
|
105
|
+
onFailure,
|
|
106
|
+
onPending,
|
|
107
|
+
onCancel,
|
|
108
|
+
renderHeader,
|
|
109
|
+
renderLoading,
|
|
110
|
+
styles: customStyles = {},
|
|
111
|
+
successTimeout = 15000,
|
|
112
|
+
showBackButton = true,
|
|
113
|
+
}: XcelPaymentWebViewProps) {
|
|
114
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
115
|
+
const [isVerifying, setIsVerifying] = useState(false);
|
|
116
|
+
const successTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
117
|
+
const hasNavigatedRef = useRef(false);
|
|
118
|
+
|
|
119
|
+
// Note: WebView is imported dynamically to avoid requiring it as a dependency
|
|
120
|
+
// Users must install react-native-webview separately
|
|
121
|
+
const [WebView, setWebView] = useState<any>(null);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
// Dynamically import WebView
|
|
125
|
+
import('react-native-webview')
|
|
126
|
+
.then((module) => {
|
|
127
|
+
setWebView(() => module.WebView);
|
|
128
|
+
})
|
|
129
|
+
.catch((error) => {
|
|
130
|
+
console.error(
|
|
131
|
+
'Error: react-native-webview is required. Install it with: npm install react-native-webview'
|
|
132
|
+
);
|
|
133
|
+
console.error(error);
|
|
134
|
+
});
|
|
135
|
+
}, []);
|
|
136
|
+
|
|
137
|
+
// Cleanup timer on unmount
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
return () => {
|
|
140
|
+
if (successTimerRef.current) {
|
|
141
|
+
clearTimeout(successTimerRef.current);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}, []);
|
|
145
|
+
|
|
146
|
+
const handlePaymentResult = useCallback(
|
|
147
|
+
(result: PaymentResult) => {
|
|
148
|
+
if (hasNavigatedRef.current) {
|
|
149
|
+
return; // Prevent duplicate calls
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
hasNavigatedRef.current = true;
|
|
153
|
+
setIsVerifying(false);
|
|
154
|
+
|
|
155
|
+
// Clear any pending timers
|
|
156
|
+
if (successTimerRef.current) {
|
|
157
|
+
clearTimeout(successTimerRef.current);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const fullResult: PaymentResult = {
|
|
161
|
+
...result,
|
|
162
|
+
paymentCode: result.paymentCode || paymentCode,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
console.log('Payment result:', fullResult);
|
|
166
|
+
|
|
167
|
+
switch (result.status) {
|
|
168
|
+
case 'SUCCESS':
|
|
169
|
+
onSuccess?.(fullResult);
|
|
170
|
+
break;
|
|
171
|
+
case 'FAILED':
|
|
172
|
+
onFailure?.(fullResult);
|
|
173
|
+
break;
|
|
174
|
+
case 'PENDING':
|
|
175
|
+
onPending?.(fullResult);
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
[paymentCode, onSuccess, onFailure, onPending]
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
const handleWebViewMessage = useCallback(
|
|
183
|
+
(event: any) => {
|
|
184
|
+
try {
|
|
185
|
+
const data = JSON.parse(event.nativeEvent.data);
|
|
186
|
+
console.log('WebView message:', data);
|
|
187
|
+
|
|
188
|
+
switch (data.type) {
|
|
189
|
+
case 'payment_success':
|
|
190
|
+
// Set timer before auto-navigating
|
|
191
|
+
if (successTimerRef.current) {
|
|
192
|
+
clearTimeout(successTimerRef.current);
|
|
193
|
+
}
|
|
194
|
+
successTimerRef.current = setTimeout(() => {
|
|
195
|
+
handlePaymentResult({
|
|
196
|
+
status: 'SUCCESS',
|
|
197
|
+
transactionId: data.url,
|
|
198
|
+
url: data.url,
|
|
199
|
+
bodyText: data.bodyText,
|
|
200
|
+
});
|
|
201
|
+
}, successTimeout);
|
|
202
|
+
break;
|
|
203
|
+
|
|
204
|
+
case 'payment_failed':
|
|
205
|
+
handlePaymentResult({
|
|
206
|
+
status: 'FAILED',
|
|
207
|
+
transactionId: data.url,
|
|
208
|
+
url: data.url,
|
|
209
|
+
bodyText: data.bodyText,
|
|
210
|
+
});
|
|
211
|
+
break;
|
|
212
|
+
|
|
213
|
+
case 'payment_pending':
|
|
214
|
+
handlePaymentResult({
|
|
215
|
+
status: 'PENDING',
|
|
216
|
+
transactionId: data.url,
|
|
217
|
+
url: data.url,
|
|
218
|
+
bodyText: data.bodyText,
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
|
|
222
|
+
case 'close_clicked':
|
|
223
|
+
if (successTimerRef.current) {
|
|
224
|
+
clearTimeout(successTimerRef.current);
|
|
225
|
+
}
|
|
226
|
+
onCancel?.();
|
|
227
|
+
break;
|
|
228
|
+
|
|
229
|
+
case 'dom_check':
|
|
230
|
+
// Check for success/failure in DOM
|
|
231
|
+
if (data.bodyText) {
|
|
232
|
+
const text = data.bodyText.toLowerCase();
|
|
233
|
+
if (
|
|
234
|
+
text.includes('payment successful') ||
|
|
235
|
+
text.includes('transaction successful')
|
|
236
|
+
) {
|
|
237
|
+
handleWebViewMessage({
|
|
238
|
+
nativeEvent: {
|
|
239
|
+
data: JSON.stringify({ type: 'payment_success', url: data.url }),
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
} else if (
|
|
243
|
+
text.includes('payment failed') ||
|
|
244
|
+
text.includes('transaction failed')
|
|
245
|
+
) {
|
|
246
|
+
handlePaymentResult({
|
|
247
|
+
status: 'FAILED',
|
|
248
|
+
url: data.url,
|
|
249
|
+
bodyText: data.bodyText,
|
|
250
|
+
});
|
|
251
|
+
} else if (text.includes('payment pending')) {
|
|
252
|
+
handlePaymentResult({
|
|
253
|
+
status: 'PENDING',
|
|
254
|
+
url: data.url,
|
|
255
|
+
bodyText: data.bodyText,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
} catch (error) {
|
|
262
|
+
console.error('Error parsing WebView message:', error);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
[handlePaymentResult, successTimeout, onCancel]
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
const handleNavigationStateChange = useCallback(
|
|
269
|
+
(navState: any) => {
|
|
270
|
+
const url = navState.url.toLowerCase();
|
|
271
|
+
|
|
272
|
+
// Check URL for success/failure indicators
|
|
273
|
+
if (url.includes('success') || url.includes('completed')) {
|
|
274
|
+
handleWebViewMessage({
|
|
275
|
+
nativeEvent: {
|
|
276
|
+
data: JSON.stringify({ type: 'payment_success', url: navState.url }),
|
|
277
|
+
},
|
|
278
|
+
});
|
|
279
|
+
} else if (url.includes('failed') || url.includes('cancel')) {
|
|
280
|
+
handlePaymentResult({
|
|
281
|
+
status: 'FAILED',
|
|
282
|
+
url: navState.url,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
[handleWebViewMessage, handlePaymentResult]
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
const handleBack = () => {
|
|
290
|
+
if (successTimerRef.current) {
|
|
291
|
+
clearTimeout(successTimerRef.current);
|
|
292
|
+
}
|
|
293
|
+
onCancel?.();
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
if (!WebView) {
|
|
297
|
+
return (
|
|
298
|
+
<View style={[styles.container, customStyles.container]}>
|
|
299
|
+
<View style={styles.loadingContainer}>
|
|
300
|
+
<ActivityIndicator size="large" color="#007AFF" />
|
|
301
|
+
<Text style={styles.loadingText}>Loading WebView...</Text>
|
|
302
|
+
</View>
|
|
303
|
+
</View>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (!paymentLink) {
|
|
308
|
+
return (
|
|
309
|
+
<View style={[styles.container, customStyles.container]}>
|
|
310
|
+
<Text style={styles.errorText}>No payment link provided</Text>
|
|
311
|
+
{showBackButton && (
|
|
312
|
+
<Pressable
|
|
313
|
+
style={[styles.backButton, customStyles.backButton]}
|
|
314
|
+
onPress={handleBack}
|
|
315
|
+
>
|
|
316
|
+
<Text style={[styles.backButtonText, customStyles.backButtonText]}>
|
|
317
|
+
Go Back
|
|
318
|
+
</Text>
|
|
319
|
+
</Pressable>
|
|
320
|
+
)}
|
|
321
|
+
</View>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// JavaScript to inject into WebView for payment detection
|
|
326
|
+
const injectedJavaScript = `
|
|
327
|
+
(function() {
|
|
328
|
+
console.log('XCEL PayGate WebView injection started');
|
|
329
|
+
|
|
330
|
+
function monitorCloseButton() {
|
|
331
|
+
const buttons = document.querySelectorAll('button, a, div');
|
|
332
|
+
buttons.forEach(btn => {
|
|
333
|
+
const text = btn.textContent?.toLowerCase() || '';
|
|
334
|
+
if (text.includes('close') || text.includes('cancel') || text.includes('back')) {
|
|
335
|
+
btn.addEventListener('click', () => {
|
|
336
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
337
|
+
type: 'close_clicked',
|
|
338
|
+
url: window.location.href
|
|
339
|
+
}));
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function checkPaymentStatus() {
|
|
346
|
+
const bodyText = document.body?.innerText?.toLowerCase() || '';
|
|
347
|
+
const title = document.title?.toLowerCase() || '';
|
|
348
|
+
const successKeywords = ['payment successful', 'transaction successful', 'payment completed'];
|
|
349
|
+
const failureKeywords = ['payment failed', 'transaction failed', 'payment cancelled'];
|
|
350
|
+
const pendingKeywords = ['payment pending', 'processing'];
|
|
351
|
+
|
|
352
|
+
const hasSuccess = successKeywords.some(kw => bodyText.includes(kw) || title.includes(kw));
|
|
353
|
+
const hasFailure = failureKeywords.some(kw => bodyText.includes(kw) || title.includes(kw));
|
|
354
|
+
const hasPending = pendingKeywords.some(kw => bodyText.includes(kw) || title.includes(kw));
|
|
355
|
+
|
|
356
|
+
if (hasSuccess) {
|
|
357
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
358
|
+
type: 'payment_success',
|
|
359
|
+
url: window.location.href,
|
|
360
|
+
bodyText: bodyText.substring(0, 500)
|
|
361
|
+
}));
|
|
362
|
+
} else if (hasFailure) {
|
|
363
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
364
|
+
type: 'payment_failed',
|
|
365
|
+
url: window.location.href,
|
|
366
|
+
bodyText: bodyText.substring(0, 500)
|
|
367
|
+
}));
|
|
368
|
+
} else if (hasPending) {
|
|
369
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
370
|
+
type: 'payment_pending',
|
|
371
|
+
url: window.location.href,
|
|
372
|
+
bodyText: bodyText.substring(0, 500)
|
|
373
|
+
}));
|
|
374
|
+
} else {
|
|
375
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
376
|
+
type: 'dom_check',
|
|
377
|
+
url: window.location.href,
|
|
378
|
+
bodyText: bodyText.substring(0, 500),
|
|
379
|
+
title: title
|
|
380
|
+
}));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const observer = new MutationObserver(() => {
|
|
385
|
+
monitorCloseButton();
|
|
386
|
+
checkPaymentStatus();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
if (document.body) {
|
|
390
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
391
|
+
monitorCloseButton();
|
|
392
|
+
checkPaymentStatus();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
window.addEventListener('load', () => {
|
|
396
|
+
setTimeout(() => {
|
|
397
|
+
monitorCloseButton();
|
|
398
|
+
checkPaymentStatus();
|
|
399
|
+
}, 1000);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
true;
|
|
403
|
+
})();
|
|
404
|
+
`;
|
|
405
|
+
|
|
406
|
+
return (
|
|
407
|
+
<View style={[styles.container, customStyles.container]}>
|
|
408
|
+
{renderHeader ? (
|
|
409
|
+
renderHeader()
|
|
410
|
+
) : (
|
|
411
|
+
<View style={[styles.header, customStyles.header]}>
|
|
412
|
+
{showBackButton && (
|
|
413
|
+
<Pressable
|
|
414
|
+
style={[styles.backButton, customStyles.backButton]}
|
|
415
|
+
onPress={handleBack}
|
|
416
|
+
>
|
|
417
|
+
<Text style={[styles.backButtonText, customStyles.backButtonText]}>
|
|
418
|
+
← Back
|
|
419
|
+
</Text>
|
|
420
|
+
</Pressable>
|
|
421
|
+
)}
|
|
422
|
+
<Text style={styles.headerTitle}>Complete Payment</Text>
|
|
423
|
+
</View>
|
|
424
|
+
)}
|
|
425
|
+
|
|
426
|
+
<WebView
|
|
427
|
+
source={{ uri: paymentLink }}
|
|
428
|
+
style={[styles.webview, customStyles.webview]}
|
|
429
|
+
startInLoadingState={true}
|
|
430
|
+
javaScriptEnabled={true}
|
|
431
|
+
domStorageEnabled={true}
|
|
432
|
+
injectedJavaScript={injectedJavaScript}
|
|
433
|
+
onMessage={handleWebViewMessage}
|
|
434
|
+
onNavigationStateChange={handleNavigationStateChange}
|
|
435
|
+
onLoadStart={() => setIsLoading(true)}
|
|
436
|
+
onLoadEnd={() => setIsLoading(false)}
|
|
437
|
+
renderLoading={() =>
|
|
438
|
+
renderLoading ? (
|
|
439
|
+
renderLoading()
|
|
440
|
+
) : (
|
|
441
|
+
<View style={styles.loadingContainer}>
|
|
442
|
+
<ActivityIndicator size="large" color="#007AFF" />
|
|
443
|
+
<Text style={styles.loadingText}>Loading payment page...</Text>
|
|
444
|
+
</View>
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
onError={(syntheticEvent: any) => {
|
|
448
|
+
const { nativeEvent } = syntheticEvent;
|
|
449
|
+
console.warn('WebView error:', nativeEvent);
|
|
450
|
+
}}
|
|
451
|
+
/>
|
|
452
|
+
|
|
453
|
+
{isVerifying && (
|
|
454
|
+
<View style={styles.verifyingOverlay}>
|
|
455
|
+
<ActivityIndicator size="large" color="#007AFF" />
|
|
456
|
+
<Text style={styles.verifyingText}>Verifying payment...</Text>
|
|
457
|
+
</View>
|
|
458
|
+
)}
|
|
459
|
+
</View>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const styles = StyleSheet.create({
|
|
464
|
+
container: {
|
|
465
|
+
flex: 1,
|
|
466
|
+
backgroundColor: '#fff',
|
|
467
|
+
},
|
|
468
|
+
header: {
|
|
469
|
+
flexDirection: 'row',
|
|
470
|
+
alignItems: 'center',
|
|
471
|
+
paddingTop: 60,
|
|
472
|
+
paddingBottom: 16,
|
|
473
|
+
paddingHorizontal: 16,
|
|
474
|
+
borderBottomWidth: 1,
|
|
475
|
+
borderBottomColor: '#e0e0e0',
|
|
476
|
+
backgroundColor: '#fff',
|
|
477
|
+
},
|
|
478
|
+
backButton: {
|
|
479
|
+
padding: 8,
|
|
480
|
+
marginRight: 8,
|
|
481
|
+
},
|
|
482
|
+
backButtonText: {
|
|
483
|
+
fontSize: 16,
|
|
484
|
+
color: '#007AFF',
|
|
485
|
+
fontWeight: '600',
|
|
486
|
+
},
|
|
487
|
+
headerTitle: {
|
|
488
|
+
fontSize: 18,
|
|
489
|
+
fontWeight: '600',
|
|
490
|
+
flex: 1,
|
|
491
|
+
color: '#000',
|
|
492
|
+
},
|
|
493
|
+
webview: {
|
|
494
|
+
flex: 1,
|
|
495
|
+
},
|
|
496
|
+
loadingContainer: {
|
|
497
|
+
position: 'absolute',
|
|
498
|
+
top: 0,
|
|
499
|
+
left: 0,
|
|
500
|
+
right: 0,
|
|
501
|
+
bottom: 0,
|
|
502
|
+
justifyContent: 'center',
|
|
503
|
+
alignItems: 'center',
|
|
504
|
+
backgroundColor: '#fff',
|
|
505
|
+
},
|
|
506
|
+
loadingText: {
|
|
507
|
+
marginTop: 16,
|
|
508
|
+
fontSize: 14,
|
|
509
|
+
color: '#666',
|
|
510
|
+
},
|
|
511
|
+
errorText: {
|
|
512
|
+
fontSize: 16,
|
|
513
|
+
color: '#c62828',
|
|
514
|
+
textAlign: 'center',
|
|
515
|
+
marginTop: 100,
|
|
516
|
+
},
|
|
517
|
+
verifyingOverlay: {
|
|
518
|
+
position: 'absolute',
|
|
519
|
+
top: 0,
|
|
520
|
+
left: 0,
|
|
521
|
+
right: 0,
|
|
522
|
+
bottom: 0,
|
|
523
|
+
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
524
|
+
justifyContent: 'center',
|
|
525
|
+
alignItems: 'center',
|
|
526
|
+
},
|
|
527
|
+
verifyingText: {
|
|
528
|
+
color: '#fff',
|
|
529
|
+
marginTop: 16,
|
|
530
|
+
fontSize: 16,
|
|
531
|
+
fontWeight: '600',
|
|
532
|
+
},
|
|
533
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* XCEL PayGate SDK - UI Components
|
|
3
|
+
*
|
|
4
|
+
* Drop-in React Native components for payment integration
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { XcelPaymentScreen } from './XcelPaymentScreen';
|
|
8
|
+
export type { XcelPaymentScreenProps } from './XcelPaymentScreen';
|
|
9
|
+
|
|
10
|
+
export { XcelPaymentWebView } from './XcelPaymentWebView';
|
|
11
|
+
export type { XcelPaymentWebViewProps, PaymentResult } from './XcelPaymentWebView';
|
|
12
|
+
|
|
13
|
+
export { XcelPaymentFlow } from './XcelPaymentFlow';
|
|
14
|
+
export type { XcelPaymentFlowProps } from './XcelPaymentFlow';
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React, { createContext, useContext, useMemo, useRef, ReactNode } from 'react';
|
|
2
|
+
import { XcelPayGateClient } from '../api/client';
|
|
3
|
+
import { CheckoutService } from '../services/checkout';
|
|
4
|
+
import { XcelWalletService } from '../services/xcel-wallet';
|
|
5
|
+
import type { XcelPayGateConfig } from '../types';
|
|
6
|
+
|
|
7
|
+
interface XcelPayGateContextValue {
|
|
8
|
+
client: XcelPayGateClient;
|
|
9
|
+
checkout: CheckoutService;
|
|
10
|
+
wallet: XcelWalletService;
|
|
11
|
+
config: XcelPayGateConfig;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const XcelPayGateContext = createContext<XcelPayGateContextValue | null>(null);
|
|
15
|
+
|
|
16
|
+
export interface XcelPayGateProviderProps {
|
|
17
|
+
config: XcelPayGateConfig;
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* XcelPayGateProvider - Provides XCEL PayGate configuration and services to your app
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```tsx
|
|
26
|
+
* import { XcelPayGateProvider } from '@xcelapp/paygate-sdk';
|
|
27
|
+
*
|
|
28
|
+
* function App() {
|
|
29
|
+
* return (
|
|
30
|
+
* <XcelPayGateProvider
|
|
31
|
+
* config={{
|
|
32
|
+
* merchantId: 'YOUR_MERCHANT_ID',
|
|
33
|
+
* publicKey: 'YOUR_PUBLIC_KEY',
|
|
34
|
+
* }}
|
|
35
|
+
* >
|
|
36
|
+
* <YourApp />
|
|
37
|
+
* </XcelPayGateProvider>
|
|
38
|
+
* );
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
export function XcelPayGateProvider({ config, children }: XcelPayGateProviderProps) {
|
|
43
|
+
// Use refs to ensure services are only created once
|
|
44
|
+
const clientRef = useRef<XcelPayGateClient | null>(null);
|
|
45
|
+
const checkoutRef = useRef<CheckoutService | null>(null);
|
|
46
|
+
const walletRef = useRef<XcelWalletService | null>(null);
|
|
47
|
+
|
|
48
|
+
if (!clientRef.current) {
|
|
49
|
+
clientRef.current = new XcelPayGateClient(config);
|
|
50
|
+
checkoutRef.current = new CheckoutService(clientRef.current);
|
|
51
|
+
walletRef.current = new XcelWalletService(clientRef.current);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const value = useMemo(
|
|
55
|
+
() => ({
|
|
56
|
+
client: clientRef.current!,
|
|
57
|
+
checkout: checkoutRef.current!,
|
|
58
|
+
wallet: walletRef.current!,
|
|
59
|
+
config,
|
|
60
|
+
}),
|
|
61
|
+
[config]
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<XcelPayGateContext.Provider value={value}>
|
|
66
|
+
{children}
|
|
67
|
+
</XcelPayGateContext.Provider>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* useXcelPayGateContext - Access XCEL PayGate services from anywhere in your app
|
|
73
|
+
*
|
|
74
|
+
* @throws {Error} If used outside of XcelPayGateProvider
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```tsx
|
|
78
|
+
* import { useXcelPayGateContext } from '@xcelapp/paygate-sdk';
|
|
79
|
+
*
|
|
80
|
+
* function PaymentScreen() {
|
|
81
|
+
* const { client, checkout, wallet } = useXcelPayGateContext();
|
|
82
|
+
*
|
|
83
|
+
* // Use the services...
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export function useXcelPayGateContext(): XcelPayGateContextValue {
|
|
88
|
+
const context = useContext(XcelPayGateContext);
|
|
89
|
+
|
|
90
|
+
if (!context) {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'useXcelPayGateContext must be used within XcelPayGateProvider. ' +
|
|
93
|
+
'Wrap your app with <XcelPayGateProvider> or pass config directly to hooks.'
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return context;
|
|
98
|
+
}
|