Files
circle-collect/src/screens/OtpVarificationScreen.tsx
T
2025-09-19 13:43:33 +05:30

269 lines
8.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { RouteProp, useNavigation, useRoute } from "@react-navigation/native";
import axios from "axios";
import React, { useEffect, useRef, useState } from "react";
import {
View,
Text,
StyleSheet,
TextInput,
Pressable,
Animated,
Platform,
NativeSyntheticEvent,
TextInputKeyPressEventData,
KeyboardAvoidingView,
StatusBar,
Alert,
ToastAndroid,
} from "react-native";
import { RootStackParamList } from "../navigation/AppNavigator";
import * as Keychain from "react-native-keychain";
import { BASE_URL, LOGIN_URL, VARIFY_OTP_URL } from "../utils/ApiUrls";
import { StackNavigationProp } from "@react-navigation/stack";
import colors from "../components/colors";
import { useDispatch } from "react-redux";
import { setUserData } from "../redux/userSlice";
import { fetchUserProfile } from "../utils/HelperFunction";
const theme = {
primary: "#111111",
accent: colors.primary,
background: "#F9F9F9",
text: "#1A1A1A",
subText: "#6B6B6B",
outline: "#E5E5E5",
};
const CELL_COUNT = 6;
type OtpVerifyRouteProp = RouteProp<RootStackParamList, "OtpVarify">;
export default function OtpVerificationScreen() {
const [code, setCode] = useState<string[]>(Array(CELL_COUNT).fill(""));
const [timer, setTimer] = useState(30);
const [isResending, setIsResending] = useState(false);
const inputs = useRef<Array<TextInput | null>>([]);
const route = useRoute<OtpVerifyRouteProp>();
const { mobile } = route.params;
const navigation = useNavigation<StackNavigationProp<RootStackParamList>>();
// animations
const fade = useRef(new Animated.Value(0)).current;
const slide = useRef(new Animated.Value(24)).current;
const ctaScale = useRef(new Animated.Value(1)).current;
useEffect(() => {
Animated.parallel([
Animated.timing(fade, { toValue: 1, duration: 600, useNativeDriver: true }),
Animated.timing(slide, { toValue: 0, duration: 600, useNativeDriver: true }),
]).start();
}, [fade, slide]);
// Countdown
useEffect(() => {
if (timer <= 0) return;
const interval = setInterval(() => {
setTimer((prev) => prev - 1);
}, 1000);
return () => clearInterval(interval);
}, [timer]);
const saveToken = async (token: string) => {
try {
await Keychain.setGenericPassword("auth", token, {
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED,
});
console.log("✅ Token saved securely");
} catch (error) {
console.error("❌ Error saving token:", error);
}
};
const onSubmit = async (otp: string, mobile: string) => {
try {
const payload = { mobile, otp };
const res = await axios.post(
VARIFY_OTP_URL,
payload,
{ headers: { "Content-Type": "application/json", Accept: "*/*" } }
);
console.log("✅ OTP Verified:", res.data);
if (res.data.statusCode === 200) {
saveToken(res.data.data);
fetchUserProfile()
navigation.navigate('Home')
ToastAndroid.show("OTP Verified Successfully!", ToastAndroid.SHORT);
}
} catch (err: any) {
console.error("❌ OTP Verification Error:", err?.response?.data || err.message);
ToastAndroid.show(err.message || "Invalid OTP. Please try again.", ToastAndroid.SHORT);
}
};
const resendOtp = async () => {
try {
setIsResending(true);
setTimer(30); // reset timer
const res = await axios.get(
LOGIN_URL + mobile,
{ headers: { "Content-Type": "application/json", Accept: "*/*" } }
);
console.log("📩 OTP Resent:", res);
setCode(Array(CELL_COUNT).fill(""));
inputs.current[0]?.focus();
ToastAndroid.show("OTP Sent", ToastAndroid.SHORT);
} catch (err: any) {
console.error("❌ Resend OTP Error:", err?.response?.data || err.message);
ToastAndroid.show("Failed to resend OTP. Try again later.", ToastAndroid.SHORT);
} finally {
setIsResending(false);
}
};
const submitIfComplete = (arr: string[]) => {
const joined = arr.join("");
if (joined.length === CELL_COUNT) {
onSubmit(joined, mobile);
}
};
const updateIndex = (idx: number, val: string) => {
const v = val.replace(/[^0-9]/g, "");
const next = [...code];
next[idx] = v.slice(-1);
setCode(next);
if (v && idx < CELL_COUNT - 1) inputs.current[idx + 1]?.focus();
submitIfComplete(next);
};
const handleFirstCellChange = (val: string) => {
const digits = val.replace(/\D/g, "");
if (digits.length <= 1) {
updateIndex(0, digits);
return;
}
const next = Array(CELL_COUNT)
.fill("")
.map((_, i) => digits[i] ?? "");
setCode(next);
const firstEmpty = next.findIndex((c) => !c);
if (firstEmpty >= 0) inputs.current[firstEmpty]?.focus();
else inputs.current[CELL_COUNT - 1]?.blur();
submitIfComplete(next);
};
const handleKeyPress = (idx: number, e: NativeSyntheticEvent<TextInputKeyPressEventData>) => {
if (e.nativeEvent.key === "Backspace" && !code[idx] && idx > 0) {
inputs.current[idx - 1]?.focus();
}
};
return (
<KeyboardAvoidingView
style={styles.container}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<StatusBar barStyle="dark-content" backgroundColor={theme.background} />
{/* Top Section */}
<View style={styles.topSection}>
<Text style={styles.title}>Verify Code 🔒</Text>
<Text style={styles.subtitle}>
We’ve sent a 6-digit verification code to {mobile}.
</Text>
<Animated.View
style={[styles.otpRow, { opacity: fade, transform: [{ translateY: slide }] }]}
>
{Array.from({ length: CELL_COUNT }).map((_, i) => (
<TextInput
key={`otp-${i}`}
ref={(r) => {
inputs.current[i] = r;
}}
style={[styles.cell, code[i] ? styles.cellFilled : null]}
keyboardType="number-pad"
maxLength={1}
value={code[i]}
onChangeText={(t) => (i === 0 ? handleFirstCellChange(t) : updateIndex(i, t))}
onKeyPress={(e) => handleKeyPress(i, e)}
placeholder="•"
placeholderTextColor="#BDBDBD"
textContentType="oneTimeCode"
/>
))}
</Animated.View>
</View>
{/* Bottom Section */}
<Animated.View style={[styles.bottomSection, { transform: [{ scale: ctaScale }] }]}>
<Pressable
onPressIn={() =>
Animated.spring(ctaScale, { toValue: 0.97, useNativeDriver: true }).start()
}
onPressOut={() =>
Animated.spring(ctaScale, { toValue: 1, useNativeDriver: true }).start()
}
onPress={() => onSubmit(code.join(""), mobile)}
style={styles.button}
>
<Text style={styles.buttonText}>Continue</Text>
</Pressable>
{timer > 0 ? (
<Text style={styles.footerText}>Resend code in 00:{timer.toString().padStart(2, "0")}</Text>
) : (
<Pressable onPress={resendOtp} disabled={isResending}>
<Text style={[styles.footerText, { color: theme.accent }]}>
{isResending ? "Resending..." : "Resend Code"}
</Text>
</Pressable>
)}
</Animated.View>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: theme.background, padding: 20 },
topSection: { flex: 1, justifyContent: "center" },
title: { fontSize: 26, fontWeight: "800", color: theme.text, marginBottom: 8 },
subtitle: {
fontSize: 15,
color: theme.subText,
marginBottom: 24,
lineHeight: 22,
},
otpRow: { flexDirection: "row", justifyContent: "space-between" },
cell: {
width: 50,
height: 58,
borderRadius: 12,
borderWidth: 1,
borderColor: theme.outline,
backgroundColor: "#fff",
textAlign: "center",
fontSize: 20,
color: theme.text,
},
cellFilled: { borderColor: "#D1D1D1" },
bottomSection: { marginBottom: 30 },
button: {
backgroundColor: colors.primary,
paddingVertical: 18,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
},
buttonText: { color: "#000", fontWeight: "700", fontSize: 16 },
footerText: {
textAlign: "center",
color: theme.subText,
fontSize: 13,
marginTop: 14,
},
});