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.
- package/dist/components/usePaymentCheckout.js +10 -1
- package/package.json +1 -1
- package/src/components/usePaymentCheckout.ts +15 -1
- package/dist/components/Checkout.d.ts +0 -9
- package/dist/components/Checkout.js +0 -520
- package/dist/components/PurchaseModal.d.ts +0 -17
- package/dist/components/PurchaseModal.js +0 -415
|
@@ -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:
|
|
275
|
+
expiryDate: formattedExpiry,
|
|
267
276
|
cvv,
|
|
268
277
|
pin,
|
|
269
278
|
};
|
package/package.json
CHANGED
|
@@ -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:
|
|
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
|
-
});
|