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