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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +225 -0
  3. package/dist/api/client.d.ts +19 -0
  4. package/dist/api/client.d.ts.map +1 -0
  5. package/dist/api/client.js +215 -0
  6. package/dist/components/XcelPaymentFlow.d.ts +50 -0
  7. package/dist/components/XcelPaymentFlow.d.ts.map +1 -0
  8. package/dist/components/XcelPaymentFlow.js +125 -0
  9. package/dist/components/XcelPaymentScreen.d.ts +67 -0
  10. package/dist/components/XcelPaymentScreen.d.ts.map +1 -0
  11. package/dist/components/XcelPaymentScreen.js +356 -0
  12. package/dist/components/XcelPaymentWebView.d.ts +76 -0
  13. package/dist/components/XcelPaymentWebView.d.ts.map +1 -0
  14. package/dist/components/XcelPaymentWebView.js +413 -0
  15. package/dist/components/index.d.ts +12 -0
  16. package/dist/components/index.d.ts.map +1 -0
  17. package/dist/components/index.js +14 -0
  18. package/dist/context/XcelPayGateProvider.d.ts +56 -0
  19. package/dist/context/XcelPayGateProvider.d.ts.map +1 -0
  20. package/dist/context/XcelPayGateProvider.js +107 -0
  21. package/dist/hooks/use-payment-completion.d.ts +75 -0
  22. package/dist/hooks/use-payment-completion.d.ts.map +1 -0
  23. package/dist/hooks/use-payment-completion.js +181 -0
  24. package/dist/hooks/use-xcel-paygate.d.ts +96 -0
  25. package/dist/hooks/use-xcel-paygate.d.ts.map +1 -0
  26. package/dist/hooks/use-xcel-paygate.js +279 -0
  27. package/dist/index.d.ts +14 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +65 -0
  30. package/dist/services/checkout.d.ts +32 -0
  31. package/dist/services/checkout.d.ts.map +1 -0
  32. package/dist/services/checkout.js +137 -0
  33. package/dist/services/xcel-wallet.d.ts +23 -0
  34. package/dist/services/xcel-wallet.d.ts.map +1 -0
  35. package/dist/services/xcel-wallet.js +107 -0
  36. package/dist/types/index.d.ts +373 -0
  37. package/dist/types/index.d.ts.map +1 -0
  38. package/dist/types/index.js +2 -0
  39. package/dist/utils/payment-completion.d.ts +87 -0
  40. package/dist/utils/payment-completion.d.ts.map +1 -0
  41. package/dist/utils/payment-completion.js +110 -0
  42. package/dist/utils/payment-helpers.d.ts +55 -0
  43. package/dist/utils/payment-helpers.d.ts.map +1 -0
  44. package/dist/utils/payment-helpers.js +261 -0
  45. package/package.json +51 -0
  46. package/src/api/client.ts +326 -0
  47. package/src/components/XcelPaymentFlow.tsx +154 -0
  48. package/src/components/XcelPaymentScreen.tsx +477 -0
  49. package/src/components/XcelPaymentWebView.tsx +533 -0
  50. package/src/components/index.ts +14 -0
  51. package/src/context/XcelPayGateProvider.tsx +98 -0
  52. package/src/hooks/use-payment-completion.ts +225 -0
  53. package/src/hooks/use-xcel-paygate.ts +363 -0
  54. package/src/index.ts +70 -0
  55. package/src/services/checkout.ts +165 -0
  56. package/src/services/xcel-wallet.ts +175 -0
  57. package/src/types/index.ts +407 -0
  58. package/src/utils/payment-completion.ts +144 -0
  59. package/src/utils/payment-helpers.ts +287 -0
@@ -0,0 +1,477 @@
1
+ /**
2
+ * XcelPaymentScreen - Complete Payment UI Component
3
+ *
4
+ * Drop-in payment screen with full UI and logic.
5
+ * Just import and use - handles everything!
6
+ *
7
+ * @example
8
+ * ```tsx
9
+ * import { XcelPaymentScreen } from '@xcelapp/paygate-sdk';
10
+ *
11
+ * export default function PaymentPage() {
12
+ * return (
13
+ * <XcelPaymentScreen
14
+ * config={{
15
+ * merchantId: 'your-merchant-id',
16
+ * publicKey: 'your-public-key',
17
+ * }}
18
+ * onPaymentComplete={(result) => {
19
+ * console.log('Payment complete:', result);
20
+ * }}
21
+ * />
22
+ * );
23
+ * }
24
+ * ```
25
+ */
26
+
27
+ import React, { useState } from 'react';
28
+ import {
29
+ ActivityIndicator,
30
+ Alert,
31
+ Pressable,
32
+ ScrollView,
33
+ StyleSheet,
34
+ Text,
35
+ TextInput,
36
+ View,
37
+ ViewStyle,
38
+ TextStyle,
39
+ } from 'react-native';
40
+ import { useCheckout, usePaymentPolling } from '../hooks/use-xcel-paygate';
41
+ import type { XcelPayGateConfig, PaymentRequest, TransactionData } from '../types';
42
+
43
+ export interface XcelPaymentScreenProps {
44
+ /** SDK Configuration */
45
+ config?: XcelPayGateConfig;
46
+
47
+ /** Optional: Custom styles */
48
+ styles?: {
49
+ container?: ViewStyle;
50
+ button?: ViewStyle;
51
+ buttonText?: TextStyle;
52
+ input?: ViewStyle;
53
+ [key: string]: ViewStyle | TextStyle | undefined;
54
+ };
55
+
56
+ /** Optional: Pre-filled form values */
57
+ defaultValues?: {
58
+ amount?: string;
59
+ email?: string;
60
+ phone?: string;
61
+ description?: string;
62
+ };
63
+
64
+ /** Optional: Customize payment request */
65
+ paymentConfig?: Partial<Omit<PaymentRequest, 'amount' | 'currency'>>;
66
+
67
+ /** Called when payment link is generated */
68
+ onPaymentLinkGenerated?: (paymentLink: string, paymentCode: string) => void;
69
+
70
+ /** Called when payment is complete (success/fail) */
71
+ onPaymentComplete?: (transaction: TransactionData) => void;
72
+
73
+ /** Called on error */
74
+ onError?: (error: Error) => void;
75
+
76
+ /** Show status check button */
77
+ showStatusButton?: boolean;
78
+
79
+ /** Enable auto-polling */
80
+ enablePolling?: boolean;
81
+
82
+ /** Custom button text */
83
+ buttonText?: string;
84
+
85
+ /** Currency (default: XAF) */
86
+ currency?: string;
87
+
88
+ /** Hide form, only show button */
89
+ minimalMode?: boolean;
90
+ }
91
+
92
+ export function XcelPaymentScreen({
93
+ config,
94
+ styles: customStyles = {},
95
+ defaultValues = {},
96
+ paymentConfig = {},
97
+ onPaymentLinkGenerated,
98
+ onPaymentComplete,
99
+ onError,
100
+ showStatusButton = true,
101
+ enablePolling = false,
102
+ buttonText = 'Generate Payment Link',
103
+ currency = 'XAF',
104
+ minimalMode = false,
105
+ }: XcelPaymentScreenProps) {
106
+ // Form state
107
+ const [amount, setAmount] = useState(defaultValues.amount || '1000');
108
+ const [email, setEmail] = useState(defaultValues.email || '');
109
+ const [phone, setPhone] = useState(defaultValues.phone || '');
110
+ const [description, setDescription] = useState(defaultValues.description || '');
111
+
112
+ // SDK Hook
113
+ const {
114
+ initiatePayment,
115
+ checkStatus,
116
+ loading,
117
+ error,
118
+ paymentLink,
119
+ paymentCode,
120
+ transaction,
121
+ } = useCheckout(config || undefined);
122
+
123
+ // Optional polling - only if enabled
124
+ const pollingEnabled = enablePolling && !!paymentCode;
125
+ const pollingResult = pollingEnabled
126
+ ? usePaymentPolling(config || null, paymentCode, {
127
+ enabled: true,
128
+ maxAttempts: 24,
129
+ intervalMs: 5000,
130
+ })
131
+ : { result: null, isPolling: false };
132
+
133
+ const { result, isPolling } = pollingResult;
134
+
135
+ // Handle payment initiation
136
+ const handlePay = async () => {
137
+ try {
138
+ const response = await initiatePayment({
139
+ amount,
140
+ currency,
141
+ client_transaction_id: `TXN-${Date.now()}`,
142
+ customer_email: email,
143
+ customer_phone: phone,
144
+ description: description,
145
+ channel: 'WEB',
146
+ redirect_url: 'https://business.xcelapp.com/#/auth',
147
+ ...paymentConfig,
148
+ });
149
+
150
+ const link = response.data.payment_link;
151
+ const code = response.data.payment_code;
152
+
153
+ console.log('✓ Payment Link Generated:', link);
154
+ console.log('✓ Payment Code:', code);
155
+
156
+ onPaymentLinkGenerated?.(link, code);
157
+ } catch (err) {
158
+ const errorObj = err instanceof Error ? err : new Error('Payment failed');
159
+ console.error('Payment Error:', errorObj);
160
+ onError?.(errorObj);
161
+ Alert.alert('Error', errorObj.message);
162
+ }
163
+ };
164
+
165
+ // Handle status check
166
+ const handleCheckStatus = async () => {
167
+ if (!paymentCode) {
168
+ Alert.alert('Error', 'No payment code available');
169
+ return;
170
+ }
171
+
172
+ try {
173
+ const txn = await checkStatus();
174
+ Alert.alert(
175
+ 'Payment Status',
176
+ `Status: ${txn.status}\nAmount: ${txn.amount} ${txn.currency}`
177
+ );
178
+
179
+ if (onPaymentComplete && (txn.status === 'SUCCESS' || txn.status === 'FAILED')) {
180
+ onPaymentComplete(txn);
181
+ }
182
+ } catch (err) {
183
+ const errorObj = err instanceof Error ? err : new Error('Status check failed');
184
+ onError?.(errorObj);
185
+ Alert.alert('Error', errorObj.message);
186
+ }
187
+ };
188
+
189
+ // Handle polling result - result is PaymentResult, not TransactionData
190
+ // We don't call onPaymentComplete here since it expects TransactionData
191
+ // User should use check status manually if using polling
192
+
193
+ // Minimal mode - just show payment link if available
194
+ if (minimalMode && paymentLink) {
195
+ return (
196
+ <View style={[styles.container, customStyles.container]}>
197
+ <Text style={styles.title}>Payment Link Ready</Text>
198
+ <Text style={styles.paymentLink}>{paymentLink}</Text>
199
+ <Text style={styles.label}>Payment Code: {paymentCode}</Text>
200
+ {showStatusButton && (
201
+ <Pressable
202
+ style={[styles.secondaryButton, customStyles.button]}
203
+ onPress={handleCheckStatus}
204
+ >
205
+ <Text style={[styles.secondaryButtonText, customStyles.buttonText]}>
206
+ Check Status
207
+ </Text>
208
+ </Pressable>
209
+ )}
210
+ </View>
211
+ );
212
+ }
213
+
214
+ return (
215
+ <ScrollView style={[styles.container, customStyles.container]}>
216
+ <View style={styles.content}>
217
+ <Text style={styles.title}>XCEL PayGate</Text>
218
+ <Text style={styles.subtitle}>Secure Payment Processing</Text>
219
+
220
+ {/* Payment Form */}
221
+ {!minimalMode && (
222
+ <View style={styles.section}>
223
+ <Text style={styles.sectionTitle}>Payment Details</Text>
224
+
225
+ <Text style={styles.label}>Amount ({currency})</Text>
226
+ <TextInput
227
+ style={[styles.input, customStyles.input]}
228
+ value={amount}
229
+ onChangeText={setAmount}
230
+ placeholder="1000"
231
+ keyboardType="decimal-pad"
232
+ />
233
+
234
+ <Text style={styles.label}>Customer Email (Optional)</Text>
235
+ <TextInput
236
+ style={[styles.input, customStyles.input]}
237
+ value={email}
238
+ onChangeText={setEmail}
239
+ placeholder="customer@example.com"
240
+ keyboardType="email-address"
241
+ autoCapitalize="none"
242
+ />
243
+
244
+ <Text style={styles.label}>Customer Phone (Optional)</Text>
245
+ <TextInput
246
+ style={[styles.input, customStyles.input]}
247
+ value={phone}
248
+ onChangeText={setPhone}
249
+ placeholder="237233429972"
250
+ keyboardType="phone-pad"
251
+ />
252
+
253
+ <Text style={styles.label}>Description (Optional)</Text>
254
+ <TextInput
255
+ style={[styles.input, customStyles.input]}
256
+ value={description}
257
+ onChangeText={setDescription}
258
+ placeholder="Payment description"
259
+ />
260
+ </View>
261
+ )}
262
+
263
+ {/* Generate Payment Button */}
264
+ <Pressable
265
+ style={[
266
+ styles.button,
267
+ customStyles.button,
268
+ loading && styles.buttonDisabled,
269
+ ]}
270
+ onPress={handlePay}
271
+ disabled={loading}
272
+ >
273
+ {loading ? (
274
+ <ActivityIndicator color="#fff" />
275
+ ) : (
276
+ <Text style={[styles.buttonText, customStyles.buttonText]}>
277
+ {buttonText}
278
+ </Text>
279
+ )}
280
+ </Pressable>
281
+
282
+ {/* Payment Result Section */}
283
+ {paymentCode && (
284
+ <View style={styles.resultSection}>
285
+ <Text style={styles.resultTitle}>✓ Payment Created</Text>
286
+
287
+ <View style={styles.resultRow}>
288
+ <Text style={styles.resultLabel}>Payment Code:</Text>
289
+ <Text style={styles.resultValue}>{paymentCode}</Text>
290
+ </View>
291
+
292
+ {paymentLink && (
293
+ <View style={styles.resultRow}>
294
+ <Text style={styles.resultLabel}>Payment Link:</Text>
295
+ <Text style={[styles.resultValue, styles.linkText]} numberOfLines={1}>
296
+ {paymentLink}
297
+ </Text>
298
+ </View>
299
+ )}
300
+
301
+ {transaction && (
302
+ <>
303
+ <View style={styles.resultRow}>
304
+ <Text style={styles.resultLabel}>Status:</Text>
305
+ <Text style={[styles.resultValue, styles.statusText]}>
306
+ {transaction.status}
307
+ </Text>
308
+ </View>
309
+
310
+ <View style={styles.resultRow}>
311
+ <Text style={styles.resultLabel}>Transaction ID:</Text>
312
+ <Text style={styles.resultValue}>
313
+ {transaction.transaction_id}
314
+ </Text>
315
+ </View>
316
+ </>
317
+ )}
318
+
319
+ {isPolling && (
320
+ <Text style={styles.pollingText}>Checking payment status...</Text>
321
+ )}
322
+
323
+ {/* Manual Status Check */}
324
+ {showStatusButton && (
325
+ <Pressable
326
+ style={[styles.secondaryButton, customStyles.button]}
327
+ onPress={handleCheckStatus}
328
+ >
329
+ <Text style={[styles.secondaryButtonText, customStyles.buttonText]}>
330
+ Check Status Manually
331
+ </Text>
332
+ </Pressable>
333
+ )}
334
+ </View>
335
+ )}
336
+
337
+ {/* Error Display */}
338
+ {error && (
339
+ <View style={styles.errorBox}>
340
+ <Text style={styles.errorText}>⚠️ {error.message}</Text>
341
+ </View>
342
+ )}
343
+ </View>
344
+ </ScrollView>
345
+ );
346
+ }
347
+
348
+ const styles = StyleSheet.create({
349
+ container: {
350
+ flex: 1,
351
+ backgroundColor: '#fff',
352
+ },
353
+ content: {
354
+ padding: 20,
355
+ },
356
+ title: {
357
+ fontSize: 24,
358
+ fontWeight: 'bold',
359
+ marginBottom: 8,
360
+ textAlign: 'center',
361
+ color: '#000',
362
+ },
363
+ subtitle: {
364
+ fontSize: 16,
365
+ marginBottom: 24,
366
+ textAlign: 'center',
367
+ color: '#666',
368
+ },
369
+ section: {
370
+ marginBottom: 24,
371
+ padding: 16,
372
+ borderRadius: 8,
373
+ backgroundColor: '#f5f5f5',
374
+ },
375
+ sectionTitle: {
376
+ fontSize: 18,
377
+ fontWeight: '600',
378
+ marginBottom: 16,
379
+ color: '#000',
380
+ },
381
+ label: {
382
+ marginBottom: 8,
383
+ fontWeight: '600',
384
+ color: '#333',
385
+ },
386
+ input: {
387
+ backgroundColor: '#fff',
388
+ borderWidth: 1,
389
+ borderColor: '#ddd',
390
+ borderRadius: 8,
391
+ padding: 12,
392
+ marginBottom: 16,
393
+ fontSize: 16,
394
+ },
395
+ button: {
396
+ backgroundColor: '#007AFF',
397
+ padding: 16,
398
+ borderRadius: 8,
399
+ alignItems: 'center',
400
+ marginBottom: 16,
401
+ },
402
+ buttonDisabled: {
403
+ opacity: 0.6,
404
+ },
405
+ buttonText: {
406
+ color: '#fff',
407
+ fontSize: 16,
408
+ fontWeight: '600',
409
+ },
410
+ secondaryButton: {
411
+ backgroundColor: '#f0f0f0',
412
+ padding: 12,
413
+ borderRadius: 8,
414
+ alignItems: 'center',
415
+ marginTop: 8,
416
+ },
417
+ secondaryButtonText: {
418
+ color: '#007AFF',
419
+ fontSize: 14,
420
+ fontWeight: '600',
421
+ },
422
+ resultSection: {
423
+ padding: 16,
424
+ borderRadius: 8,
425
+ backgroundColor: '#e8f5e9',
426
+ marginBottom: 16,
427
+ },
428
+ resultTitle: {
429
+ fontSize: 18,
430
+ fontWeight: 'bold',
431
+ marginBottom: 16,
432
+ color: '#2e7d32',
433
+ },
434
+ resultRow: {
435
+ marginBottom: 12,
436
+ },
437
+ resultLabel: {
438
+ fontWeight: '600',
439
+ marginBottom: 4,
440
+ color: '#333',
441
+ },
442
+ resultValue: {
443
+ fontSize: 14,
444
+ color: '#000',
445
+ },
446
+ linkText: {
447
+ fontSize: 12,
448
+ color: '#007AFF',
449
+ },
450
+ statusText: {
451
+ fontWeight: 'bold',
452
+ fontSize: 16,
453
+ },
454
+ pollingText: {
455
+ marginTop: 12,
456
+ fontStyle: 'italic',
457
+ color: '#666',
458
+ },
459
+ errorBox: {
460
+ backgroundColor: '#ffebee',
461
+ padding: 16,
462
+ borderRadius: 8,
463
+ marginBottom: 16,
464
+ },
465
+ errorText: {
466
+ color: '#c62828',
467
+ fontWeight: '500',
468
+ },
469
+ paymentLink: {
470
+ fontSize: 12,
471
+ color: '#007AFF',
472
+ marginBottom: 16,
473
+ padding: 12,
474
+ backgroundColor: '#f5f5f5',
475
+ borderRadius: 8,
476
+ },
477
+ });