fairsure-checkout-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/dist/components/CardPayment.d.ts +18 -0
- package/dist/components/CardPayment.js +302 -0
- package/dist/components/Checkout.d.ts +9 -0
- package/dist/components/Checkout.js +520 -0
- package/dist/components/OTPInput.d.ts +17 -0
- package/dist/components/OTPInput.js +132 -0
- package/dist/components/PaymentMethodSelector.d.ts +12 -0
- package/dist/components/PaymentMethodSelector.js +67 -0
- package/dist/components/PaystackCheckout.d.ts +13 -0
- package/dist/components/PaystackCheckout.js +385 -0
- package/dist/components/PurchaseModal.d.ts +17 -0
- package/dist/components/PurchaseModal.js +415 -0
- package/dist/components/TransferPayment.d.ts +16 -0
- package/dist/components/TransferPayment.js +311 -0
- package/dist/components/USSDPayment.d.ts +21 -0
- package/dist/components/USSDPayment.js +316 -0
- package/dist/components/usePaymentCheckout.d.ts +61 -0
- package/dist/components/usePaymentCheckout.js +398 -0
- package/dist/context/CheckoutProvider.d.ts +5 -0
- package/dist/context/CheckoutProvider.js +16 -0
- package/dist/hooks/useCheckout.d.ts +1 -0
- package/dist/hooks/useCheckout.js +8 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.js +1 -0
- package/fairsure-checkout-sdk-1.0.0.tgz +0 -0
- package/package.json +24 -0
- package/src/components/CardPayment.tsx +386 -0
- package/src/components/OTPInput.tsx +204 -0
- package/src/components/PaymentMethodSelector.tsx +99 -0
- package/src/components/PaystackCheckout.tsx +506 -0
- package/src/components/TransferPayment.tsx +372 -0
- package/src/components/USSDPayment.tsx +422 -0
- package/src/components/usePaymentCheckout.ts +441 -0
- package/src/context/CheckoutProvider.tsx +37 -0
- package/src/hooks/useCheckout.ts +11 -0
- package/src/index.ts +2 -0
- package/src/types.ts +9 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,520 @@
|
|
|
1
|
+
// components/PaystackCheckout.tsx
|
|
2
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
3
|
+
import * as Clipboard from "expo-clipboard";
|
|
4
|
+
import React, { useEffect, useState } from "react";
|
|
5
|
+
import { Alert, Modal, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, } from "react-native";
|
|
6
|
+
export default function Checkout({ amount, merchantName = "Merchant", onClose, onSuccess, }) {
|
|
7
|
+
var _a;
|
|
8
|
+
const [selectedMethod, setSelectedMethod] = useState("card");
|
|
9
|
+
const [cardNumber, setCardNumber] = useState("");
|
|
10
|
+
const [expiryDate, setExpiryDate] = useState("");
|
|
11
|
+
const [cvv, setCvv] = useState("");
|
|
12
|
+
const [selectedBank, setSelectedBank] = useState("");
|
|
13
|
+
const [showBankDropdown, setShowBankDropdown] = useState(false);
|
|
14
|
+
const [accountNumber] = useState("1234567890");
|
|
15
|
+
const [copied, setCopied] = useState(false);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
console.log("method", selectedMethod);
|
|
18
|
+
}, [selectedMethod]);
|
|
19
|
+
const paymentMethods = [
|
|
20
|
+
{ id: "card", name: "Card", icon: "card-outline" },
|
|
21
|
+
{ id: "transfer", name: "Transfer", icon: "business-outline" },
|
|
22
|
+
{ id: "ussd", name: "USSD", icon: "phone-portrait-outline" },
|
|
23
|
+
];
|
|
24
|
+
const ussdBanks = [
|
|
25
|
+
{ code: "gtb", name: "GTBank", ussdCode: `*737*50*${amount}*159#` },
|
|
26
|
+
{ code: "access", name: "Access Bank", ussdCode: `*901*${amount}*159#` },
|
|
27
|
+
{ code: "zenith", name: "Zenith Bank", ussdCode: `*966*${amount}*159#` },
|
|
28
|
+
{ code: "uba", name: "UBA", ussdCode: `*919*${amount}*159#` },
|
|
29
|
+
{ code: "first", name: "First Bank", ussdCode: `*894*${amount}*159#` },
|
|
30
|
+
{
|
|
31
|
+
code: "fidelity",
|
|
32
|
+
name: "Fidelity Bank",
|
|
33
|
+
ussdCode: `*770*${amount}*159#`,
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
const formatAmount = (value) => {
|
|
37
|
+
return value.toLocaleString("en-NG", {
|
|
38
|
+
minimumFractionDigits: 2,
|
|
39
|
+
maximumFractionDigits: 2,
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
const formatCardNumber = (value) => {
|
|
43
|
+
const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, "");
|
|
44
|
+
const matches = v.match(/\d{4,16}/g);
|
|
45
|
+
const match = (matches && matches[0]) || "";
|
|
46
|
+
const parts = [];
|
|
47
|
+
for (let i = 0; i < match.length; i += 4) {
|
|
48
|
+
parts.push(match.substring(i, i + 4));
|
|
49
|
+
}
|
|
50
|
+
return parts.length ? parts.join(" ") : value;
|
|
51
|
+
};
|
|
52
|
+
const formatExpiry = (value) => {
|
|
53
|
+
const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, "");
|
|
54
|
+
if (v.length >= 2) {
|
|
55
|
+
return v.slice(0, 2) + "/" + v.slice(2, 4);
|
|
56
|
+
}
|
|
57
|
+
return v;
|
|
58
|
+
};
|
|
59
|
+
const handleCopy = async (text) => {
|
|
60
|
+
await Clipboard.setStringAsync(text);
|
|
61
|
+
setCopied(true);
|
|
62
|
+
setTimeout(() => setCopied(false), 2000);
|
|
63
|
+
};
|
|
64
|
+
const handlePay = () => {
|
|
65
|
+
const reference = `ref_${Date.now()}`;
|
|
66
|
+
Alert.alert("Payment Successful", `Your payment of ₦${formatAmount(amount)} has been processed successfully.`, [
|
|
67
|
+
{
|
|
68
|
+
text: "OK",
|
|
69
|
+
onPress: () => {
|
|
70
|
+
if (onSuccess)
|
|
71
|
+
onSuccess(reference);
|
|
72
|
+
onClose();
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
]);
|
|
76
|
+
};
|
|
77
|
+
const handleClose = () => {
|
|
78
|
+
setCardNumber("");
|
|
79
|
+
setExpiryDate("");
|
|
80
|
+
setCvv("");
|
|
81
|
+
setSelectedBank("");
|
|
82
|
+
setSelectedMethod("card");
|
|
83
|
+
onClose();
|
|
84
|
+
};
|
|
85
|
+
return (<Modal visible transparent animationType="slide" onRequestClose={handleClose}>
|
|
86
|
+
<View style={styles.overlay}>
|
|
87
|
+
<View style={styles.modalContainer}>
|
|
88
|
+
{/* Header */}
|
|
89
|
+
<View style={styles.header}>
|
|
90
|
+
<View style={styles.headerTop}>
|
|
91
|
+
<View>
|
|
92
|
+
<Text style={styles.headerTitle}>Pay {merchantName}</Text>
|
|
93
|
+
<Text style={styles.headerSubtitle}>Complete your payment</Text>
|
|
94
|
+
</View>
|
|
95
|
+
<TouchableOpacity onPress={handleClose} style={styles.closeBtn}>
|
|
96
|
+
<Ionicons name="close" size={24} color="white"/>
|
|
97
|
+
</TouchableOpacity>
|
|
98
|
+
</View>
|
|
99
|
+
|
|
100
|
+
<View style={styles.amountBox}>
|
|
101
|
+
<Text style={styles.amountLabel}>Amount to pay</Text>
|
|
102
|
+
<Text style={styles.amountText}>₦{formatAmount(amount)}</Text>
|
|
103
|
+
</View>
|
|
104
|
+
</View>
|
|
105
|
+
|
|
106
|
+
<ScrollView style={styles.scrollContainer}>
|
|
107
|
+
<View style={styles.body}>
|
|
108
|
+
{/* Payment Methods */}
|
|
109
|
+
<View style={styles.paymentMethodsContainer}>
|
|
110
|
+
{paymentMethods.map((method) => (<TouchableOpacity key={method.id} onPress={() => setSelectedMethod(method.id)} style={[
|
|
111
|
+
styles.paymentMethod,
|
|
112
|
+
selectedMethod === method.id &&
|
|
113
|
+
styles.paymentMethodSelected,
|
|
114
|
+
]}>
|
|
115
|
+
<Ionicons name={method.icon} size={20} color={selectedMethod === method.id ? "#4F46E5" : "#6B7280"}/>
|
|
116
|
+
<Text style={[
|
|
117
|
+
styles.paymentMethodText,
|
|
118
|
+
selectedMethod === method.id &&
|
|
119
|
+
styles.paymentMethodTextSelected,
|
|
120
|
+
]}>
|
|
121
|
+
{method.name}
|
|
122
|
+
</Text>
|
|
123
|
+
</TouchableOpacity>))}
|
|
124
|
+
</View>
|
|
125
|
+
|
|
126
|
+
{/* Card Payment */}
|
|
127
|
+
{selectedMethod === "card" && (<View>
|
|
128
|
+
<View style={styles.inputGroup}>
|
|
129
|
+
<Text style={styles.inputLabel}>Card Number</Text>
|
|
130
|
+
<View style={styles.inputWrapper}>
|
|
131
|
+
<TextInput value={cardNumber} onChangeText={(text) => setCardNumber(formatCardNumber(text))} placeholder="0000 0000 0000 0000" maxLength={19} keyboardType="number-pad" style={styles.input}/>
|
|
132
|
+
<Ionicons name="card-outline" size={20} color="#9CA3AF" style={styles.inputIcon}/>
|
|
133
|
+
</View>
|
|
134
|
+
</View>
|
|
135
|
+
|
|
136
|
+
<View style={styles.row}>
|
|
137
|
+
<View style={styles.flex1}>
|
|
138
|
+
<Text style={styles.inputLabel}>Expiry Date</Text>
|
|
139
|
+
<TextInput value={expiryDate} onChangeText={(text) => setExpiryDate(formatExpiry(text))} placeholder="MM/YY" maxLength={5} keyboardType="number-pad" style={styles.input}/>
|
|
140
|
+
</View>
|
|
141
|
+
<View style={styles.flex1}>
|
|
142
|
+
<Text style={styles.inputLabel}>CVV</Text>
|
|
143
|
+
<TextInput value={cvv} onChangeText={(text) => setCvv(text.replace(/\D/g, "").slice(0, 3))} placeholder="123" maxLength={3} keyboardType="number-pad" secureTextEntry style={styles.input}/>
|
|
144
|
+
</View>
|
|
145
|
+
</View>
|
|
146
|
+
|
|
147
|
+
<View style={styles.secureBox}>
|
|
148
|
+
<Ionicons name="lock-closed" size={16} color="#6B7280"/>
|
|
149
|
+
<Text style={styles.secureText}>
|
|
150
|
+
Your payment is secured with 256-bit SSL encryption
|
|
151
|
+
</Text>
|
|
152
|
+
</View>
|
|
153
|
+
</View>)}
|
|
154
|
+
|
|
155
|
+
{/* Transfer Payment */}
|
|
156
|
+
{selectedMethod === "transfer" && (<View>
|
|
157
|
+
<View style={styles.transferBox}>
|
|
158
|
+
<Text style={styles.transferText}>
|
|
159
|
+
Transfer ₦{formatAmount(amount)} to the account below
|
|
160
|
+
</Text>
|
|
161
|
+
|
|
162
|
+
<View>
|
|
163
|
+
<View style={styles.transferDetail}>
|
|
164
|
+
<Text style={styles.transferLabel}>Bank Name</Text>
|
|
165
|
+
<Text style={styles.transferValue}>Wema Bank</Text>
|
|
166
|
+
</View>
|
|
167
|
+
|
|
168
|
+
<View style={styles.transferDetail}>
|
|
169
|
+
<Text style={styles.transferLabel}>Account Number</Text>
|
|
170
|
+
<View style={styles.row}>
|
|
171
|
+
<Text style={styles.accountNumber}>
|
|
172
|
+
{accountNumber}
|
|
173
|
+
</Text>
|
|
174
|
+
<TouchableOpacity onPress={() => handleCopy(accountNumber)}>
|
|
175
|
+
<Ionicons name={copied ? "checkmark" : "copy-outline"} size={20} color="#4F46E5"/>
|
|
176
|
+
</TouchableOpacity>
|
|
177
|
+
</View>
|
|
178
|
+
</View>
|
|
179
|
+
|
|
180
|
+
<View style={styles.transferDetail}>
|
|
181
|
+
<Text style={styles.transferLabel}>Account Name</Text>
|
|
182
|
+
<Text style={styles.transferValue}>
|
|
183
|
+
Paystack-John Doe
|
|
184
|
+
</Text>
|
|
185
|
+
</View>
|
|
186
|
+
</View>
|
|
187
|
+
</View>
|
|
188
|
+
|
|
189
|
+
<View style={styles.transferNote}>
|
|
190
|
+
<Text style={styles.transferNoteText}>
|
|
191
|
+
<Text style={{ fontWeight: "bold" }}>Note:</Text> This
|
|
192
|
+
account expires in 60 minutes. Payment will be confirmed
|
|
193
|
+
automatically.
|
|
194
|
+
</Text>
|
|
195
|
+
</View>
|
|
196
|
+
</View>)}
|
|
197
|
+
|
|
198
|
+
{/* USSD Payment */}
|
|
199
|
+
{selectedMethod === "ussd" && (<View>
|
|
200
|
+
<View style={styles.inputGroup}>
|
|
201
|
+
<Text style={styles.inputLabel}>Select Your Bank</Text>
|
|
202
|
+
<TouchableOpacity onPress={() => setShowBankDropdown(!showBankDropdown)} style={styles.dropdown}>
|
|
203
|
+
<Text style={selectedBank
|
|
204
|
+
? styles.dropdownText
|
|
205
|
+
: styles.dropdownPlaceholder}>
|
|
206
|
+
{selectedBank || "Choose your bank"}
|
|
207
|
+
</Text>
|
|
208
|
+
<Ionicons name="chevron-down" size={20} color="#9CA3AF"/>
|
|
209
|
+
</TouchableOpacity>
|
|
210
|
+
|
|
211
|
+
{showBankDropdown && (<View style={styles.dropdownList}>
|
|
212
|
+
<ScrollView style={{ maxHeight: 240 }}>
|
|
213
|
+
{ussdBanks.map((bank) => (<TouchableOpacity key={bank.code} onPress={() => {
|
|
214
|
+
setSelectedBank(bank.name);
|
|
215
|
+
setShowBankDropdown(false);
|
|
216
|
+
}} style={styles.dropdownItem}>
|
|
217
|
+
<Text style={styles.dropdownItemText}>
|
|
218
|
+
{bank.name}
|
|
219
|
+
</Text>
|
|
220
|
+
</TouchableOpacity>))}
|
|
221
|
+
</ScrollView>
|
|
222
|
+
</View>)}
|
|
223
|
+
</View>
|
|
224
|
+
|
|
225
|
+
{selectedBank && (<View style={styles.ussdContainer}>
|
|
226
|
+
<Text style={styles.ussdTitle}>
|
|
227
|
+
Dial the code below to complete payment
|
|
228
|
+
</Text>
|
|
229
|
+
|
|
230
|
+
<View style={styles.ussdCodeContainer}>
|
|
231
|
+
<View style={styles.row}>
|
|
232
|
+
<Text style={styles.ussdCodeText}>
|
|
233
|
+
{(_a = ussdBanks.find((b) => b.name === selectedBank)) === null || _a === void 0 ? void 0 : _a.ussdCode}
|
|
234
|
+
</Text>
|
|
235
|
+
<TouchableOpacity onPress={() => {
|
|
236
|
+
var _a;
|
|
237
|
+
return handleCopy(((_a = ussdBanks.find((b) => b.name === selectedBank)) === null || _a === void 0 ? void 0 : _a.ussdCode) || "");
|
|
238
|
+
}}>
|
|
239
|
+
<Ionicons name={copied ? "checkmark" : "copy-outline"} size={20} color="#4F46E5"/>
|
|
240
|
+
</TouchableOpacity>
|
|
241
|
+
</View>
|
|
242
|
+
</View>
|
|
243
|
+
|
|
244
|
+
<View>
|
|
245
|
+
<Text style={styles.ussdCodeText}>Steps:</Text>
|
|
246
|
+
<Text style={styles.ussdStepsText}>
|
|
247
|
+
1. Dial the USSD code on your phone{"\n"}
|
|
248
|
+
2. Follow the prompts on your screen{"\n"}
|
|
249
|
+
3. Authorize payment with your bank PIN
|
|
250
|
+
</Text>
|
|
251
|
+
</View>
|
|
252
|
+
</View>)}
|
|
253
|
+
</View>)}
|
|
254
|
+
|
|
255
|
+
{/* Pay Button */}
|
|
256
|
+
<TouchableOpacity onPress={handlePay} disabled={(selectedMethod === "card" &&
|
|
257
|
+
(!cardNumber || !expiryDate || !cvv)) ||
|
|
258
|
+
(selectedMethod === "ussd" && !selectedBank)} style={[
|
|
259
|
+
styles.payButton,
|
|
260
|
+
(selectedMethod === "card" &&
|
|
261
|
+
(!cardNumber || !expiryDate || !cvv)) ||
|
|
262
|
+
(selectedMethod === "ussd" && !selectedBank)
|
|
263
|
+
? styles.payButtonDisabled
|
|
264
|
+
: styles.payButtonActive,
|
|
265
|
+
]}>
|
|
266
|
+
<Text style={styles.payButtonText}>
|
|
267
|
+
{selectedMethod === "transfer"
|
|
268
|
+
? "I have sent the money"
|
|
269
|
+
: `Pay ₦${formatAmount(amount)}`}
|
|
270
|
+
</Text>
|
|
271
|
+
</TouchableOpacity>
|
|
272
|
+
|
|
273
|
+
{/* Footer */}
|
|
274
|
+
<View style={styles.footer}>
|
|
275
|
+
<Ionicons name="lock-closed" size={16} color="#9CA3AF"/>
|
|
276
|
+
<Text style={styles.footerText}>Secured by Paystack</Text>
|
|
277
|
+
</View>
|
|
278
|
+
</View>
|
|
279
|
+
</ScrollView>
|
|
280
|
+
</View>
|
|
281
|
+
</View>
|
|
282
|
+
</Modal>);
|
|
283
|
+
}
|
|
284
|
+
const styles = StyleSheet.create({
|
|
285
|
+
overlay: {
|
|
286
|
+
flex: 1,
|
|
287
|
+
backgroundColor: "rgba(0,0,0,0.5)",
|
|
288
|
+
justifyContent: "flex-end",
|
|
289
|
+
},
|
|
290
|
+
modalContainer: {
|
|
291
|
+
width: "90%",
|
|
292
|
+
alignSelf: "center",
|
|
293
|
+
borderRadius: 20,
|
|
294
|
+
height: "85%",
|
|
295
|
+
backgroundColor: "white",
|
|
296
|
+
},
|
|
297
|
+
header: {
|
|
298
|
+
backgroundColor: "#6B21A8",
|
|
299
|
+
padding: 16,
|
|
300
|
+
borderTopLeftRadius: 20,
|
|
301
|
+
borderTopRightRadius: 20,
|
|
302
|
+
},
|
|
303
|
+
headerTop: {
|
|
304
|
+
flexDirection: "row",
|
|
305
|
+
justifyContent: "space-between",
|
|
306
|
+
alignItems: "flex-start",
|
|
307
|
+
marginBottom: 16,
|
|
308
|
+
},
|
|
309
|
+
headerTitle: { color: "white", fontSize: 24, fontWeight: "bold" },
|
|
310
|
+
headerSubtitle: { color: "#E0E7FF", fontSize: 14, marginTop: 4 },
|
|
311
|
+
closeBtn: {
|
|
312
|
+
backgroundColor: "rgba(255,255,255,0.2)",
|
|
313
|
+
borderRadius: 999,
|
|
314
|
+
padding: 8,
|
|
315
|
+
},
|
|
316
|
+
amountBox: {
|
|
317
|
+
backgroundColor: "rgba(255,255,255,0.2)",
|
|
318
|
+
borderRadius: 12,
|
|
319
|
+
padding: 8,
|
|
320
|
+
},
|
|
321
|
+
amountLabel: { color: "#E0E7FF", fontSize: 12, marginBottom: 4 },
|
|
322
|
+
amountText: { color: "white", fontSize: 20, fontWeight: "bold" },
|
|
323
|
+
scrollContainer: { flex: 1 },
|
|
324
|
+
body: {
|
|
325
|
+
padding: 24,
|
|
326
|
+
backgroundColor: "white",
|
|
327
|
+
borderBottomLeftRadius: 20,
|
|
328
|
+
borderBottomRightRadius: 20,
|
|
329
|
+
},
|
|
330
|
+
paymentMethodsContainer: {
|
|
331
|
+
flexDirection: "row",
|
|
332
|
+
marginBottom: 24,
|
|
333
|
+
backgroundColor: "#F3F4F6",
|
|
334
|
+
padding: 4,
|
|
335
|
+
borderRadius: 12,
|
|
336
|
+
},
|
|
337
|
+
paymentMethod: {
|
|
338
|
+
flex: 1,
|
|
339
|
+
flexDirection: "row",
|
|
340
|
+
alignItems: "center",
|
|
341
|
+
justifyContent: "center",
|
|
342
|
+
paddingVertical: 12,
|
|
343
|
+
borderRadius: 12,
|
|
344
|
+
marginHorizontal: 4,
|
|
345
|
+
},
|
|
346
|
+
paymentMethodSelected: { backgroundColor: "white" },
|
|
347
|
+
paymentMethodText: { fontSize: 14, color: "#4B5563", marginLeft: 8 },
|
|
348
|
+
paymentMethodTextSelected: { color: "#4F46E5", fontWeight: "600" },
|
|
349
|
+
inputGroup: { marginBottom: 16 },
|
|
350
|
+
inputLabel: {
|
|
351
|
+
fontSize: 14,
|
|
352
|
+
fontWeight: "500",
|
|
353
|
+
color: "#374151",
|
|
354
|
+
marginBottom: 4,
|
|
355
|
+
},
|
|
356
|
+
inputWrapper: { position: "relative" },
|
|
357
|
+
input: {
|
|
358
|
+
width: "100%",
|
|
359
|
+
paddingHorizontal: 16,
|
|
360
|
+
paddingVertical: 12,
|
|
361
|
+
borderWidth: 1,
|
|
362
|
+
borderColor: "#D1D5DB",
|
|
363
|
+
borderRadius: 12,
|
|
364
|
+
backgroundColor: "white",
|
|
365
|
+
},
|
|
366
|
+
inputIcon: { position: "absolute", right: 16, top: 14 },
|
|
367
|
+
row: { flexDirection: "row", gap: 16 },
|
|
368
|
+
flex1: { flex: 1 },
|
|
369
|
+
secureBox: {
|
|
370
|
+
flexDirection: "row",
|
|
371
|
+
alignItems: "center",
|
|
372
|
+
backgroundColor: "#F9FAFB",
|
|
373
|
+
padding: 12,
|
|
374
|
+
borderRadius: 12,
|
|
375
|
+
gap: 8,
|
|
376
|
+
marginBottom: 16,
|
|
377
|
+
},
|
|
378
|
+
secureText: { fontSize: 12, color: "#4B5563", flex: 1 },
|
|
379
|
+
transferBox: {
|
|
380
|
+
backgroundColor: "#EFF6FF",
|
|
381
|
+
borderWidth: 1,
|
|
382
|
+
borderColor: "#BFDBFE",
|
|
383
|
+
borderRadius: 12,
|
|
384
|
+
padding: 8,
|
|
385
|
+
marginBottom: 8,
|
|
386
|
+
},
|
|
387
|
+
transferText: {
|
|
388
|
+
fontSize: 14,
|
|
389
|
+
color: "#1E3A8A",
|
|
390
|
+
fontWeight: "500",
|
|
391
|
+
marginBottom: 12,
|
|
392
|
+
},
|
|
393
|
+
transferDetail: {
|
|
394
|
+
backgroundColor: "white",
|
|
395
|
+
borderRadius: 12,
|
|
396
|
+
padding: 8,
|
|
397
|
+
marginBottom: 12,
|
|
398
|
+
},
|
|
399
|
+
transferLabel: { fontSize: 10, color: "#4B5563", marginBottom: 4 },
|
|
400
|
+
transferValue: { fontWeight: "600", color: "#111827" },
|
|
401
|
+
accountNumber: {
|
|
402
|
+
fontWeight: "600",
|
|
403
|
+
color: "#111827",
|
|
404
|
+
fontSize: 16,
|
|
405
|
+
letterSpacing: 2,
|
|
406
|
+
flex: 1,
|
|
407
|
+
},
|
|
408
|
+
transferNote: {
|
|
409
|
+
backgroundColor: "#FEF3C7",
|
|
410
|
+
borderWidth: 1,
|
|
411
|
+
borderColor: "#FDE68A",
|
|
412
|
+
borderRadius: 12,
|
|
413
|
+
padding: 8,
|
|
414
|
+
},
|
|
415
|
+
transferNoteText: { fontSize: 14, color: "#78350F" },
|
|
416
|
+
dropdown: {
|
|
417
|
+
width: "100%",
|
|
418
|
+
paddingHorizontal: 16,
|
|
419
|
+
paddingVertical: 12,
|
|
420
|
+
borderWidth: 1,
|
|
421
|
+
borderColor: "#D1D5DB",
|
|
422
|
+
borderRadius: 12,
|
|
423
|
+
flexDirection: "row",
|
|
424
|
+
justifyContent: "space-between",
|
|
425
|
+
alignItems: "center",
|
|
426
|
+
},
|
|
427
|
+
dropdownText: { color: "#111827" },
|
|
428
|
+
dropdownPlaceholder: { color: "#9CA3AF" },
|
|
429
|
+
dropdownList: {
|
|
430
|
+
marginTop: 8,
|
|
431
|
+
backgroundColor: "white",
|
|
432
|
+
borderWidth: 1,
|
|
433
|
+
borderColor: "#E5E7EB",
|
|
434
|
+
borderRadius: 12,
|
|
435
|
+
overflow: "hidden",
|
|
436
|
+
maxHeight: 240,
|
|
437
|
+
},
|
|
438
|
+
dropdownItem: {
|
|
439
|
+
paddingVertical: 12,
|
|
440
|
+
paddingHorizontal: 16,
|
|
441
|
+
borderBottomWidth: 1,
|
|
442
|
+
borderBottomColor: "#F3F4F6",
|
|
443
|
+
},
|
|
444
|
+
dropdownItemText: { color: "#111827" },
|
|
445
|
+
ussdContainer: {
|
|
446
|
+
backgroundColor: "#E0E7FF", // bg-indigo-50
|
|
447
|
+
borderWidth: 1,
|
|
448
|
+
borderColor: "#C7D2FE", // border-indigo-200
|
|
449
|
+
borderRadius: 12, // rounded-xl
|
|
450
|
+
padding: 16, // p-4
|
|
451
|
+
marginBottom: 16,
|
|
452
|
+
},
|
|
453
|
+
ussdTitle: {
|
|
454
|
+
fontSize: 14, // text-sm
|
|
455
|
+
color: "#1E3A8A", // text-indigo-900
|
|
456
|
+
fontWeight: "500", // font-medium
|
|
457
|
+
marginBottom: 12, // mb-3
|
|
458
|
+
},
|
|
459
|
+
ussdCodeContainer: {
|
|
460
|
+
backgroundColor: "white", // bg-white
|
|
461
|
+
borderRadius: 12, // rounded-xl
|
|
462
|
+
padding: 16, // p-4
|
|
463
|
+
marginBottom: 12, // mb-3
|
|
464
|
+
},
|
|
465
|
+
ussdCodeRow: {
|
|
466
|
+
flexDirection: "row",
|
|
467
|
+
justifyContent: "space-between",
|
|
468
|
+
alignItems: "center",
|
|
469
|
+
},
|
|
470
|
+
ussdCodeText: {
|
|
471
|
+
fontFamily: "monospace", // font-mono
|
|
472
|
+
fontSize: 16, // text-base
|
|
473
|
+
fontWeight: "700", // font-bold
|
|
474
|
+
color: "#111827", // text-gray-900
|
|
475
|
+
flex: 1,
|
|
476
|
+
},
|
|
477
|
+
ussdCopyBtn: {
|
|
478
|
+
padding: 8, // p-2
|
|
479
|
+
},
|
|
480
|
+
ussdStepsLabel: {
|
|
481
|
+
fontSize: 14, // text-sm
|
|
482
|
+
color: "#1E3A8A", // text-indigo-900
|
|
483
|
+
fontWeight: "600", // font-semibold
|
|
484
|
+
marginBottom: 8, // mb-2
|
|
485
|
+
},
|
|
486
|
+
ussdStepsText: {
|
|
487
|
+
fontSize: 14, // text-sm
|
|
488
|
+
color: "#1E40AF", // text-indigo-800
|
|
489
|
+
},
|
|
490
|
+
payButton: {
|
|
491
|
+
width: "100%",
|
|
492
|
+
paddingVertical: 16, // py-4
|
|
493
|
+
borderRadius: 12, // rounded-xl
|
|
494
|
+
marginTop: 8, // mt-2
|
|
495
|
+
alignItems: "center",
|
|
496
|
+
},
|
|
497
|
+
payButtonActive: {
|
|
498
|
+
backgroundColor: "#4F46E5", // bg-indigo-600
|
|
499
|
+
},
|
|
500
|
+
payButtonDisabled: {
|
|
501
|
+
backgroundColor: "#D1D5DB", // bg-gray-300
|
|
502
|
+
},
|
|
503
|
+
payButtonText: {
|
|
504
|
+
color: "white",
|
|
505
|
+
fontWeight: "600",
|
|
506
|
+
fontSize: 18, // text-lg
|
|
507
|
+
textAlign: "center",
|
|
508
|
+
},
|
|
509
|
+
footer: {
|
|
510
|
+
flexDirection: "row",
|
|
511
|
+
justifyContent: "center",
|
|
512
|
+
alignItems: "center",
|
|
513
|
+
marginTop: 8, // mt-2
|
|
514
|
+
gap: 8, // supported in RN 0.71+, acts like Tailwind gap-2
|
|
515
|
+
},
|
|
516
|
+
footerText: {
|
|
517
|
+
fontSize: 14, // text-sm
|
|
518
|
+
color: "#6B7280", // text-gray-500
|
|
519
|
+
},
|
|
520
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
type OTPInputProps = {
|
|
3
|
+
length?: number;
|
|
4
|
+
onComplete?: (otp: string) => void;
|
|
5
|
+
onChangeOTP?: (otp: string) => void;
|
|
6
|
+
autoFocus?: boolean;
|
|
7
|
+
secureTextEntry?: boolean;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
inputStyle?: object;
|
|
10
|
+
containerStyle?: object;
|
|
11
|
+
focusedInputStyle?: object;
|
|
12
|
+
};
|
|
13
|
+
export type OTPInputRef = {
|
|
14
|
+
clear: () => void;
|
|
15
|
+
};
|
|
16
|
+
export default function OTPInput({ length, onComplete, onChangeOTP, autoFocus, secureTextEntry, disabled, inputStyle, containerStyle, focusedInputStyle, }: OTPInputProps): React.JSX.Element;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
|
+
import { StyleSheet, TextInput, View, } from "react-native";
|
|
3
|
+
export default function OTPInput({ length = 6, onComplete, onChangeOTP, autoFocus = true, secureTextEntry = false, disabled = false, inputStyle, containerStyle, focusedInputStyle, }) {
|
|
4
|
+
const [otp, setOtp] = useState(Array(length).fill(""));
|
|
5
|
+
const [focusedIndex, setFocusedIndex] = useState(autoFocus ? 0 : null);
|
|
6
|
+
const ref = React.useRef(null);
|
|
7
|
+
const inputRefs = React.useRef([]);
|
|
8
|
+
// Focus first input on mount if autoFocus is true
|
|
9
|
+
useEffect(() => {
|
|
10
|
+
if (autoFocus && inputRefs.current[0]) {
|
|
11
|
+
inputRefs.current[0].focus();
|
|
12
|
+
}
|
|
13
|
+
}, [autoFocus]);
|
|
14
|
+
// Notify parent of OTP changes
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const otpString = otp.join("");
|
|
17
|
+
onChangeOTP === null || onChangeOTP === void 0 ? void 0 : onChangeOTP(otpString);
|
|
18
|
+
// Call onComplete when all digits are filled
|
|
19
|
+
if (otpString.length === length && !otpString.includes("")) {
|
|
20
|
+
onComplete === null || onComplete === void 0 ? void 0 : onComplete(otpString);
|
|
21
|
+
}
|
|
22
|
+
}, [otp, length, onComplete, onChangeOTP]);
|
|
23
|
+
const handleChangeText = (text, index) => {
|
|
24
|
+
var _a, _b;
|
|
25
|
+
// Only allow numbers
|
|
26
|
+
const sanitizedText = text.replace(/[^0-9]/g, "");
|
|
27
|
+
if (sanitizedText === "") {
|
|
28
|
+
// Handle delete
|
|
29
|
+
const newOtp = [...otp];
|
|
30
|
+
newOtp[index] = "";
|
|
31
|
+
setOtp(newOtp);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
// Handle paste (multiple characters)
|
|
35
|
+
if (sanitizedText.length > 1) {
|
|
36
|
+
const pastedDigits = sanitizedText.slice(0, length).split("");
|
|
37
|
+
const newOtp = [...otp];
|
|
38
|
+
pastedDigits.forEach((digit, i) => {
|
|
39
|
+
if (index + i < length) {
|
|
40
|
+
newOtp[index + i] = digit;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
setOtp(newOtp);
|
|
44
|
+
// Focus the next empty input or the last input
|
|
45
|
+
const nextIndex = Math.min(index + pastedDigits.length, length - 1);
|
|
46
|
+
(_a = inputRefs.current[nextIndex]) === null || _a === void 0 ? void 0 : _a.focus();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Handle single digit input
|
|
50
|
+
const newOtp = [...otp];
|
|
51
|
+
newOtp[index] = sanitizedText;
|
|
52
|
+
setOtp(newOtp);
|
|
53
|
+
// Auto-focus next input
|
|
54
|
+
if (index < length - 1 && sanitizedText !== "") {
|
|
55
|
+
(_b = inputRefs.current[index + 1]) === null || _b === void 0 ? void 0 : _b.focus();
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
const handleKeyPress = (e, index) => {
|
|
59
|
+
var _a;
|
|
60
|
+
if (e.nativeEvent.key === "Backspace") {
|
|
61
|
+
if (otp[index] === "") {
|
|
62
|
+
// If current input is empty, focus previous input
|
|
63
|
+
if (index > 0) {
|
|
64
|
+
(_a = inputRefs.current[index - 1]) === null || _a === void 0 ? void 0 : _a.focus();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// Clear current input
|
|
69
|
+
const newOtp = [...otp];
|
|
70
|
+
newOtp[index] = "";
|
|
71
|
+
setOtp(newOtp);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
const handleFocus = (index) => {
|
|
76
|
+
setFocusedIndex(index);
|
|
77
|
+
};
|
|
78
|
+
const handleBlur = () => {
|
|
79
|
+
setFocusedIndex(null);
|
|
80
|
+
};
|
|
81
|
+
// Method to clear OTP (can be called from parent component)
|
|
82
|
+
const clearOTP = () => {
|
|
83
|
+
var _a;
|
|
84
|
+
setOtp(Array(length).fill(""));
|
|
85
|
+
(_a = inputRefs.current[0]) === null || _a === void 0 ? void 0 : _a.focus();
|
|
86
|
+
};
|
|
87
|
+
// Expose clearOTP method
|
|
88
|
+
React.useImperativeHandle(ref, () => ({
|
|
89
|
+
clear: clearOTP,
|
|
90
|
+
}));
|
|
91
|
+
return (<View style={[styles.container, containerStyle]}>
|
|
92
|
+
{Array(length)
|
|
93
|
+
.fill(0)
|
|
94
|
+
.map((_, index) => (<TextInput key={index} ref={(ref) => {
|
|
95
|
+
inputRefs.current[index] = ref;
|
|
96
|
+
}} style={[
|
|
97
|
+
styles.input,
|
|
98
|
+
inputStyle,
|
|
99
|
+
focusedIndex === index && styles.focusedInput,
|
|
100
|
+
focusedIndex === index && focusedInputStyle,
|
|
101
|
+
disabled && styles.disabledInput,
|
|
102
|
+
]} value={otp[index]} onChangeText={(text) => handleChangeText(text, index)} onKeyPress={(e) => handleKeyPress(e, index)} onFocus={() => handleFocus(index)} onBlur={handleBlur} keyboardType="number-pad" maxLength={1} selectTextOnFocus editable={!disabled} secureTextEntry={secureTextEntry} textContentType="oneTimeCode" autoComplete="sms-otp"/>))}
|
|
103
|
+
</View>);
|
|
104
|
+
}
|
|
105
|
+
const styles = StyleSheet.create({
|
|
106
|
+
container: {
|
|
107
|
+
flexDirection: "row",
|
|
108
|
+
justifyContent: "center",
|
|
109
|
+
alignItems: "center",
|
|
110
|
+
gap: 2,
|
|
111
|
+
},
|
|
112
|
+
input: {
|
|
113
|
+
width: 40,
|
|
114
|
+
height: 50,
|
|
115
|
+
borderWidth: 1,
|
|
116
|
+
borderColor: "#D1D5DB",
|
|
117
|
+
borderRadius: 10,
|
|
118
|
+
textAlign: "center",
|
|
119
|
+
fontSize: 24,
|
|
120
|
+
fontWeight: "400",
|
|
121
|
+
backgroundColor: "#FFFFFF",
|
|
122
|
+
color: "#111827",
|
|
123
|
+
},
|
|
124
|
+
focusedInput: {
|
|
125
|
+
borderColor: "#3B82F6",
|
|
126
|
+
borderWidth: 2,
|
|
127
|
+
},
|
|
128
|
+
disabledInput: {
|
|
129
|
+
backgroundColor: "#F3F4F6",
|
|
130
|
+
color: "#9CA3AF",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type PaymentMethod = {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
icon: any;
|
|
5
|
+
};
|
|
6
|
+
type PaymentMethodSelectorProps = {
|
|
7
|
+
methods: PaymentMethod[];
|
|
8
|
+
selectedMethod: string;
|
|
9
|
+
onSelectMethod: (methodId: string) => void;
|
|
10
|
+
};
|
|
11
|
+
export default function PaymentMethodSelector({ methods, selectedMethod, onSelectMethod, }: PaymentMethodSelectorProps): import("react").JSX.Element;
|
|
12
|
+
export {};
|