Feature/addsignupsignin (#83)
* first branch commit * testing formik yup in react native * made TextInput work with formik * react native base form control working great removed formik * adding the button logic * fix stupid error when merging this commit and precending ones need to be rebased * fix multiple merging issues * functionnal login form setup in the app * added translation support for LoginForm * added a forgotten password and added translation fixes * Renamed loginform to Signinform for better coherence * v1 of signup form and finished a missing renamed for signinform * errors messages are now displayed correctly * removed unused helper text * addded translations * added a password complexity check * added missing translations and a temporary implementation of the AuthentificationWiew * PR diff preview quick fixes: Removed unused imports, auto format, removed unnecessary changes * Front: Authentication View: Use toast as helper messages * first branch commit * testing formik yup in react native * made TextInput work with formik * react native base form control working great removed formik * adding the button logic * fix stupid error when merging this commit and precending ones need to be rebased * fix multiple merging issues * functionnal login form setup in the app * added translation support for LoginForm * added a forgotten password and added translation fixes * Renamed loginform to Signinform for better coherence * v1 of signup form and finished a missing renamed for signinform * errors messages are now displayed correctly * removed unused helper text * addded translations * added a password complexity check * added missing translations and a temporary implementation of the AuthentificationWiew * PR diff preview quick fixes: Removed unused imports, auto format, removed unnecessary changes * Front: Authentication View: Use toast as helper messages * removed a worng declaration * fixed PR comments Co-authored-by: Arthi-chaud <arthur.jamet@gmail.com>
This commit is contained in:
@@ -9,3 +9,4 @@ prisma/migrations/*
|
||||
output.xml
|
||||
report.html
|
||||
log.html
|
||||
.expo
|
||||
@@ -5,6 +5,7 @@ import LessonHistory from "./models/LessonHistory";
|
||||
import Song from "./models/Song";
|
||||
import SongHistory from "./models/SongHistory";
|
||||
import User from "./models/User";
|
||||
import { translate } from "./i18n/i18n";
|
||||
|
||||
const delay = (seconds: number) => new Promise(resolve => setTimeout(resolve, seconds * 1000));
|
||||
|
||||
@@ -134,4 +135,33 @@ export default class API {
|
||||
userId: 1
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the login information status
|
||||
*
|
||||
*/
|
||||
static async checkSigninCredentials(username: string, password: string): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (username === "katerina" && password === "1234") {
|
||||
return resolve("token signin");
|
||||
}
|
||||
return reject(translate("invalidCredentials"));
|
||||
}, 1000);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the register information status
|
||||
*/
|
||||
static async checkSignupCredentials(username: string, password: string, email: string): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
if (username === "bluub") {
|
||||
return reject(translate("usernameTaken"));
|
||||
}
|
||||
return resolve("token signup");
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -29,4 +29,4 @@ export const Router = () => {
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,4 @@ const LoadingComponent = () => {
|
||||
const theme = useTheme();
|
||||
return <Spinner color={theme.colors.primary[500]}/>
|
||||
}
|
||||
|
||||
export default LoadingComponent;
|
||||
@@ -0,0 +1,128 @@
|
||||
// a form for sign in
|
||||
|
||||
import React from "react";
|
||||
import { translate } from "../../i18n/i18n";
|
||||
import { string } from "yup";
|
||||
import {
|
||||
FormControl,
|
||||
Input,
|
||||
Stack,
|
||||
WarningOutlineIcon,
|
||||
Box,
|
||||
Button,
|
||||
useToast,
|
||||
} from "native-base";
|
||||
|
||||
interface SigninFormProps {
|
||||
onSubmit: (username: string, password: string) => Promise<string>;
|
||||
}
|
||||
|
||||
const LoginForm = ({ onSubmit }: SigninFormProps) => {
|
||||
const [formData, setFormData] = React.useState({
|
||||
username: {
|
||||
value: "",
|
||||
error: null as string | null,
|
||||
},
|
||||
password: {
|
||||
value: "",
|
||||
error: null as string | null,
|
||||
},
|
||||
});
|
||||
const [submittingForm, setSubmittingForm] = React.useState(false);
|
||||
|
||||
const validationSchemas = {
|
||||
username: string()
|
||||
.min(3, translate("usernameTooShort"))
|
||||
.max(20, translate("usernameTooLong"))
|
||||
.required("Username is required"),
|
||||
password: string()
|
||||
.min(4, translate("passwordTooShort"))
|
||||
.max(100, translate("passwordTooLong"))
|
||||
.required("Password is required"),
|
||||
};
|
||||
const toast = useToast();
|
||||
return (
|
||||
<Box alignItems="center" style={{ width: '100%' }}>
|
||||
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
|
||||
<FormControl
|
||||
isRequired
|
||||
isInvalid={
|
||||
formData.username.error !== null ||
|
||||
formData.password.error !== null
|
||||
}
|
||||
>
|
||||
<FormControl.Label>{translate("username")}</FormControl.Label>
|
||||
<Input
|
||||
isRequired
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={formData.username.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.username
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, username: { value: t, error } });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FormControl.ErrorMessage
|
||||
leftIcon={<WarningOutlineIcon size="xs" />}
|
||||
>
|
||||
{formData.username.error}
|
||||
</FormControl.ErrorMessage>
|
||||
<FormControl.Label>{translate("password")}</FormControl.Label>
|
||||
<Input
|
||||
isRequired
|
||||
type="password"
|
||||
placeholder="password"
|
||||
value={formData.password.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.password
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, password: { value: t, error } });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FormControl.ErrorMessage
|
||||
leftIcon={<WarningOutlineIcon size="xs" />}
|
||||
>
|
||||
{formData.password.error}
|
||||
</FormControl.ErrorMessage>
|
||||
<Button
|
||||
style={{ marginTop: 10 }}
|
||||
isLoading={submittingForm}
|
||||
isDisabled={
|
||||
formData.password.error !== null ||
|
||||
formData.username.error !== null ||
|
||||
formData.username.value === "" ||
|
||||
formData.password.value === ""
|
||||
}
|
||||
onPress={async () => {
|
||||
setSubmittingForm(true);
|
||||
try {
|
||||
const resp = await onSubmit(
|
||||
formData.username.value,
|
||||
formData.password.value
|
||||
);
|
||||
toast.show({ description: resp, colorScheme: 'secondary' })
|
||||
} catch (e) {
|
||||
toast.show({ description: e as string, colorScheme: 'red', avoidKeyboard: true })
|
||||
} finally {
|
||||
setSubmittingForm(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{translate("login")}
|
||||
</Button>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
@@ -0,0 +1,205 @@
|
||||
// a form for sign up
|
||||
|
||||
import React from "react";
|
||||
import { translate } from "../../i18n/i18n";
|
||||
import { string } from "yup";
|
||||
import {
|
||||
FormControl,
|
||||
Input,
|
||||
Stack,
|
||||
WarningOutlineIcon,
|
||||
Box,
|
||||
Button,
|
||||
useToast,
|
||||
} from "native-base";
|
||||
|
||||
interface SignupFormProps {
|
||||
onSubmit: (
|
||||
username: string,
|
||||
password: string,
|
||||
email: string
|
||||
) => Promise<string>;
|
||||
}
|
||||
|
||||
const LoginForm = ({ onSubmit }: SignupFormProps) => {
|
||||
const [formData, setFormData] = React.useState({
|
||||
username: {
|
||||
value: "",
|
||||
error: null as string | null,
|
||||
},
|
||||
password: {
|
||||
value: "",
|
||||
error: null as string | null,
|
||||
},
|
||||
repeatPassword: {
|
||||
value: "",
|
||||
error: null as string | null,
|
||||
},
|
||||
email: {
|
||||
value: "",
|
||||
error: null as string | null,
|
||||
},
|
||||
});
|
||||
const [submittingForm, setSubmittingForm] = React.useState(false);
|
||||
|
||||
const validationSchemas = {
|
||||
username: string()
|
||||
.min(3, translate("usernameTooShort"))
|
||||
.max(20, translate("usernameTooLong"))
|
||||
.required("Username is required"),
|
||||
email: string().email("Invalid email").required("Email is required"),
|
||||
password: string()
|
||||
.min(4, translate("passwordTooShort"))
|
||||
.max(100, translate("passwordTooLong"))
|
||||
.matches(
|
||||
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#\$%\^&\*])(?=.{8,})/,
|
||||
translate(
|
||||
"Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character"
|
||||
)
|
||||
)
|
||||
.required("Password is required"),
|
||||
};
|
||||
const toast = useToast();
|
||||
|
||||
return (
|
||||
<Box alignItems="center" style={{ width: '100%' }}>
|
||||
<Box style={{ width: '80%', maxWidth: 400 }}>
|
||||
<Stack mx="4">
|
||||
<FormControl
|
||||
isRequired
|
||||
isInvalid={
|
||||
formData.username.error !== null ||
|
||||
formData.password.error !== null ||
|
||||
formData.repeatPassword.error !== null ||
|
||||
formData.email.error !== null
|
||||
}
|
||||
>
|
||||
<FormControl.Label>{translate("username")}</FormControl.Label>
|
||||
<Input
|
||||
isRequired
|
||||
type="text"
|
||||
placeholder="Katerina"
|
||||
value={formData.username.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.username
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, username: { value: t, error } });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FormControl.ErrorMessage
|
||||
leftIcon={<WarningOutlineIcon size="xs" />}
|
||||
>
|
||||
{formData.username.error}
|
||||
</FormControl.ErrorMessage>
|
||||
<FormControl.Label>{translate("email")}</FormControl.Label>
|
||||
<Input
|
||||
isRequired
|
||||
type="text"
|
||||
placeholder="lucy@er.com"
|
||||
value={formData.email.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.email
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, email: { value: t, error } });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FormControl.ErrorMessage
|
||||
leftIcon={<WarningOutlineIcon size="xs" />}
|
||||
>
|
||||
{formData.email.error}
|
||||
</FormControl.ErrorMessage>
|
||||
<FormControl.Label>{translate("password")}</FormControl.Label>
|
||||
<Input
|
||||
isRequired
|
||||
type="password"
|
||||
placeholder="password"
|
||||
value={formData.password.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.password
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
setFormData({ ...formData, password: { value: t, error } });
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FormControl.ErrorMessage
|
||||
leftIcon={<WarningOutlineIcon size="xs" />}
|
||||
>
|
||||
{formData.password.error}
|
||||
</FormControl.ErrorMessage>
|
||||
<FormControl.Label>{translate("repeatPassword")}</FormControl.Label>
|
||||
<Input
|
||||
isRequired
|
||||
type="password"
|
||||
placeholder="password"
|
||||
value={formData.repeatPassword.value}
|
||||
onChangeText={(t) => {
|
||||
let error: null | string = null;
|
||||
validationSchemas.password
|
||||
.validate(t)
|
||||
.catch((e) => (error = e.message))
|
||||
.finally(() => {
|
||||
if (!error && t !== formData.password.value) {
|
||||
error = translate("passwordsDontMatch");
|
||||
}
|
||||
setFormData({
|
||||
...formData,
|
||||
repeatPassword: { value: t, error },
|
||||
});
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<FormControl.ErrorMessage
|
||||
leftIcon={<WarningOutlineIcon size="xs" />}
|
||||
>
|
||||
{formData.repeatPassword.error}
|
||||
</FormControl.ErrorMessage>
|
||||
<Button
|
||||
style={{ marginTop: 10 }}
|
||||
isLoading={submittingForm}
|
||||
isDisabled={
|
||||
formData.password.error !== null ||
|
||||
formData.username.error !== null ||
|
||||
formData.repeatPassword.error !== null ||
|
||||
formData.email.error !== null ||
|
||||
formData.username.value === "" ||
|
||||
formData.password.value === "" ||
|
||||
formData.repeatPassword.value === "" ||
|
||||
formData.repeatPassword.value === ""
|
||||
}
|
||||
onPress={async () => {
|
||||
setSubmittingForm(true);
|
||||
try {
|
||||
const resp = await onSubmit(
|
||||
formData.username.value,
|
||||
formData.password.value,
|
||||
formData.email.value
|
||||
);
|
||||
toast.show({ description: resp });
|
||||
} catch (e) {
|
||||
toast.show({ description: e as string });
|
||||
} finally {
|
||||
setSubmittingForm(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{translate("signUp")}
|
||||
</Button>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
@@ -11,6 +11,7 @@ export const en = {
|
||||
chapters: 'Chapters',
|
||||
bestScore: 'Best Score',
|
||||
lastScore: 'Last Score',
|
||||
play: 'Play',
|
||||
langBtn: 'Language',
|
||||
diffBtn: 'Difficulty',
|
||||
backBtn: 'Back',
|
||||
@@ -27,7 +28,6 @@ export const en = {
|
||||
dark: 'Dark',
|
||||
system: 'System',
|
||||
light: 'Light',
|
||||
play: 'Play',
|
||||
goNextStep: 'Step Up!',
|
||||
mySkillsToImprove: "My Competencies to work on",
|
||||
recentlyPlayed: 'Recently played',
|
||||
@@ -43,6 +43,25 @@ export const en = {
|
||||
arpegeCompetency: 'Arpeges',
|
||||
chordsCompetency: 'Chords',
|
||||
|
||||
usernameTooShort: 'Username is too short',
|
||||
passwordTooShort: 'Password is too short',
|
||||
usernameTooLong: 'Username is too long',
|
||||
passwordTooLong: 'Password is too long',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
login: 'Login',
|
||||
invalidCredentials: 'Invalid credentials',
|
||||
forgottenPassword: 'Forgotten password',
|
||||
invalidEmail: 'Invalid email',
|
||||
email: 'Email',
|
||||
repeatPassword: 'Repeat password',
|
||||
passwordsDontMatch: 'Passwords don\'t match',
|
||||
signUp: 'Sign up',
|
||||
signIn: 'Sign in',
|
||||
"Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character": "Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character",
|
||||
accountCreated: 'Account created',
|
||||
loggedIn: 'Logged in',
|
||||
usernameTaken: 'Username already taken',
|
||||
};
|
||||
|
||||
export const fr: typeof en = {
|
||||
@@ -58,6 +77,7 @@ export const fr: typeof en = {
|
||||
chapters: 'Chapitres',
|
||||
bestScore: 'Meilleur Score',
|
||||
lastScore: 'Dernier Score',
|
||||
play: 'Jouer',
|
||||
langBtn: 'Langage',
|
||||
diffBtn: 'Difficulté',
|
||||
backBtn: 'Retour',
|
||||
@@ -73,7 +93,6 @@ export const fr: typeof en = {
|
||||
dark: 'Foncé',
|
||||
system: 'Système',
|
||||
light: 'Clair',
|
||||
play: 'Jouer',
|
||||
settingsBtn: "Réglages",
|
||||
goNextStep: "Prochaine Etape",
|
||||
mySkillsToImprove: "Mes Skills",
|
||||
@@ -86,7 +105,26 @@ export const fr: typeof en = {
|
||||
leftHandCompetency: "Main gauche",
|
||||
accuracyCompetency: "Justesse",
|
||||
arpegeCompetency: "Arpege",
|
||||
chordsCompetency: "Accords"
|
||||
chordsCompetency: "Accords",
|
||||
usernameTooShort: 'Le nom d\'utilisateur est trop court',
|
||||
passwordTooShort: 'Le mot de passe est trop court',
|
||||
usernameTooLong: 'Le nom d\'utilisateur est trop long',
|
||||
passwordTooLong: 'Le mot de passe est trop long',
|
||||
username: 'Nom d\'utilisateur',
|
||||
password: 'Mot de passe',
|
||||
login: 'Se connecter',
|
||||
invalidCredentials: "Informations d'identification invalides",
|
||||
loggedIn: 'Connecté',
|
||||
usernameTaken: 'Nom d\'utilisateur déjà pris',
|
||||
forgottenPassword: "Mot de passe oublié",
|
||||
invalidEmail: "Email invalide",
|
||||
email: "Email",
|
||||
repeatPassword: "Confirmer",
|
||||
passwordsDontMatch: "Mots de passes différents",
|
||||
signUp: "S'inscrire",
|
||||
signIn: "Se connecter",
|
||||
"Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character": "",
|
||||
accountCreated: "Compte créé"
|
||||
};
|
||||
|
||||
export const sp: typeof en = {
|
||||
@@ -132,5 +170,24 @@ export const sp: typeof en = {
|
||||
chapters: "Chapitres",
|
||||
bestScore: "Meilleur Score",
|
||||
lastScore: "Dernier score",
|
||||
recentlyPlayed: "Joués récemment"
|
||||
recentlyPlayed: "Joués récemment",
|
||||
usernameTooShort: 'Le nom d\'utilisateur est trop court',
|
||||
passwordTooShort: 'Le mot de passe est trop court',
|
||||
usernameTooLong: 'Le nom d\'utilisateur est trop long',
|
||||
passwordTooLong: 'Le mot de passe est trop long',
|
||||
username: 'Nom d\'utilisateur',
|
||||
password: 'Mot de passe',
|
||||
login: 'Se connecter',
|
||||
invalidCredentials: "Informations d'identification invalides",
|
||||
forgottenPassword: 'Mot de passe oublié',
|
||||
invalidEmail: 'Email invalide',
|
||||
email: 'Email',
|
||||
repeatPassword: 'Répéter le mot de passe',
|
||||
passwordsDontMatch: 'Les mots de passe ne correspondent pas',
|
||||
signUp: 'S\'inscrire',
|
||||
signIn: 'Se connecter',
|
||||
"Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and One Special Case Character": "Doit contenir 8 caractères, une majuscule, une minuscule, un chiffre et un caractère spécial",
|
||||
accountCreated: 'Compte créé',
|
||||
loggedIn: 'Connectado',
|
||||
usernameTaken: 'Nombre de usuario ya tomado',
|
||||
};
|
||||
+2
-1
@@ -40,7 +40,8 @@
|
||||
"react-native-svg": "12.3.0",
|
||||
"react-native-testing-library": "^6.0.0",
|
||||
"react-native-web": "0.17.7",
|
||||
"react-redux": "^8.0.2"
|
||||
"react-redux": "^8.0.2",
|
||||
"yup": "^0.32.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.9",
|
||||
|
||||
@@ -1,20 +1,52 @@
|
||||
import React from "react";
|
||||
import { useDispatch } from '../state/Store';
|
||||
import { translate } from "../i18n/i18n";
|
||||
import API from "../API";
|
||||
import { setUserToken } from "../state/UserSlice";
|
||||
import { Center, Button, Text } from 'native-base';
|
||||
import SigninForm from "../components/forms/signinform";
|
||||
import SignupForm from "../components/forms/signupform";
|
||||
|
||||
const hanldeSignin = async (username: string, password: string, tokenSetter: (token: string) => void): Promise<string> => {
|
||||
try {
|
||||
const response = await API.checkSigninCredentials(username, password);
|
||||
tokenSetter(response);
|
||||
return translate("loggedIn");
|
||||
} catch (error) {
|
||||
return error as string;
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignup = async (username: string, password: string, email: string, tokenSetter: (t: string) => void): Promise<string> => {
|
||||
try {
|
||||
const response = await API.checkSignupCredentials(username, password, email);
|
||||
tokenSetter(response);
|
||||
return translate("loggedIn");
|
||||
} catch (error) {
|
||||
return error as string;
|
||||
}
|
||||
};
|
||||
|
||||
const AuthenticationView = () => {
|
||||
const dispatch = useDispatch();
|
||||
const [mode, setMode] = React.useState("signin" as "signin" | "signup");
|
||||
|
||||
return (
|
||||
<Center style={{ flex: 1 }}>
|
||||
<Text>{translate('welcome')}</Text>
|
||||
<Button variant='ghost' onPress={() => dispatch(setUserToken('kkkk'))}>
|
||||
{translate('signinBtn')}
|
||||
{mode === "signin" ? (<>
|
||||
<Text fontWeight='thin'>username, password:</Text>
|
||||
<Text fontWeight='thin'>katerina, 1234</Text>
|
||||
<SigninForm onSubmit={(username, password) => hanldeSignin(username, password, (token) => dispatch(setUserToken(token)))} />
|
||||
</>) : (
|
||||
<SignupForm onSubmit={(username, password, email) => handleSignup(username, password, email, (token) => dispatch(setUserToken(token)))} />
|
||||
)}
|
||||
{ mode ==="signin" && <Button variant="outline" marginTop={5} colorScheme="error" >{translate("forgottenPassword")}</Button> }
|
||||
<Button variant='outline' marginTop={5} colorScheme='primary' onPress={() => setMode(mode === "signin" ? "signup" : "signin")}>
|
||||
<Text>{translate(mode === "signin" ? "signUp" : "signIn")}</Text>
|
||||
</Button>
|
||||
</Center>
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export default AuthenticationView;
|
||||
|
||||
+391
-355
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user