fairsure-checkout-sdk 1.0.0 → 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.
@@ -193,6 +193,14 @@ export function usePaymentCheckout(amount, requestId, customerEmail, customerNam
193
193
  maximumFractionDigits: 2,
194
194
  });
195
195
  };
196
+ const convertExpiryForAPI = (expiry) => {
197
+ const clean = expiry.replace("/", ""); // 1326
198
+ if (clean.length !== 4)
199
+ return clean;
200
+ const mm = clean.slice(0, 2);
201
+ const yy = clean.slice(2, 4);
202
+ return `${yy}${mm}`; // 2613
203
+ };
196
204
  const formatCardNumber = (value) => {
197
205
  const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, "");
198
206
  const matches = v.match(/\d{4,16}/g);
@@ -259,11 +267,12 @@ export function usePaymentCheckout(amount, requestId, customerEmail, customerNam
259
267
  };
260
268
  const handleCardPayment = async () => {
261
269
  var _a, _b;
270
+ const formattedExpiry = convertExpiryForAPI(expiryDate.replace("/", ""));
262
271
  setLoading(true);
263
272
  try {
264
273
  const payload = {
265
274
  pan: cardNumber.replace(/\s/g, ""),
266
- expiryDate: expiryDate.replace("/", ""),
275
+ expiryDate: formattedExpiry,
267
276
  cvv,
268
277
  pin,
269
278
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fairsure-checkout-sdk",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "peerDependencies": {
@@ -225,6 +225,17 @@ export function usePaymentCheckout(
225
225
  });
226
226
  };
227
227
 
228
+ const convertExpiryForAPI = (expiry: string) => {
229
+ const clean = expiry.replace("/", ""); // 1326
230
+
231
+ if (clean.length !== 4) return clean;
232
+
233
+ const mm = clean.slice(0, 2);
234
+ const yy = clean.slice(2, 4);
235
+
236
+ return `${yy}${mm}`; // 2613
237
+ };
238
+
228
239
  const formatCardNumber = (value: string) => {
229
240
  const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, "");
230
241
  const matches = v.match(/\d{4,16}/g);
@@ -243,6 +254,7 @@ export function usePaymentCheckout(
243
254
  }
244
255
  return v;
245
256
  };
257
+
246
258
  const completeCard = async (data: string) => {
247
259
  setLoading(true);
248
260
  try {
@@ -291,12 +303,14 @@ export function usePaymentCheckout(
291
303
  }
292
304
  }
293
305
  };
306
+
294
307
  const handleCardPayment = async () => {
308
+ const formattedExpiry = convertExpiryForAPI(expiryDate.replace("/", ""));
295
309
  setLoading(true);
296
310
  try {
297
311
  const payload = {
298
312
  pan: cardNumber.replace(/\s/g, ""),
299
- expiryDate: expiryDate.replace("/", ""),
313
+ expiryDate: formattedExpiry,
300
314
  cvv,
301
315
  pin,
302
316
  };
@@ -1,9 +0,0 @@
1
- import React from "react";
2
- interface PaystackCheckoutProps {
3
- amount: number;
4
- merchantName?: string;
5
- onClose: () => void;
6
- onSuccess?: (reference: string) => void;
7
- }
8
- export default function Checkout({ amount, merchantName, onClose, onSuccess, }: PaystackCheckoutProps): React.JSX.Element;
9
- export {};
@@ -1,520 +0,0 @@
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
- });
@@ -1,17 +0,0 @@
1
- import React, { ReactElement } from "react";
2
- interface FilterItem {
3
- key: string;
4
- render: () => ReactElement | null;
5
- }
6
- interface PurchaseModalProps {
7
- visible: boolean;
8
- loading?: boolean;
9
- title?: string;
10
- description?: string;
11
- filters: FilterItem[];
12
- onClose: () => void;
13
- onApply?: () => void;
14
- onReset?: () => void;
15
- }
16
- declare const PurchaseModal: React.FC<PurchaseModalProps>;
17
- export default PurchaseModal;
@@ -1,415 +0,0 @@
1
- // import { AntDesign } from "@expo/vector-icons";
2
- // import { useTheme } from "@react-navigation/core";
3
- // import React, { ReactElement, useEffect, useRef } from "react";
4
- // import {
5
- // Animated,
6
- // Easing,
7
- // FlatList,
8
- // Modal,
9
- // PanResponder,
10
- // Pressable,
11
- // StyleSheet,
12
- // TouchableOpacity,
13
- // View,
14
- // } from "react-native";
15
- // import { ThemedText } from "../themed-text";
16
- // import { Spinner } from "../ui/buttons/spinner";
17
- // interface FilterItem {
18
- // key: string;
19
- // render: () => ReactElement | null;
20
- // }
21
- // interface PurchaseModalProps {
22
- // visible: boolean;
23
- // loading?: boolean;
24
- // title?: string;
25
- // description?: string;
26
- // filters: FilterItem[];
27
- // onClose: () => void;
28
- // onApply?: () => void;
29
- // onReset?: () => void;
30
- // }
31
- // const PurchaseModal: React.FC<PurchaseModalProps> = ({
32
- // visible,
33
- // title = "Buy Airtime",
34
- // description,
35
- // filters,
36
- // loading,
37
- // onClose,
38
- // onApply,
39
- // onReset,
40
- // }) => {
41
- // const { dark } = useTheme();
42
- // const translateY = useRef(new Animated.Value(400)).current;
43
- // const backdropOpacity = useRef(new Animated.Value(0)).current;
44
- // const [internalVisible, setInternalVisible] = React.useState(visible);
45
- // useEffect(() => {
46
- // if (visible) {
47
- // setInternalVisible(true);
48
- // // 🔑 Reset position BEFORE animating
49
- // translateY.setValue(400);
50
- // backdropOpacity.setValue(0);
51
- // Animated.parallel([
52
- // Animated.timing(translateY, {
53
- // toValue: 0,
54
- // duration: 350,
55
- // easing: Easing.out(Easing.ease),
56
- // useNativeDriver: true,
57
- // }),
58
- // Animated.timing(backdropOpacity, {
59
- // toValue: 1,
60
- // duration: 250,
61
- // useNativeDriver: true,
62
- // }),
63
- // ]).start();
64
- // } else {
65
- // Animated.parallel([
66
- // Animated.timing(translateY, {
67
- // toValue: 400,
68
- // duration: 250,
69
- // easing: Easing.in(Easing.ease),
70
- // useNativeDriver: true,
71
- // }),
72
- // Animated.timing(backdropOpacity, {
73
- // toValue: 0,
74
- // duration: 200,
75
- // useNativeDriver: true,
76
- // }),
77
- // ]).start(() => {
78
- // setInternalVisible(false);
79
- // });
80
- // }
81
- // }, [visible]);
82
- // const DRAG_CLOSE_THRESHOLD = 100;
83
- // const panResponder = useRef(
84
- // PanResponder.create({
85
- // onMoveShouldSetPanResponder: (_, gestureState) =>
86
- // gestureState.dy > 5 || gestureState.dy < -5, // detect upward drag too
87
- // onPanResponderGrant: () => {
88
- // // set current position as offset
89
- // translateY.extractOffset();
90
- // },
91
- // onPanResponderMove: (_, gestureState) => {
92
- // translateY.setValue(gestureState.dy); // smooth follow
93
- // },
94
- // onPanResponderRelease: (_, gestureState) => {
95
- // translateY.flattenOffset(); // combine offset & value
96
- // if (gestureState.dy > DRAG_CLOSE_THRESHOLD || gestureState.vy > 1.2) {
97
- // // close sheet
98
- // Animated.timing(translateY, {
99
- // toValue: 400,
100
- // duration: 200,
101
- // easing: Easing.in(Easing.ease),
102
- // useNativeDriver: true,
103
- // }).start(() => onClose());
104
- // } else {
105
- // // snap back
106
- // Animated.spring(translateY, {
107
- // toValue: 0,
108
- // tension: 120,
109
- // friction: 12,
110
- // useNativeDriver: true,
111
- // }).start();
112
- // }
113
- // },
114
- // }),
115
- // ).current;
116
- // if (!internalVisible) return null;
117
- // return (
118
- // <Modal transparent visible animationType="none">
119
- // {/* Backdrop */}
120
- // <Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]}>
121
- // <Pressable style={StyleSheet.absoluteFill} onPress={onClose} />
122
- // </Animated.View>
123
- // {/* Bottom Sheet */}
124
- // <Animated.View
125
- // {...panResponder.panHandlers}
126
- // style={[
127
- // styles.bottomSheet,
128
- // {
129
- // backgroundColor: dark ? "#121212" : "#fff",
130
- // transform: [{ translateY }],
131
- // },
132
- // ]}
133
- // >
134
- // {/* Handle */}
135
- // <TouchableOpacity style={styles.handle} onPress={onClose} />
136
- // {/* Header */}
137
- // <View style={styles.header}>
138
- // <ThemedText style={styles.title}>{title}</ThemedText>
139
- // <TouchableOpacity onPress={onClose}>
140
- // <AntDesign
141
- // name="close-circle"
142
- // size={18}
143
- // color={dark ? "#fff" : "#333740"}
144
- // />
145
- // </TouchableOpacity>
146
- // </View>
147
- // <ThemedText className="text-sm text-gray-500 mb-4">
148
- // {description}
149
- // </ThemedText>
150
- // {/* Content */}
151
- // <FlatList
152
- // data={filters}
153
- // keyExtractor={(item) => item.key}
154
- // showsVerticalScrollIndicator={false}
155
- // contentContainerStyle={{ paddingBottom: 120, height: "100%" }}
156
- // renderItem={({ item }) => item.render()}
157
- // />
158
- // {/* Actions */}
159
- // <View style={styles.actions}>
160
- // <TouchableOpacity onPress={onReset} style={styles.resetButton}>
161
- // <ThemedText>Cancel</ThemedText>
162
- // </TouchableOpacity>
163
- // <TouchableOpacity
164
- // onPress={onApply}
165
- // style={styles.applyButton}
166
- // className="flex flex-row justify-center items-center gap-2"
167
- // >
168
- // <ThemedText style={{ color: "#fff" }}>Proceed</ThemedText>
169
- // {loading && <Spinner size={18} color="#fff" />}
170
- // </TouchableOpacity>
171
- // </View>
172
- // </Animated.View>
173
- // </Modal>
174
- // );
175
- // };
176
- // export default PurchaseModal;
177
- // const styles = StyleSheet.create({
178
- // backdrop: {
179
- // ...StyleSheet.absoluteFillObject,
180
- // backgroundColor: "rgba(0,0,0,0.4)",
181
- // },
182
- // bottomSheet: {
183
- // position: "absolute",
184
- // bottom: 0,
185
- // width: "100%",
186
- // height: "80%",
187
- // paddingHorizontal: 10,
188
- // paddingVertical: 16,
189
- // borderTopLeftRadius: 24,
190
- // borderTopRightRadius: 24,
191
- // },
192
- // handle: {
193
- // width: 40,
194
- // height: 5,
195
- // borderRadius: 10,
196
- // backgroundColor: "#ccc",
197
- // alignSelf: "center",
198
- // marginBottom: 12,
199
- // },
200
- // header: {
201
- // flexDirection: "row",
202
- // justifyContent: "space-between",
203
- // alignItems: "center",
204
- // paddingHorizontal: 16,
205
- // marginBottom: 12,
206
- // },
207
- // title: {
208
- // fontFamily: "PoppinsSemiBold",
209
- // fontSize: 20,
210
- // },
211
- // actions: {
212
- // position: "absolute",
213
- // bottom: 24,
214
- // left: 16,
215
- // right: 16,
216
- // flexDirection: "row",
217
- // gap: 12,
218
- // },
219
- // resetButton: {
220
- // flex: 1,
221
- // borderWidth: 1,
222
- // borderColor: "#9ca3af",
223
- // backgroundColor: "#fff",
224
- // paddingVertical: 10,
225
- // borderRadius: 10,
226
- // alignItems: "center",
227
- // },
228
- // applyButton: {
229
- // flex: 1,
230
- // paddingVertical: 10,
231
- // borderRadius: 10,
232
- // alignItems: "center",
233
- // backgroundColor: "#027DFF",
234
- // },
235
- // });
236
- import { AntDesign } from "@expo/vector-icons";
237
- import { useTheme } from "@react-navigation/core";
238
- import React, { useEffect, useRef, useState } from "react";
239
- import { Animated, Easing, FlatList, Modal, PanResponder, Pressable, StyleSheet, TouchableOpacity, View, } from "react-native";
240
- import { ThemedText } from "../themed-text";
241
- import { Spinner } from "../ui/buttons/spinner";
242
- const PurchaseModal = ({ visible, title = "Buy Airtime", description, filters, loading, onClose, onApply, onReset, }) => {
243
- const { dark } = useTheme();
244
- const translateY = useRef(new Animated.Value(400)).current;
245
- const backdropOpacity = useRef(new Animated.Value(0)).current;
246
- const [internalVisible, setInternalVisible] = useState(visible);
247
- /* ---------------- Animation ---------------- */
248
- useEffect(() => {
249
- if (visible) {
250
- setInternalVisible(true);
251
- translateY.setValue(400);
252
- backdropOpacity.setValue(0);
253
- Animated.parallel([
254
- Animated.timing(translateY, {
255
- toValue: 0,
256
- duration: 350,
257
- easing: Easing.out(Easing.ease),
258
- useNativeDriver: true,
259
- }),
260
- Animated.timing(backdropOpacity, {
261
- toValue: 1,
262
- duration: 250,
263
- useNativeDriver: true,
264
- }),
265
- ]).start();
266
- }
267
- else {
268
- Animated.parallel([
269
- Animated.timing(translateY, {
270
- toValue: 400,
271
- duration: 250,
272
- easing: Easing.in(Easing.ease),
273
- useNativeDriver: true,
274
- }),
275
- Animated.timing(backdropOpacity, {
276
- toValue: 0,
277
- duration: 200,
278
- useNativeDriver: true,
279
- }),
280
- ]).start(() => setInternalVisible(false));
281
- }
282
- }, [visible]);
283
- /* ---------------- PanResponder (Handle only) ---------------- */
284
- const panResponder = useRef(PanResponder.create({
285
- onMoveShouldSetPanResponder: (_, gesture) => Math.abs(gesture.dy) > 8,
286
- onPanResponderMove: (_, gesture) => {
287
- if (gesture.dy > 0) {
288
- translateY.setValue(gesture.dy);
289
- }
290
- },
291
- onPanResponderRelease: (_, gesture) => {
292
- if (gesture.dy > 120 || gesture.vy > 1.2) {
293
- Animated.timing(translateY, {
294
- toValue: 400,
295
- duration: 200,
296
- easing: Easing.in(Easing.ease),
297
- useNativeDriver: true,
298
- }).start(onClose);
299
- }
300
- else {
301
- Animated.spring(translateY, {
302
- toValue: 0,
303
- tension: 120,
304
- friction: 12,
305
- useNativeDriver: true,
306
- }).start();
307
- }
308
- },
309
- })).current;
310
- if (!internalVisible)
311
- return null;
312
- return (<Modal transparent visible animationType="none">
313
- {/* Backdrop */}
314
- <Animated.View style={[styles.backdrop, { opacity: backdropOpacity }]}>
315
- <Pressable style={StyleSheet.absoluteFill} onPress={onClose}/>
316
- </Animated.View>
317
-
318
- {/* Bottom Sheet */}
319
- <Animated.View style={[
320
- styles.bottomSheet,
321
- {
322
- backgroundColor: dark ? "#333740" : "#fff",
323
- transform: [{ translateY }],
324
- },
325
- ]}>
326
- {/* Handle (Drag here only) */}
327
- <TouchableOpacity {...panResponder.panHandlers} style={styles.handle}/>
328
-
329
- {/* Header */}
330
- <View style={styles.header}>
331
- <ThemedText style={styles.title}>{title}</ThemedText>
332
- <TouchableOpacity onPress={onClose}>
333
- <AntDesign name="close-circle" size={18} color={dark ? "#fff" : "#333740"}/>
334
- </TouchableOpacity>
335
- </View>
336
-
337
- {description && (<ThemedText className="text-sm text-gray-500 mb-4">
338
- {description}
339
- </ThemedText>)}
340
-
341
- {/* Content (NOW SCROLLABLE ✅) */}
342
- <FlatList data={filters} keyExtractor={(item) => item.key} showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 140 }} renderItem={({ item }) => item.render()}/>
343
-
344
- {/* Actions */}
345
- <View style={styles.actions}>
346
- <TouchableOpacity onPress={onReset} style={styles.resetButton}>
347
- <ThemedText>Cancel</ThemedText>
348
- </TouchableOpacity>
349
-
350
- <TouchableOpacity onPress={onApply} style={styles.applyButton} className="flex-row justify-center items-center gap-2">
351
- <ThemedText style={{ color: "#fff" }}>Proceed</ThemedText>
352
- {loading && <Spinner size={18} color="#fff"/>}
353
- </TouchableOpacity>
354
- </View>
355
- </Animated.View>
356
- </Modal>);
357
- };
358
- export default PurchaseModal;
359
- /* ---------------- Styles ---------------- */
360
- const styles = StyleSheet.create({
361
- backdrop: Object.assign(Object.assign({}, StyleSheet.absoluteFillObject), { backgroundColor: "rgba(0,0,0,0.4)" }),
362
- bottomSheet: {
363
- position: "absolute",
364
- bottom: 0,
365
- width: "100%",
366
- height: "80%",
367
- paddingHorizontal: 10,
368
- paddingVertical: 16,
369
- borderTopLeftRadius: 24,
370
- borderTopRightRadius: 24,
371
- },
372
- handle: {
373
- width: 40,
374
- height: 5,
375
- borderRadius: 10,
376
- backgroundColor: "#ccc",
377
- alignSelf: "center",
378
- marginBottom: 12,
379
- },
380
- header: {
381
- flexDirection: "row",
382
- justifyContent: "space-between",
383
- alignItems: "center",
384
- paddingHorizontal: 16,
385
- marginBottom: 12,
386
- },
387
- title: {
388
- fontFamily: "PoppinsSemiBold",
389
- fontSize: 20,
390
- },
391
- actions: {
392
- position: "absolute",
393
- bottom: 24,
394
- left: 16,
395
- right: 16,
396
- flexDirection: "row",
397
- gap: 12,
398
- },
399
- resetButton: {
400
- flex: 1,
401
- borderWidth: 1,
402
- borderColor: "#9ca3af",
403
- backgroundColor: "#fff",
404
- paddingVertical: 10,
405
- borderRadius: 10,
406
- alignItems: "center",
407
- },
408
- applyButton: {
409
- flex: 1,
410
- paddingVertical: 10,
411
- borderRadius: 10,
412
- alignItems: "center",
413
- backgroundColor: "#027DFF",
414
- },
415
- });