275 lines
8.5 KiB
TypeScript
275 lines
8.5 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react";
|
|
import {
|
|
View,
|
|
ScrollView,
|
|
Keyboard,
|
|
Platform,
|
|
KeyboardAvoidingView,
|
|
TouchableWithoutFeedback,
|
|
type ScrollView as ScrollViewType,
|
|
} from "react-native";
|
|
import { Button } from "~/components/ui/button";
|
|
import { Input } from "~/components/ui/input";
|
|
import { Label } from "~/components/ui/label";
|
|
import { Text } from "~/components/ui/text";
|
|
import { router } from "expo-router";
|
|
import { usePhoneAuth, useAuthState } from "~/lib/hooks/useAuth";
|
|
import { ROUTES } from "~/lib/routes";
|
|
import BackButton from "~/components/ui/backButton";
|
|
import { AuthService } from "~/lib/services/authServices";
|
|
import { useGlobalLoading } from "~/lib/hooks/useGlobalLoading";
|
|
import ScreenWrapper from "~/components/ui/ScreenWrapper";
|
|
import ModalToast from "~/components/ui/toast";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
export default function OTP() {
|
|
const [otpCode, setOtpCode] = useState("");
|
|
const [countdown, setCountdown] = useState(60);
|
|
const [otpVerified, setOtpVerified] = useState(false);
|
|
const { t } = useTranslation();
|
|
|
|
const { verifyOTP, sendOTP, loading, error } = usePhoneAuth();
|
|
const { user } = useAuthState();
|
|
const { showLoader, hideLoader, isGlobalLoading } = useGlobalLoading();
|
|
const loaderActiveRef = useRef(false);
|
|
const scrollViewRef = useRef<ScrollViewType>(null);
|
|
|
|
const [toastVisible, setToastVisible] = useState(false);
|
|
const [toastTitle, setToastTitle] = useState("");
|
|
const [toastDescription, setToastDescription] = useState<string | undefined>(
|
|
undefined
|
|
);
|
|
const [toastVariant, setToastVariant] = useState<
|
|
"success" | "error" | "warning" | "info"
|
|
>("info");
|
|
const toastTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const showToast = (
|
|
title: string,
|
|
description?: string,
|
|
variant: "success" | "error" | "warning" | "info" = "info"
|
|
) => {
|
|
if (toastTimeoutRef.current) {
|
|
clearTimeout(toastTimeoutRef.current);
|
|
}
|
|
|
|
setToastTitle(title);
|
|
setToastDescription(description);
|
|
setToastVariant(variant);
|
|
setToastVisible(true);
|
|
|
|
toastTimeoutRef.current = setTimeout(() => {
|
|
setToastVisible(false);
|
|
toastTimeoutRef.current = null;
|
|
}, 2500);
|
|
};
|
|
|
|
const handleVerifyOTP = async () => {
|
|
Keyboard.dismiss();
|
|
|
|
if (!otpCode.trim() || otpCode.length !== 6) {
|
|
showToast(
|
|
t("otp.validationErrorTitle"),
|
|
t("otp.validationInvalidCode"),
|
|
"warning"
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
loaderActiveRef.current = true;
|
|
showLoader();
|
|
await verifyOTP(otpCode);
|
|
console.log("OTP VERIFIED successfully");
|
|
} catch (error) {
|
|
if (loaderActiveRef.current) {
|
|
hideLoader();
|
|
loaderActiveRef.current = false;
|
|
}
|
|
showToast(t("otp.toastErrorTitle"), t("otp.toastInvalidCode"), "error");
|
|
}
|
|
};
|
|
|
|
const handleResendCode = async () => {
|
|
Keyboard.dismiss();
|
|
|
|
// You'll need to store the phone number from the previous screen
|
|
// For now, we'll show a message
|
|
showToast(t("otp.toastInfoTitle"), t("otp.toastResendInfo"), "info");
|
|
};
|
|
|
|
// Countdown timer for resend button
|
|
useEffect(() => {
|
|
if (countdown > 0) {
|
|
const timer = setTimeout(() => setCountdown(countdown - 1), 1000);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [countdown]);
|
|
|
|
// Scroll back when keyboard is dismissed
|
|
useEffect(() => {
|
|
const keyboardDidHide = Keyboard.addListener(
|
|
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
|
|
() => {
|
|
scrollViewRef.current?.scrollTo({ y: 0, animated: true });
|
|
}
|
|
);
|
|
|
|
return () => {
|
|
keyboardDidHide.remove();
|
|
};
|
|
}, []);
|
|
|
|
// Show errors
|
|
useEffect(() => {
|
|
if (error) {
|
|
showToast(t("otp.toastVerificationErrorTitle"), error, "error");
|
|
}
|
|
}, [error]);
|
|
|
|
// Handle navigation after OTP verification
|
|
useEffect(() => {
|
|
const checkProfileAndNavigate = async () => {
|
|
if (user?.uid && !otpVerified) {
|
|
console.log("User authenticated, checking profile existence");
|
|
setOtpVerified(true); // Set flag before navigation
|
|
|
|
try {
|
|
const profileExists = await AuthService.checkUserProfileExists(
|
|
user.uid
|
|
);
|
|
console.log("Profile exists:", profileExists);
|
|
|
|
if (profileExists) {
|
|
console.log("User profile exists, redirecting to home");
|
|
router.replace(ROUTES.HOME);
|
|
} else {
|
|
console.log(
|
|
"User profile does not exist, redirecting to phone setup"
|
|
);
|
|
router.replace(ROUTES.PHONE_SETUP);
|
|
}
|
|
} catch (error) {
|
|
console.error("Error checking profile:", error);
|
|
// Default to phone setup if there's an error
|
|
router.replace(ROUTES.PHONE_SETUP);
|
|
} finally {
|
|
if (loaderActiveRef.current) {
|
|
hideLoader();
|
|
loaderActiveRef.current = false;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
checkProfileAndNavigate();
|
|
}, [user, otpVerified, hideLoader]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (toastTimeoutRef.current) {
|
|
clearTimeout(toastTimeoutRef.current);
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
// Note: We don't redirect authenticated users here since they need to complete setup
|
|
|
|
return (
|
|
<ScreenWrapper edges={[]}>
|
|
<KeyboardAvoidingView
|
|
behavior={Platform.OS === "ios" ? "padding" : "height"}
|
|
className="flex-1"
|
|
keyboardVerticalOffset={Platform.OS === "ios" ? 10 : 0}
|
|
>
|
|
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
|
<View className="flex-1 justify-between">
|
|
<View className="flex-1">
|
|
<BackButton
|
|
onPress={() => {
|
|
showToast(
|
|
t("otp.toastInfoTitle"),
|
|
t("otp.toastBackInfo"),
|
|
"info"
|
|
);
|
|
router.back();
|
|
}}
|
|
/>
|
|
<ScrollView
|
|
ref={scrollViewRef}
|
|
className="flex-1 px-5"
|
|
contentContainerStyle={{ paddingBottom: 20 }}
|
|
keyboardShouldPersistTaps="handled"
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
<View className="h-36" />
|
|
<Text className="text-3xl font-dmsans text-primary">
|
|
{t("otp.title")}
|
|
</Text>
|
|
<View className="h-4" />
|
|
<Text className="text-sm font-dmsans-light">
|
|
{t("otp.description")}
|
|
</Text>
|
|
<View className="h-4" />
|
|
<Label className="text-base font-dmsans-medium">
|
|
{t("otp.codeLabel")}
|
|
</Label>
|
|
<View className="h-2" />
|
|
<Input
|
|
placeholder={t("otp.codePlaceholder")}
|
|
value={otpCode}
|
|
onChangeText={setOtpCode}
|
|
keyboardType="numeric"
|
|
maxLength={6}
|
|
containerClassName="w-full"
|
|
borderClassName="border-[#D9DBE9] bg-white"
|
|
placeholderColor="#7E7E7E"
|
|
textClassName="text-[#000] text-sm"
|
|
/>
|
|
</ScrollView>
|
|
</View>
|
|
|
|
<View className="px-5 pb-6">
|
|
<Button
|
|
className="bg-primary rounded-[8px]"
|
|
onPress={handleVerifyOTP}
|
|
disabled={
|
|
loading ||
|
|
isGlobalLoading ||
|
|
!otpCode.trim() ||
|
|
otpCode.length !== 6
|
|
}
|
|
>
|
|
<Text className="font-dmsans text-white">
|
|
{loading || isGlobalLoading
|
|
? t("otp.verifyButtonLoading")
|
|
: t("otp.verifyButton")}
|
|
</Text>
|
|
</Button>
|
|
<View className="h-3" />
|
|
<Button
|
|
className="bg-secondary rounded-[8px]"
|
|
onPress={handleResendCode}
|
|
disabled={loading || countdown > 0 || isGlobalLoading}
|
|
>
|
|
<Text className="font-dmsans text-white">
|
|
{countdown > 0
|
|
? t("otp.resendButtonCountdown", { countdown })
|
|
: t("otp.resendButton")}
|
|
</Text>
|
|
</Button>
|
|
</View>
|
|
</View>
|
|
</TouchableWithoutFeedback>
|
|
</KeyboardAvoidingView>
|
|
|
|
<ModalToast
|
|
visible={toastVisible}
|
|
title={toastTitle}
|
|
description={toastDescription}
|
|
variant={toastVariant}
|
|
/>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|