Skip to content

Commit f0f9a3c

Browse files
authored
fix(preference): add icon and able to expand (#120)
- add icons on preference settings - redirect the login activities preference [issueid: #104]
1 parent 544afe7 commit f0f9a3c

File tree

16 files changed

+564
-44
lines changed

16 files changed

+564
-44
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1309.2410.30012.0

Diff for: app/auth/login/index.tsx

+11-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import OrgLogin from '@/components/Login/OrgLogin';
22
import UserLogin from '@/components/Login/UserLogin';
3+
import TwoFactorScreen from '../two-factor';
34
import { LOGIN_MUTATION, ORG_LOGIN_MUTATION } from '@/graphql/mutations/login.mutation';
45
import { ApolloError, useApolloClient, useMutation } from '@apollo/client';
56
import AsyncStorage from '@react-native-async-storage/async-storage';
@@ -86,6 +87,7 @@ export default function SignInOrganization() {
8687
try {
8788
const orgToken = await AsyncStorage.getItem('orgToken');
8889
userInput.orgToken = orgToken;
90+
await AsyncStorage.setItem('user_email', userInput.email);
8991

9092
await LoginUser({
9193
variables: {
@@ -97,7 +99,7 @@ export default function SignInOrganization() {
9799
return;
98100
}
99101

100-
if (data.loginUser) {
102+
if (data.loginUser && !data.loginUser.otpRequired) {
101103
const token = data.loginUser.token;
102104

103105
if (data.loginUser.user.role === 'trainee') {
@@ -118,7 +120,14 @@ export default function SignInOrganization() {
118120
return;
119121
}
120122

121-
} else {
123+
}
124+
else if(data.loginUser.otpRequired)
125+
{
126+
await AsyncStorage.setItem('userpassword', userInput.password);
127+
router.push('/auth/two-factor')
128+
return
129+
}
130+
else {
122131
await AsyncStorage.setItem('authToken', data.loginUser.token);
123132
router.push('/dashboard');
124133
}

Diff for: app/auth/two-factor/index.tsx

+263
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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;

Diff for: assets/Preference_icons/down_arrow.png

2.79 KB
Loading

Diff for: assets/Preference_icons/forward-pref-icon.png

2.69 KB
Loading

Diff for: assets/Preference_icons/index.d.ts

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare module '*.png' {
2+
const content: any;
3+
export default content;
4+
}
5+
6+

Diff for: assets/Preference_icons/preference_icons.ts

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import forward_pref_icon from '@/assets/Preference_icons/forward-pref-icon.png'
2+
import down_arrow from '@/assets/Preference_icons/down_arrow.png'
3+
4+
export {
5+
forward_pref_icon,
6+
down_arrow
7+
}

Diff for: components/Login/OrgLogin.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export default function OrgLogin({ onSubmit }: OrgLoginProps) {
2525
initialValues: {} as FormValues,
2626
onSubmit: async (values) => {
2727
setLoading(true);
28-
await onSubmit(values);
28+
onSubmit(values);
2929
setLoading(false);
3030
},
3131
validationSchema: OrgLoginSchema,

Diff for: components/Login/UserLogin.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export default function UserLogin({ onSubmit }: userLoginProps) {
4545
initialValues: {email: '', password: '' } as FormValues,
4646
onSubmit: async (values: FormValues) => {
4747
setLoading(true);
48-
await onSubmit(values);
48+
onSubmit(values);
4949
setLoading(false);
5050
},
5151
validationSchema: UserLoginSchema,

0 commit comments

Comments
 (0)