Amba-Agent-App/app/(root)/auth/otp.tsx
2026-01-16 00:22:35 +03:00

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>
);
}