1+ import { useState , useEffect , useRef } from "react" ;
2+ import {
3+ View ,
4+ Text ,
5+ TextInput ,
6+ TouchableOpacity ,
7+ KeyboardAvoidingView ,
8+ useColorScheme ,
9+ } from "react-native" ;
10+ import { useMutation , gql } from "@apollo/client" ;
11+ import { LOGIN_MUTATION } from '@/graphql/mutations/login.mutation' ;
12+ import { LOGIN_WITH_2FA } from '@/graphql/mutations/two-factor.mutation' ;
13+ import { Href , useLocalSearchParams , useRouter } from 'expo-router' ;
14+ import AsyncStorage from "@react-native-async-storage/async-storage" ;
15+ import { useToast } from 'react-native-toast-notifications' ;
16+ import { useTranslation } from 'react-i18next' ;
17+
18+
19+ const TwoFactorScreen = ( ) => {
20+ const [ input , setInput ] = useState < string [ ] > ( Array ( 6 ) . fill ( "" ) ) ;
21+ const [ userEmail , setuserEmail ] = useState < string > ( '' ) ;
22+ const [ error , setError ] = useState < string > ( "" ) ;
23+ const [ countdown , setCountdown ] = useState < number > ( 30 ) ;
24+ const [ isTimerActive , setIsTimerActive ] = useState < boolean > ( true ) ;
25+ const { t} = useTranslation ( ) ;
26+ const [ loading , setLoading ] = useState < boolean > ( false ) ;
27+ const [ resending , setResending ] = useState < boolean > ( false ) ;
28+ const colorScheme = useColorScheme ( ) ;
29+ const inputRefs = useRef < TextInput [ ] > ( [ ] ) ;
30+ const params = useLocalSearchParams < { redirect ?: string ; logout : string } > ( ) ;
31+ const router = useRouter ( ) ;
32+ const toast = useToast ( ) ;
33+
34+ useEffect ( ( ) => {
35+ const fetchUserEmail = async ( ) => {
36+ try {
37+ const email = await AsyncStorage . getItem ( "user_email" ) ;
38+ if ( email ) {
39+ setuserEmail ( email ) ;
40+ }
41+ } catch ( error ) {
42+ toast . show ( `Failed to fetch email from storage` , {
43+ type : 'danger' ,
44+ placement : 'top' ,
45+ duration : 4000 ,
46+ animationType : 'slide-in' ,
47+ } ) ;
48+ }
49+ } ;
50+
51+ fetchUserEmail ( ) ;
52+ } , [ ] ) ;
53+
54+ const resetTimer = ( ) => {
55+ setCountdown ( 30 ) ;
56+ setIsTimerActive ( true ) ;
57+ } ;
58+
59+ const [ loginWithTwoFactorAuthentication ] = useMutation ( LOGIN_WITH_2FA , {
60+ onCompleted : async ( data ) => {
61+ const response = data . loginWithTwoFactorAuthentication ;
62+ const token = response . token ;
63+ if ( response . user . role === 'trainee' ) {
64+ await AsyncStorage . setItem ( 'authToken' , token ) ;
65+
66+ while ( router . canGoBack ( ) ) {
67+ router . back ( ) ;
68+ }
69+
70+ params . redirect
71+ ? router . push ( `${ params . redirect } ` as Href < string | object > )
72+ : router . push ( '/dashboard' ) ;
73+ return ;
74+ } else {
75+ toast . show ( t ( 'toasts.auth.loginErr' ) , {
76+ type : 'danger' ,
77+ } ) ;
78+ return ;
79+ }
80+ } ,
81+ onError : ( error ) => {
82+ setLoading ( false )
83+ setError ( error . message || "Verification Failed" ) ;
84+ toast . show ( `Verification Failed: ${ error . message } ` , {
85+ type : 'danger' ,
86+ placement : 'top' ,
87+ duration : 4000 ,
88+ animationType : 'slide-in' ,
89+ } ) ;
90+ setInput ( Array ( 6 ) . fill ( "" ) ) ;
91+ } ,
92+ } ) ;
93+
94+ const [ LoginUser ] = useMutation ( LOGIN_MUTATION , {
95+ onCompleted : ( ) => {
96+ setResending ( false ) ;
97+ toast . show ( t ( 'toasts.two-factor.Code-resent-successfully' ) , {
98+ type : 'success' ,
99+ placement : 'top' ,
100+ duration : 4000 ,
101+ } ) ;
102+ resetTimer ( ) ;
103+ } ,
104+ onError : ( error ) => {
105+ setResending ( false ) ;
106+ toast . show ( t ( 'toasts.two-factor.Failed-to-resend-code' ) , {
107+ type : 'danger' ,
108+ placement : 'top' ,
109+ duration : 4000 ,
110+ } ) ;
111+ } ,
112+ } ) ;
113+
114+
115+ useEffect ( ( ) => {
116+ let timer : NodeJS . Timeout ;
117+ if ( isTimerActive && countdown > 0 ) {
118+ timer = setInterval ( ( ) => {
119+ setCountdown ( ( prev ) => prev - 1 ) ;
120+ } , 1000 ) ;
121+ } else if ( countdown === 0 ) {
122+ setIsTimerActive ( false ) ;
123+ }
124+ return ( ) => clearInterval ( timer ) ;
125+ } , [ countdown , isTimerActive ] ) ;
126+
127+ const handleResendOTP = async ( ) => {
128+ if ( resending || isTimerActive ) return ;
129+
130+ try {
131+ setResending ( true ) ;
132+ const email = await AsyncStorage . getItem ( 'user_email' ) ;
133+ const password = await AsyncStorage . getItem ( 'userpassword' ) ;
134+ const orgToken = await AsyncStorage . getItem ( 'orgToken' ) ;
135+ const loginInput = {
136+ email : email ,
137+ password : password ,
138+ orgToken : orgToken ,
139+ } ;
140+ if ( email && password ) {
141+ await LoginUser ( {
142+ variables : {
143+ loginInput,
144+ } ,
145+ } ) ;
146+ }
147+ } catch ( error ) {
148+ setResending ( false ) ;
149+ toast . show ( t ( 'toasts.two-factor.Failed-to-resend-code' ) , {
150+ type : 'danger' ,
151+ placement : 'top' ,
152+ duration : 4000 ,
153+ } ) ;
154+ }
155+ } ;
156+
157+ const verifyOtp = async ( ) => {
158+ const email = await AsyncStorage . getItem ( 'user_email' ) ;
159+
160+ if ( input . some ( ( val ) => ! val ) ) {
161+ setError ( "Please enter all digits" ) ;
162+ return ;
163+ }
164+
165+ setLoading ( true ) ;
166+
167+ try {
168+ await loginWithTwoFactorAuthentication ( {
169+ variables : {
170+ email,
171+ otp : input . join ( "" )
172+ } ,
173+ } ) ;
174+ setLoading ( false )
175+ } catch {
176+ setLoading ( false ) ;
177+ }
178+ } ;
179+
180+ const handleInputChange = ( index : number , value : string ) => {
181+ if ( ! / ^ \d * $ / . test ( value ) ) return ;
182+
183+ if ( value . length === 6 ) {
184+ const newInput = value . split ( "" ) . slice ( 0 , 6 ) ;
185+ setInput ( newInput ) ;
186+ inputRefs . current [ 5 ] ?. focus ( ) ;
187+ } else {
188+ const newInput = [ ...input ] ;
189+ newInput [ index ] = value ;
190+ setInput ( newInput ) ;
191+
192+ if ( value && index < input . length - 1 ) {
193+ inputRefs . current [ index + 1 ] ?. focus ( ) ;
194+ }
195+ }
196+ } ;
197+
198+
199+ const handleBackspace = ( index : number , value : string ) => {
200+ if ( ! value && index > 0 ) {
201+ inputRefs . current [ index - 1 ] ?. focus ( ) ;
202+ }
203+ } ;
204+
205+
206+ return (
207+ < KeyboardAvoidingView className = "h-full mx-5 flex flex-col justify-top items-center" >
208+ < View className = { `w-full h-fit mt-16 bg-white dark:bg-gray-800 rounded-lg p-6 shadow ${ colorScheme === "dark" ? "bg-gray-100" : "dark:bg-gray-900" } ` } >
209+ < Text className = { `text-center text-2xl font-Inter-Bold ${ colorScheme === "dark" ? "text-gray-100" : "text-gray-800" } ` } > { t ( 'toasts.two-factor.verficationtitle1' ) } </ Text >
210+ < Text className = { `text-center font-Inter-Bold text-lg ${ colorScheme === "dark" ? "text-gray-400" : "text-gray-600" } mt-2` } > { t ( 'toasts.two-factor.verficationtitle2' ) } </ Text >
211+ < Text className = { `text-center font-bold mt-1 ${ colorScheme === "dark" ? "text-gray-400" : "text-gray-600" } ` } > { userEmail } </ Text >
212+
213+ < View className = "flex-row justify-between mt-6 items-center gap-3" >
214+ { input . map ( ( value , index ) => (
215+ < TextInput
216+ key = { index }
217+ ref = { ( el ) => ( inputRefs . current [ index ] = el ! ) }
218+ value = { value }
219+ maxLength = { 6 }
220+ keyboardType = "numeric"
221+ onChangeText = { ( val ) => handleInputChange ( index , val ) }
222+ onKeyPress = { ( { nativeEvent } ) =>
223+ nativeEvent . key === "Backspace" && handleBackspace ( index , value )
224+ }
225+ className = { `w-10 h-10 text-center text-lg font-semibold border ${
226+ colorScheme === "dark"
227+ ? "bg-gray-700 text-gray-100 border-gray-600"
228+ : "bg-white text-gray-800"
229+ } rounded`}
230+ />
231+ ) ) }
232+ </ View >
233+
234+ < TouchableOpacity
235+ onPress = { verifyOtp }
236+ disabled = { loading || input . some ( ( val ) => ! val ) }
237+ className = { `mt-6 py-3 px-4 rounded ${ loading || input . some ( ( val ) => ! val ) ? "bg-gray-400" : "bg-[#8667F2]" } ` }
238+ >
239+ < Text className = "text-center text-white" > { loading ? t ( 'toasts.two-factor.Verifying' ) : t ( 'toasts.two-factor.Verify-Code' ) } </ Text >
240+ </ TouchableOpacity >
241+
242+ < View className = "mt-4 flex items-center justify-center" >
243+ { isTimerActive ? (
244+ < Text className = { `text-center ${ colorScheme === "dark" ? "text-gray-400" : "text-gray-600" } ` } >
245+ { t ( 'toasts.two-factor.Resend-code-in' ) } { countdown } s
246+ </ Text >
247+ ) : (
248+ < TouchableOpacity
249+ onPress = { handleResendOTP }
250+ disabled = { resending }
251+ >
252+ < Text className = "text-center text-[#8667F2] font-semibold" >
253+ { resending ? t ( 'toasts.two-factor.Sending' ) : t ( 'toasts.two-factor.Resend-Code' ) }
254+ </ Text >
255+ </ TouchableOpacity >
256+ ) }
257+ </ View >
258+ </ View >
259+ </ KeyboardAvoidingView >
260+ ) ;
261+ } ;
262+
263+ export default TwoFactorScreen ;
0 commit comments