Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[auth] Styled and implemented forgotPassword screen #60

Merged
merged 14 commits into from
Mar 13, 2024
6 changes: 0 additions & 6 deletions .vscode/settings.json

This file was deleted.

12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"react-native-url-polyfill": "^2.0.0",
"react-native-vector-icons": "^10.0.2",
"react-scroll-to-top": "^3.0.0",
"use-debounce": "^10.0.0",
"validator": "^13.11.0"
},
"devDependencies": {
Expand Down
167 changes: 79 additions & 88 deletions src/app/auth/forgotPassword/index.tsx
Original file line number Diff line number Diff line change
@@ -1,116 +1,107 @@
import { router } from 'expo-router';
import React, { useState } from 'react';
import { Alert, TextInput, View } from 'react-native';
import { Button, Input } from 'react-native-elements';
import { router, Link } from 'expo-router';
import { useState, useRef, useEffect } from 'react';
import { Alert, Text, View } from 'react-native';
import validator from 'validator';
import { useDebounce } from 'use-debounce';

import styles from './styles';
import globalStyles from '../../../styles/globalStyles';
import { useSession } from '../../../utils/AuthContext';
import UserStringInput from '../../../components/UserStringInput/UserStringInput';
import StyledButton from '../../../components/StyledButton/StyledButton';
import { isEmailTaken } from '../../../queries/profiles';
import { queryEmailByUsername } from '../../../queries/auth';
import colors from '../../../styles/colors';

function ForgotPasswordScreen() {
const { updateUser, signOut, resetPassword, verifyOtp } = useSession();
const { resetPassword } = useSession();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [verificationCode, setCode] = useState<string>('');
const [changingPassword, setChangingPassword] = useState(false);
const [emailToReset, setEmailToReset] = useState('');
const [emailError, setEmailError] = useState('');
const [value] = useDebounce(email, 250);
const [validEmail, setValidEmail] = useState(false);

const sendResetEmail = async () => {
const { error } = await resetPassword(email);
const { error } = await resetPassword(emailToReset);
if (error)
Alert.alert('Could not send a reset password email. Please try again.');
else
Alert.alert(
'Enter the verification code from your email to change your password',
);
else router.replace('auth/signup');
};
const verifyCode = async () => {
setLoading(true);

if (email && verificationCode) {
const { error } = await verifyOtp(email, verificationCode);
useEffect(() => {
checkEmailOrUsername(value);
}, [value]);

if (error) {
Alert.alert(error.message);
setChangingPassword(false);
} else {
router.push('auth/resetPassword');
const checkEmailOrUsername = async (newEmail: string) => {
setValidEmail(false);
if (validator.isEmail(newEmail)) {
if (newEmail.length === 0) {
setEmailError('');
return;
}
const emailIsTaken = await isEmailTaken(newEmail);
if (!emailIsTaken) {
setEmailError(
'An account with that email does not exist. Please try again.',
);
setValidEmail(false);
return;
}
} else if (!verificationCode) {
Alert.alert(`Please enter a verification code`);
setEmailToReset(newEmail);
} else {
Alert.alert(`Please sign up again.`);
}
const { data, error } = await queryEmailByUsername(newEmail);

setLoading(false);
};

const changePassword = async () => {
setLoading(true);
const { error } = await updateUser({ password });

if (error) {
Alert.alert('Updating password failed');
} else {
await signOut();
router.replace('/auth/login');
if (data && data?.length > 0 && !error) {
setValidEmail(true);
setEmailToReset(data[0].email);
} else {
setEmailError(
'An account with that username does not exist. Please try again.',
);
setValidEmail(false);
return;
}
}

setLoading(false);
setValidEmail(true);
setEmailError('');
};

return (
<View style={styles.container}>
<View style={[styles.verticallySpaced, globalStyles.mt20]}>
adityapawar1 marked this conversation as resolved.
Show resolved Hide resolved
<Input
label="Email"
leftIcon={{ type: 'font-awesome', name: 'envelope' }}
onChangeText={text => setEmail(text)}
value={email}
placeholder="[email protected]"
autoCapitalize="none"
/>
</View>
<View style={[styles.verticallySpaced, globalStyles.mt20]}>
<Button title="Send" disabled={loading} onPress={sendResetEmail} />
</View>
<Link href="/auth/login" style={styles.back}>
<Text style={[globalStyles.subtext, styles.backText]}>{'<Back'}</Text>
</Link>
<View style={styles.body}>
<View>
<Text style={[globalStyles.h1, styles.heading]}>
Forgot Password?
</Text>
<UserStringInput
placeholder="Email or account username"
placeholderTextColor={colors.darkGrey}
value={email}
label="Email or account username"
onChange={e => {
setEmail(e);
}}
/>
<Text style={[globalStyles.errorMessage, styles.subtext]}>
We'll email you a code to confirm your email.
</Text>

<TextInput
style={styles.input}
keyboardType="numeric"
onChangeText={setCode}
placeholder="Verification Code"
value={verificationCode}
maxLength={6}
/>
{email !== '' && (
<Text style={[globalStyles.errorMessage]}>{emailError}</Text>
)}
</View>

<View style={[styles.verticallySpaced, globalStyles.mt20]}>
<Button title="Verify" disabled={loading} onPress={verifyCode} />
<View style={styles.button}>
<StyledButton
disabled={!validEmail || email !== value}
text="Continue"
onPress={sendResetEmail}
/>
</View>
</View>

{changingPassword && (
<>
<View style={styles.verticallySpaced}>
<Input
label="Password"
leftIcon={{ type: 'font-awesome', name: 'lock' }}
onChangeText={text => setPassword(text)}
value={password}
secureTextEntry
placeholder="Password"
autoCapitalize="none"
/>
</View>

<View style={[styles.verticallySpaced, globalStyles.mt20]}>
<Button
title="Change Password"
disabled={loading}
onPress={changePassword}
/>
</View>
</>
)}
</View>
);
}
Expand Down
43 changes: 29 additions & 14 deletions src/app/auth/forgotPassword/styles.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,36 @@
import { StyleSheet } from 'react-native';
import colors from '../../../styles/colors';

export default StyleSheet.create({
input: {
height: 40,
borderColor: 'gray',
borderWidth: 1,
marginTop: 10,
padding: 5,
container: {
flex: 1,
paddingVertical: 32,
paddingLeft: 30,
paddingRight: 30,
},
verticallySpaced: {
paddingTop: 4,
paddingBottom: 4,
alignSelf: 'stretch',
heading: {
paddingBottom: 8,
},
container: {
paddingVertical: 63,
paddingLeft: 43,
paddingRight: 44,
body: {
flex: 1,
justifyContent: 'space-between',
paddingHorizontal: 12,
},
button: {
paddingBottom: 40,
},
subtext: {
paddingVertical: 18,
color: colors.darkGrey,
},
back: {
paddingTop: 30,
paddingBottom: 16,
color: colors.darkGrey,
fontSize: 12,
fontWeight: '400',
},
backText: {
color: colors.darkGrey,
},
});
10 changes: 2 additions & 8 deletions src/app/auth/signup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import colors from '../../../styles/colors';
import globalStyles from '../../../styles/globalStyles';
import { useSession } from '../../../utils/AuthContext';
import supabase from '../../../utils/supabase';
import { isEmailTaken } from '../../../queries/profiles';

function SignUpScreen() {
const { signUp } = useSession();
Expand Down Expand Up @@ -97,18 +98,11 @@ function SignUpScreen() {
return;
}

const { count } = await supabase
.from('profiles')
.select(`*`, { count: 'exact' })
.limit(1)
.eq('email', newEmail);
const emailIsTaken = (count ?? 0) >= 1;

const emailIsTaken = await isEmailTaken(newEmail);
if (emailIsTaken) {
setEmailError('That email is not available. Please try again.');
return;
}

setEmailError('');
};

Expand Down
10 changes: 10 additions & 0 deletions src/queries/profiles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,13 @@ export async function fetchUsername(user_id: string | undefined) {
return data[0].username as string;
}
}

export async function isEmailTaken(newEmail: string) {
const { count } = await supabase
.from('profiles')
.select(`*`, { count: 'exact' })
.limit(1)
.eq('email', newEmail);
const emailIsTaken = (count ?? 0) >= 1;
return emailIsTaken as boolean;
}
Loading