Skip to content

Commit

Permalink
fix(preference): add icon and able to expand
Browse files Browse the repository at this point in the history
- add icons on preference settings
- redirect the login activities preference

[issueid: #104]
  • Loading branch information
niyibi250 authored and MugemaneBertin2001 committed Nov 26, 2024
1 parent 0d3869b commit 7148780
Show file tree
Hide file tree
Showing 17 changed files with 583 additions and 4,038 deletions.
1 change: 1 addition & 0 deletions %ProgramData%/Microsoft/Windows/UUS/State/_active.uusver
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1309.2410.30012.0
13 changes: 11 additions & 2 deletions app/auth/login/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import OrgLogin from '@/components/Login/OrgLogin';
import UserLogin from '@/components/Login/UserLogin';
import TwoFactorScreen from '../two-factor';
import { LOGIN_MUTATION, ORG_LOGIN_MUTATION } from '@/graphql/mutations/login.mutation';
import { ApolloError, useApolloClient, useMutation } from '@apollo/client';
import AsyncStorage from '@react-native-async-storage/async-storage';
Expand Down Expand Up @@ -86,6 +87,7 @@ export default function SignInOrganization() {
try {
const orgToken = await AsyncStorage.getItem('orgToken');
userInput.orgToken = orgToken;
await AsyncStorage.setItem('user_email', userInput.email);

await LoginUser({
variables: {
Expand All @@ -97,7 +99,7 @@ export default function SignInOrganization() {
return;
}

if (data.loginUser) {
if (data.loginUser && !data.loginUser.otpRequired) {
const token = data.loginUser.token;

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

} else {
}
else if(data.loginUser.otpRequired)
{
await AsyncStorage.setItem('userpassword', userInput.password);
router.push('/auth/two-factor')
return
}
else {
await AsyncStorage.setItem('authToken', data.loginUser.token);
router.push('/dashboard');
}
Expand Down
296 changes: 296 additions & 0 deletions app/auth/two-factor/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
import { useState, useEffect, useRef } from "react";
import {
View,
Text,
TextInput,
TouchableOpacity,
KeyboardAvoidingView,
useColorScheme,
} from "react-native";
import { useMutation, gql } from "@apollo/client";
import { LOGIN_MUTATION} from '@/graphql/mutations/login.mutation';
import { Href, useLocalSearchParams, useRouter } from 'expo-router';
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useToast } from 'react-native-toast-notifications';
import { useTranslation } from 'react-i18next';

export const LOGIN_WITH_2FA = gql`
mutation LoginWithTwoFactorAuthentication($email: String!, $otp: String!) {
loginWithTwoFactorAuthentication(email: $email, otp: $otp) {
token
user {
id
role
email
profile {
id
firstName
lastName
name
address
city
country
phoneNumber
biography
avatar
cover
}
}
message
}
}
`;

export const RESEND_OTP = gql`
mutation ResendOTP($email: String!) {
resendOTP(email: $email) {
message
}
}
`;

const TwoFactorScreen = () => {
const [input, setInput] = useState<string[]>(Array(6).fill(""));
const [userEmail, setuserEmail] = useState<string>('');
const [error, setError] = useState<string>("");
const [countdown, setCountdown] = useState<number>(30);
const [isTimerActive, setIsTimerActive] = useState<boolean>(true);
const {t} = useTranslation();
const [loading, setLoading] = useState<boolean>(false);
const [resending, setResending] = useState<boolean>(false);
const colorScheme = useColorScheme();
const inputRefs = useRef<TextInput[]>([]);
const params = useLocalSearchParams<{ redirect?: string; logout: string }>();
const router = useRouter();
const toast = useToast();

useEffect(() => {
const fetchUserEmail = async () => {
try {
const email = await AsyncStorage.getItem("user_email");
if (email) {
setuserEmail(email);
}
} catch (error) {
toast.show(`Failed to fetch email from storage`, {
type: 'danger',
placement: 'top',
duration: 4000,
animationType: 'slide-in',
});
}
};

fetchUserEmail();
}, []);

const resetTimer = () => {
setCountdown(30);
setIsTimerActive(true);
};

const [loginWithTwoFactorAuthentication] = useMutation(LOGIN_WITH_2FA, {
onCompleted: async (data) => {
const response = data.loginWithTwoFactorAuthentication;
const token = response.token;
if (response.user.role === 'trainee') {
await AsyncStorage.setItem('authToken', token);

while (router.canGoBack()) {
router.back();
}

params.redirect
? router.push(`${params.redirect}` as Href<string | object>)
: router.push('/dashboard');
return;
} else {
toast.show(t('toasts.auth.loginErr'), {
type: 'danger',
});
return;
}
},
onError: (error) => {
setLoading(false)
setError(error.message || "Verification Failed");
toast.show(`Verification Failed: ${error.message}`, {
type: 'danger',
placement: 'top',
duration: 4000,
animationType: 'slide-in',
});
setInput(Array(6).fill(""));
},
});

const [LoginUser] = useMutation(LOGIN_MUTATION, {
onCompleted: () => {
setResending(false);
toast.show(t('toasts.two-factor.Code-resent-successfully'), {
type: 'success',
placement: 'top',
duration: 4000,
});
resetTimer();
},
onError: (error) => {
setResending(false);
toast.show(t('toasts.two-factor.Failed-to-resend-code'), {
type: 'danger',
placement: 'top',
duration: 4000,
});
},
});


useEffect(() => {
let timer: NodeJS.Timeout;
if (isTimerActive && countdown > 0) {
timer = setInterval(() => {
setCountdown((prev) => prev - 1);
}, 1000);
} else if (countdown === 0) {
setIsTimerActive(false);
}
return () => clearInterval(timer);
}, [countdown, isTimerActive]);

const handleResendOTP = async () => {
if (resending || isTimerActive) return;

try {
setResending(true);
const email = await AsyncStorage.getItem('user_email');
const password = await AsyncStorage.getItem('userpassword');
const orgToken = await AsyncStorage.getItem('orgToken');
const loginInput = {
email: email,
password: password,
orgToken: orgToken,
};
if (email && password) {
await LoginUser({
variables: {
loginInput,
},
});
}
} catch (error) {
setResending(false);
toast.show(t('toasts.two-factor.Failed-to-resend-code'), {
type: 'danger',
placement: 'top',
duration: 4000,
});
}
};

const verifyOtp = async () => {
const email = await AsyncStorage.getItem('user_email');

if (input.some((val) => !val)) {
setError("Please enter all digits");
return;
}

setLoading(true);

try {
await loginWithTwoFactorAuthentication({
variables: {
email,
otp: input.join("")
},
});
setLoading(false)
} catch {
setLoading(false);
}
};

const handleInputChange = (index: number, value: string) => {
if (!/^\d*$/.test(value)) return;

if (value.length === 6) {
const newInput = value.split("").slice(0, 6);
setInput(newInput);
inputRefs.current[5]?.focus();
} else {
const newInput = [...input];
newInput[index] = value;
setInput(newInput);

if (value && index < input.length - 1) {
inputRefs.current[index + 1]?.focus();
}
}
};


const handleBackspace = (index: number, value: string) => {
if (!value && index > 0) {
inputRefs.current[index - 1]?.focus();
}
};


return (
<KeyboardAvoidingView className="h-full mx-5 flex flex-col justify-top items-center">
<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"}`}>
<Text className={`text-center text-2xl font-Inter-Bold ${colorScheme === "dark" ? "text-gray-100" : "text-gray-800"}`}>{t('toasts.two-factor.verficationtitle1')}</Text>
<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>
<Text className={`text-center font-bold mt-1 ${colorScheme === "dark" ? "text-gray-400" : "text-gray-600"}`}>{userEmail}</Text>

<View className="flex-row justify-between mt-6 items-center gap-3">
{input.map((value, index) => (
<TextInput
key={index}
ref={(el) => (inputRefs.current[index] = el!)}
value={value}
maxLength={6}
keyboardType="numeric"
onChangeText={(val) => handleInputChange(index, val)}
onKeyPress={({ nativeEvent }) =>
nativeEvent.key === "Backspace" && handleBackspace(index, value)
}
className={`w-10 h-10 text-center text-lg font-semibold border ${
colorScheme === "dark"
? "bg-gray-700 text-gray-100 border-gray-600"
: "bg-white text-gray-800"
} rounded`}
/>
))}
</View>

<TouchableOpacity
onPress={verifyOtp}
disabled={loading || input.some((val) => !val)}
className={`mt-6 py-3 px-4 rounded ${loading || input.some((val) => !val) ? "bg-gray-400" : "bg-[#8667F2]"}`}
>
<Text className="text-center text-white">{loading ? t('toasts.two-factor.Verifying') : t('toasts.two-factor.Verify-Code')}</Text>
</TouchableOpacity>

<View className="mt-4 flex items-center justify-center">
{isTimerActive ? (
<Text className={`text-center ${colorScheme === "dark" ? "text-gray-400" : "text-gray-600"}`}>
{t('toasts.two-factor.Resend-code-in')} {countdown}s
</Text>
) : (
<TouchableOpacity
onPress={handleResendOTP}
disabled={resending}
>
<Text className="text-center text-[#8667F2] font-semibold">
{resending ? t('toasts.two-factor.Sending') : t('toasts.two-factor.Resend-Code')}
</Text>
</TouchableOpacity>
)}
</View>
</View>
</KeyboardAvoidingView>
);
};

export default TwoFactorScreen;
Binary file added assets/Preference_icons/down_arrow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/Preference_icons/forward-pref-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions assets/Preference_icons/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
declare module '*.png' {
const content: any;
export default content;
}


7 changes: 7 additions & 0 deletions assets/Preference_icons/preference_icons.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import forward_pref_icon from '@/assets/Preference_icons/forward-pref-icon.png'
import down_arrow from '@/assets/Preference_icons/down_arrow.png'

export {
forward_pref_icon,
down_arrow
}
2 changes: 1 addition & 1 deletion components/Login/OrgLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function OrgLogin({ onSubmit }: OrgLoginProps) {
initialValues: {} as FormValues,
onSubmit: async (values) => {
setLoading(true);
await onSubmit(values);
onSubmit(values);
setLoading(false);
},
validationSchema: OrgLoginSchema,
Expand Down
2 changes: 1 addition & 1 deletion components/Login/UserLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function UserLogin({ onSubmit }: userLoginProps) {
initialValues: {email: '', password: '' } as FormValues,
onSubmit: async (values: FormValues) => {
setLoading(true);
await onSubmit(values);
onSubmit(values);
setLoading(false);
},
validationSchema: UserLoginSchema,
Expand Down
Loading

0 comments on commit 7148780

Please sign in to comment.