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
JavaScript
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',
},
});