UNPKG

zips-react-native-sdk-test

Version:

Lightweight ZIPS Payment Gateway SDK for React Native - Complete payment solution with card payments, wallet payments (AfrMoney & ZApp), netbanking, and native UI design

339 lines (338 loc) 15 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useState, useRef, useEffect } from 'react'; import { View, Text, TouchableOpacity, StyleSheet, TextInput, Image, } from 'react-native'; import Toast from 'react-native-toast-message'; import { Button } from '../common/Button'; import { ShimmerButton } from '../common/Shimmer'; import { useNetBankingContext } from '../../context/NetBankingProvider'; import { useZipsContext } from '../../context/ZipsProvider'; import { useGetOtp, useVerifyTransaction, } from '../../tanstack-query/mutations'; export const OTPVerification = () => { const { selectedBank, setAccountDetails, accountDetails } = useNetBankingContext(); const { setCurrentStep, setOrder, transactionDetails, paymentDetails } = useZipsContext(); const verifyTransactionMutation = useVerifyTransaction(); const getOtpMutations = useGetOtp(); const [otp, setOtp] = useState(['', '', '', '', '', '']); const [resendTimer, setResendTimer] = useState(0); const [canResend, setCanResend] = useState(true); const inputRefs = useRef([]); const handleOTPChange = (value, index) => { var _a; // Only allow numeric input const numericValue = value.replace(/[^0-9]/g, ''); const newOtp = [...otp]; newOtp[index] = numericValue; setOtp(newOtp); // Auto-focus next input if value is entered if (numericValue && index < 5) { (_a = inputRefs.current[index + 1]) === null || _a === void 0 ? void 0 : _a.focus(); } }; const handleKeyPress = (e, index) => { var _a; const { key } = e.nativeEvent; if (key === 'Backspace') { const newOtp = [...otp]; if (otp[index]) { // If current field has value, clear it newOtp[index] = ''; setOtp(newOtp); } else if (index > 0) { // If current field is empty, move to previous field and clear it newOtp[index - 1] = ''; setOtp(newOtp); (_a = inputRefs.current[index - 1]) === null || _a === void 0 ? void 0 : _a.focus(); } } }; const handleVerify = async () => { const otpString = otp.join(''); if (otpString.length === 6) { try { await verifyTransactionMutation.mutateAsync({ mandateReference: accountDetails === null || accountDetails === void 0 ? void 0 : accountDetails.mandateReference, customerChallenge: otpString, customerAccount: accountDetails === null || accountDetails === void 0 ? void 0 : accountDetails.accountNumber, amount: (accountDetails === null || accountDetails === void 0 ? void 0 : accountDetails.amount) || 0, orderId: transactionDetails === null || transactionDetails === void 0 ? void 0 : transactionDetails.orderId, purpose: (transactionDetails === null || transactionDetails === void 0 ? void 0 : transactionDetails.orderName) || 'Transfer', }, { onError: (err) => { var _a, _b; console.error('OTP Verification Error:', JSON.stringify(err, null, 2)); Toast.show({ type: 'error', text1: 'Verification Failed', text2: ((_b = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) || (err === null || err === void 0 ? void 0 : err.message) || 'Failed to verify OTP. Please try again.', position: 'top', visibilityTime: 3000, }); }, onSuccess: (data) => { var _a, _b; console.log('OTP verified successfully:', data === null || data === void 0 ? void 0 : data.data); if (((_a = data === null || data === void 0 ? void 0 : data.data) === null || _a === void 0 ? void 0 : _a.status) === 'Success') { setOrder({ ...(_b = data === null || data === void 0 ? void 0 : data.data) === null || _b === void 0 ? void 0 : _b.data, }); } }, }); } catch (error) { console.error('Error verifying OTP:', error); } } }; const isOTPComplete = otp.every((digit) => digit.length === 1); useEffect(() => { var _a; // Focus first input when component mounts setOtp(['', '', '', '', '', '']); (_a = inputRefs.current[0]) === null || _a === void 0 ? void 0 : _a.focus(); }, []); // Timer effect for resend button useEffect(() => { let interval; if (resendTimer > 0) { interval = setInterval(() => { setResendTimer((prev) => { if (prev <= 1) { setCanResend(true); return 0; } return prev - 1; }); }, 1000); } return () => clearInterval(interval); }, [resendTimer]); const handleResendOTP = async () => { var _a; if (!canResend || getOtpMutations.isPending) return; setCanResend(false); setResendTimer(30); // 30 seconds cooldown setOtp(['', '', '', '', '', '']); (_a = inputRefs.current[0]) === null || _a === void 0 ? void 0 : _a.focus(); try { await getOtpMutations.mutateAsync({ customerAccount: accountDetails === null || accountDetails === void 0 ? void 0 : accountDetails.accountNumber, customerName: accountDetails === null || accountDetails === void 0 ? void 0 : accountDetails.accountName, amount: (paymentDetails === null || paymentDetails === void 0 ? void 0 : paymentDetails.amount) || 0, customerLocation: 'Gambia', purpose: (accountDetails === null || accountDetails === void 0 ? void 0 : accountDetails.purpose) || 'Transfer', }, { onError: (err) => { var _a, _b; console.error('OTP Resend Error:', JSON.stringify(err, null, 2)); Toast.show({ type: 'error', text1: 'Resend Failed', text2: ((_b = (_a = err === null || err === void 0 ? void 0 : err.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.message) || (err === null || err === void 0 ? void 0 : err.message) || 'Failed to resend OTP. Please try again.', position: 'top', visibilityTime: 3000, }); setCanResend(true); setResendTimer(0); }, onSuccess: (data) => { var _a; console.log('OTP resent successfully:', data === null || data === void 0 ? void 0 : data.data); if (((_a = data === null || data === void 0 ? void 0 : data.data) === null || _a === void 0 ? void 0 : _a.status) === 'Success') { setAccountDetails((prev) => { var _a; return ({ ...prev, ...(_a = data === null || data === void 0 ? void 0 : data.data) === null || _a === void 0 ? void 0 : _a.data, }); }); } }, }); } catch (error) { console.error('Error resending OTP:', error); setCanResend(true); setResendTimer(0); } }; return (_jsxs(View, { style: styles.container, children: [_jsxs(View, { style: styles.header, children: [_jsx(TouchableOpacity, { onPress: () => { setAccountDetails((prev) => ({ ...prev, accountName: '', accountNumber: '', })); setCurrentStep('bank-account-input'); }, style: styles.backButton, children: _jsx(Text, { style: styles.backButtonText, children: "\u2190 Back" }) }), _jsx(Text, { style: styles.title, children: "OTP Verification" }), _jsx(View, { style: styles.placeholder })] }), _jsxs(View, { style: styles.bankHeader, children: [_jsx(View, { style: styles.bankLogoContainer, children: _jsx(Image, { source: { uri: (selectedBank === null || selectedBank === void 0 ? void 0 : selectedBank.bankLogo) || (selectedBank === null || selectedBank === void 0 ? void 0 : selectedBank.logo) }, style: styles.bankLogo, resizeMode: "contain" }) }), _jsx(Text, { style: styles.bankName, children: (selectedBank === null || selectedBank === void 0 ? void 0 : selectedBank.bankName) || (selectedBank === null || selectedBank === void 0 ? void 0 : selectedBank.name) })] }), _jsxs(View, { style: styles.instructionsSection, children: [_jsx(Text, { style: styles.instructionText, children: "A verification pin has been sent to your phone or email" }), _jsx(Text, { style: styles.subInstructionText, children: "Please check your inbox to confirm the payment" })] }), _jsx(View, { style: styles.otpContainer, children: otp.map((digit, index) => (_jsx(TextInput, { ref: (ref) => (inputRefs.current[index] = ref), style: [styles.otpInput, digit ? styles.otpInputFilled : null], value: digit, onChangeText: (value) => handleOTPChange(value, index), onKeyPress: (e) => handleKeyPress(e, index), keyboardType: "numeric", maxLength: 1, textAlign: "center", selectTextOnFocus: true, autoCorrect: false, autoCapitalize: "none", blurOnSubmit: false }, index))) }), _jsx(View, { style: styles.actionSection, children: (verifyTransactionMutation === null || verifyTransactionMutation === void 0 ? void 0 : verifyTransactionMutation.isPending) ? (_jsx(ShimmerButton, {})) : (_jsx(Button, { title: "Confirm payment", onPress: handleVerify, disabled: !isOTPComplete, variant: "success", size: "large", style: styles.verifyButton })) }), _jsx(TouchableOpacity, { onPress: handleResendOTP, disabled: !canResend || getOtpMutations.isPending, style: [ styles.resendButton, (!canResend || getOtpMutations.isPending) && styles.resendButtonDisabled, ], children: getOtpMutations.isPending ? (_jsx(Text, { style: [styles.resendButtonText, styles.resendButtonTextDisabled], children: "Sending OTP..." })) : resendTimer > 0 ? (_jsxs(Text, { style: [styles.resendButtonText, styles.resendButtonTextDisabled], children: ["Resend OTP in ", resendTimer, "s"] })) : (_jsx(Text, { style: styles.resendButtonText, children: "Resend OTP" })) })] })); }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#FFFFFF', paddingHorizontal: 16, // Reduced from 20 paddingTop: 24, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24, }, backButton: { paddingVertical: 8, paddingHorizontal: 12, }, backButtonText: { fontSize: 16, color: '#2563eb', fontWeight: '600', }, title: { fontSize: 20, fontWeight: '700', color: '#1e293b', textAlign: 'center', }, placeholder: { width: 60, }, bankHeader: { alignItems: 'center', marginBottom: 32, }, bankLogoContainer: { width: 80, height: 80, borderRadius: 12, backgroundColor: '#F3F4F6', justifyContent: 'center', alignItems: 'center', marginBottom: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.1, shadowRadius: 4, elevation: 2, }, bankLogo: { width: 60, height: 60, }, bankName: { fontSize: 18, fontWeight: '600', color: '#1F2937', textAlign: 'center', }, accountDetailsSection: { backgroundColor: '#F8FAFC', borderRadius: 12, padding: 16, marginBottom: 24, borderWidth: 1, borderColor: '#E5E7EB', }, accountLabel: { fontSize: 14, fontWeight: '600', color: '#6B7280', marginBottom: 8, textAlign: 'center', }, accountInfoContainer: { alignItems: 'center', }, accountName: { fontSize: 18, fontWeight: '700', color: '#1F2937', marginBottom: 4, textAlign: 'center', }, accountNumber: { fontSize: 16, fontWeight: '500', color: '#6B7280', textAlign: 'center', letterSpacing: 0.5, }, instructionsSection: { marginBottom: 32, }, instructionText: { fontSize: 16, color: '#374151', textAlign: 'center', marginBottom: 8, lineHeight: 24, }, subInstructionText: { fontSize: 14, color: '#6B7280', textAlign: 'center', lineHeight: 20, }, otpContainer: { flexDirection: 'row', justifyContent: 'center', gap: 12, marginBottom: 24, }, otpInput: { width: 48, height: 56, borderWidth: 2, borderColor: '#D1D5DB', borderRadius: 8, fontSize: 24, fontWeight: '600', color: '#1F2937', backgroundColor: '#FFFFFF', }, otpInputFilled: { borderColor: '#007AFF', backgroundColor: '#F8FAFF', }, otpInputError: { borderColor: '#EF4444', }, errorText: { color: '#EF4444', fontSize: 14, textAlign: 'center', marginBottom: 16, }, actionSection: { marginBottom: 24, }, verifyButton: { backgroundColor: '#10B981', }, resendButton: { alignItems: 'center', paddingVertical: 12, marginBottom: 16, }, resendButtonDisabled: { opacity: 0.5, }, resendButtonText: { fontSize: 16, color: '#007AFF', textDecorationLine: 'underline', }, resendButtonTextDisabled: { color: '#9CA3AF', textDecorationLine: 'none', }, });