Application Android et IOS pour l'amicale des élèves https://play.google.com/store/apps/details?id=fr.amicaleinsat.application
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

LoginScreen.tsx 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469
  1. /*
  2. * Copyright (c) 2019 - 2020 Arnaud Vergnet.
  3. *
  4. * This file is part of Campus INSAT.
  5. *
  6. * Campus INSAT is free software: you can redistribute it and/or modify
  7. * it under the terms of the GNU General Public License as published by
  8. * the Free Software Foundation, either version 3 of the License, or
  9. * (at your option) any later version.
  10. *
  11. * Campus INSAT is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. * GNU General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU General Public License
  17. * along with Campus INSAT. If not, see <https://www.gnu.org/licenses/>.
  18. */
  19. import * as React from 'react';
  20. import { Image, KeyboardAvoidingView, StyleSheet, View } from 'react-native';
  21. import {
  22. Button,
  23. Card,
  24. HelperText,
  25. TextInput,
  26. withTheme,
  27. } from 'react-native-paper';
  28. import i18n from 'i18n-js';
  29. import { StackNavigationProp, StackScreenProps } from '@react-navigation/stack';
  30. import LinearGradient from 'react-native-linear-gradient';
  31. import ConnectionManager from '../../managers/ConnectionManager';
  32. import ErrorDialog from '../../components/Dialogs/ErrorDialog';
  33. import { MASCOT_STYLE } from '../../components/Mascot/Mascot';
  34. import MascotPopup from '../../components/Mascot/MascotPopup';
  35. import CollapsibleScrollView from '../../components/Collapsible/CollapsibleScrollView';
  36. import { MainStackParamsList } from '../../navigation/MainNavigator';
  37. import GENERAL_STYLES from '../../constants/Styles';
  38. import Urls from '../../constants/Urls';
  39. import { ApiRejectType } from '../../utils/WebData';
  40. import { REQUEST_STATUS } from '../../utils/Requests';
  41. type LoginScreenNavigationProp = StackScreenProps<MainStackParamsList, 'login'>;
  42. type Props = LoginScreenNavigationProp & {
  43. navigation: StackNavigationProp<any>;
  44. theme: ReactNativePaper.Theme;
  45. };
  46. type StateType = {
  47. email: string;
  48. password: string;
  49. isEmailValidated: boolean;
  50. isPasswordValidated: boolean;
  51. loading: boolean;
  52. dialogVisible: boolean;
  53. dialogError: ApiRejectType;
  54. mascotDialogVisible: boolean | undefined;
  55. };
  56. const ICON_AMICALE = require('../../../assets/amicale.png');
  57. const emailRegex = /^.+@.+\..+$/;
  58. const styles = StyleSheet.create({
  59. card: {
  60. marginTop: 'auto',
  61. marginBottom: 'auto',
  62. },
  63. header: {
  64. fontSize: 36,
  65. marginBottom: 48,
  66. },
  67. text: {
  68. color: '#ffffff',
  69. },
  70. buttonContainer: {
  71. flexWrap: 'wrap',
  72. },
  73. lockButton: {
  74. marginRight: 'auto',
  75. marginBottom: 20,
  76. },
  77. sendButton: {
  78. marginLeft: 'auto',
  79. },
  80. });
  81. class LoginScreen extends React.Component<Props, StateType> {
  82. onEmailChange: (value: string) => void;
  83. onPasswordChange: (value: string) => void;
  84. passwordInputRef: {
  85. // @ts-ignore
  86. current: null | TextInput;
  87. };
  88. nextScreen: string | null;
  89. constructor(props: Props) {
  90. super(props);
  91. this.nextScreen = null;
  92. this.passwordInputRef = React.createRef();
  93. this.onEmailChange = (value: string) => {
  94. this.onInputChange(true, value);
  95. };
  96. this.onPasswordChange = (value: string) => {
  97. this.onInputChange(false, value);
  98. };
  99. props.navigation.addListener('focus', this.onScreenFocus);
  100. this.state = {
  101. email: '',
  102. password: '',
  103. isEmailValidated: false,
  104. isPasswordValidated: false,
  105. loading: false,
  106. dialogVisible: false,
  107. dialogError: { status: REQUEST_STATUS.SUCCESS },
  108. mascotDialogVisible: undefined,
  109. };
  110. }
  111. onScreenFocus = () => {
  112. this.handleNavigationParams();
  113. };
  114. /**
  115. * Navigates to the Amicale website screen with the reset password link as navigation parameters
  116. */
  117. onResetPasswordClick = () => {
  118. const { navigation } = this.props;
  119. navigation.navigate('website', {
  120. host: Urls.websites.amicale,
  121. path: Urls.amicale.resetPassword,
  122. title: i18n.t('screens.websites.amicale'),
  123. });
  124. };
  125. /**
  126. * Called when the user input changes in the email or password field.
  127. * This saves the new value in the State and disabled input validation (to prevent errors to show while typing)
  128. *
  129. * @param isEmail True if the field is the email field
  130. * @param value The new field value
  131. */
  132. onInputChange(isEmail: boolean, value: string) {
  133. if (isEmail) {
  134. this.setState({
  135. email: value,
  136. isEmailValidated: false,
  137. });
  138. } else {
  139. this.setState({
  140. password: value,
  141. isPasswordValidated: false,
  142. });
  143. }
  144. }
  145. /**
  146. * Focuses the password field when the email field is done
  147. *
  148. * @returns {*}
  149. */
  150. onEmailSubmit = () => {
  151. if (this.passwordInputRef.current != null) {
  152. this.passwordInputRef.current.focus();
  153. }
  154. };
  155. /**
  156. * Called when the user clicks on login or finishes to type his password.
  157. *
  158. * Checks if we should allow the user to login,
  159. * then makes the login request and enters a loading state until the request finishes
  160. *
  161. */
  162. onSubmit = () => {
  163. const { email, password } = this.state;
  164. if (this.shouldEnableLogin()) {
  165. this.setState({ loading: true });
  166. ConnectionManager.getInstance()
  167. .connect(email, password)
  168. .then(this.handleSuccess)
  169. .catch(this.showErrorDialog)
  170. .finally(() => {
  171. this.setState({ loading: false });
  172. });
  173. }
  174. };
  175. /**
  176. * Gets the form input
  177. *
  178. * @returns {*}
  179. */
  180. getFormInput() {
  181. const { email, password } = this.state;
  182. return (
  183. <View>
  184. <TextInput
  185. label={i18n.t('screens.login.email')}
  186. mode="outlined"
  187. value={email}
  188. onChangeText={this.onEmailChange}
  189. onBlur={this.validateEmail}
  190. onSubmitEditing={this.onEmailSubmit}
  191. error={this.shouldShowEmailError()}
  192. textContentType="emailAddress"
  193. autoCapitalize="none"
  194. autoCompleteType="email"
  195. autoCorrect={false}
  196. keyboardType="email-address"
  197. returnKeyType="next"
  198. secureTextEntry={false}
  199. />
  200. <HelperText type="error" visible={this.shouldShowEmailError()}>
  201. {i18n.t('screens.login.emailError')}
  202. </HelperText>
  203. <TextInput
  204. ref={this.passwordInputRef}
  205. label={i18n.t('screens.login.password')}
  206. mode="outlined"
  207. value={password}
  208. onChangeText={this.onPasswordChange}
  209. onBlur={this.validatePassword}
  210. onSubmitEditing={this.onSubmit}
  211. error={this.shouldShowPasswordError()}
  212. textContentType="password"
  213. autoCapitalize="none"
  214. autoCompleteType="password"
  215. autoCorrect={false}
  216. keyboardType="default"
  217. returnKeyType="done"
  218. secureTextEntry
  219. />
  220. <HelperText type="error" visible={this.shouldShowPasswordError()}>
  221. {i18n.t('screens.login.passwordError')}
  222. </HelperText>
  223. </View>
  224. );
  225. }
  226. /**
  227. * Gets the card containing the input form
  228. * @returns {*}
  229. */
  230. getMainCard() {
  231. const { props, state } = this;
  232. return (
  233. <View style={styles.card}>
  234. <Card.Title
  235. title={i18n.t('screens.login.title')}
  236. titleStyle={styles.text}
  237. subtitle={i18n.t('screens.login.subtitle')}
  238. subtitleStyle={styles.text}
  239. left={({ size }) => (
  240. <Image
  241. source={ICON_AMICALE}
  242. style={{
  243. width: size,
  244. height: size,
  245. }}
  246. />
  247. )}
  248. />
  249. <Card.Content>
  250. {this.getFormInput()}
  251. <Card.Actions style={styles.buttonContainer}>
  252. <Button
  253. icon="lock-question"
  254. mode="contained"
  255. onPress={this.onResetPasswordClick}
  256. color={props.theme.colors.warning}
  257. style={styles.lockButton}
  258. >
  259. {i18n.t('screens.login.resetPassword')}
  260. </Button>
  261. <Button
  262. icon="send"
  263. mode="contained"
  264. disabled={!this.shouldEnableLogin()}
  265. loading={state.loading}
  266. onPress={this.onSubmit}
  267. style={styles.sendButton}
  268. >
  269. {i18n.t('screens.login.title')}
  270. </Button>
  271. </Card.Actions>
  272. <Card.Actions>
  273. <Button
  274. icon="help-circle"
  275. mode="contained"
  276. onPress={this.showMascotDialog}
  277. style={GENERAL_STYLES.centerHorizontal}
  278. >
  279. {i18n.t('screens.login.mascotDialog.title')}
  280. </Button>
  281. </Card.Actions>
  282. </Card.Content>
  283. </View>
  284. );
  285. }
  286. /**
  287. * The user has unfocused the input, his email is ready to be validated
  288. */
  289. validateEmail = () => {
  290. this.setState({ isEmailValidated: true });
  291. };
  292. /**
  293. * The user has unfocused the input, his password is ready to be validated
  294. */
  295. validatePassword = () => {
  296. this.setState({ isPasswordValidated: true });
  297. };
  298. hideMascotDialog = () => {
  299. this.setState({ mascotDialogVisible: false });
  300. };
  301. showMascotDialog = () => {
  302. this.setState({ mascotDialogVisible: true });
  303. };
  304. /**
  305. * Shows an error dialog with the corresponding login error
  306. *
  307. * @param error The error given by the login request
  308. */
  309. showErrorDialog = (error: ApiRejectType) => {
  310. console.log(error);
  311. this.setState({
  312. dialogVisible: true,
  313. dialogError: error,
  314. });
  315. };
  316. hideErrorDialog = () => {
  317. this.setState({ dialogVisible: false });
  318. };
  319. /**
  320. * Navigates to the screen specified in navigation parameters or simply go back tha stack.
  321. * Saves in user preferences to not show the login banner again.
  322. */
  323. handleSuccess = () => {
  324. const { navigation } = this.props;
  325. // Do not show the home login banner again
  326. // TODO
  327. // AsyncStorageManager.set(
  328. // AsyncStorageManager.PREFERENCES.homeShowMascot.key,
  329. // false
  330. // );
  331. if (this.nextScreen == null) {
  332. navigation.goBack();
  333. } else {
  334. navigation.replace(this.nextScreen);
  335. }
  336. };
  337. /**
  338. * Saves the screen to navigate to after a successful login if one was provided in navigation parameters
  339. */
  340. handleNavigationParams() {
  341. this.nextScreen = this.props.route.params?.nextScreen;
  342. }
  343. /**
  344. * Checks if the entered email is valid (matches the regex)
  345. *
  346. * @returns {boolean}
  347. */
  348. isEmailValid(): boolean {
  349. const { email } = this.state;
  350. return emailRegex.test(email);
  351. }
  352. /**
  353. * Checks if we should tell the user his email is invalid.
  354. * We should only show this if his email is invalid and has been checked when un-focusing the input
  355. *
  356. * @returns {boolean|boolean}
  357. */
  358. shouldShowEmailError(): boolean {
  359. const { isEmailValidated } = this.state;
  360. return isEmailValidated && !this.isEmailValid();
  361. }
  362. /**
  363. * Checks if the user has entered a password
  364. *
  365. * @returns {boolean}
  366. */
  367. isPasswordValid(): boolean {
  368. const { password } = this.state;
  369. return password !== '';
  370. }
  371. /**
  372. * Checks if we should tell the user his password is invalid.
  373. * We should only show this if his password is invalid and has been checked when un-focusing the input
  374. *
  375. * @returns {boolean|boolean}
  376. */
  377. shouldShowPasswordError(): boolean {
  378. const { isPasswordValidated } = this.state;
  379. return isPasswordValidated && !this.isPasswordValid();
  380. }
  381. /**
  382. * If the email and password are valid, and we are not loading a request, then the login button can be enabled
  383. *
  384. * @returns {boolean}
  385. */
  386. shouldEnableLogin(): boolean {
  387. const { loading } = this.state;
  388. return this.isEmailValid() && this.isPasswordValid() && !loading;
  389. }
  390. render() {
  391. const { mascotDialogVisible, dialogVisible, dialogError } = this.state;
  392. return (
  393. <LinearGradient
  394. style={GENERAL_STYLES.flex}
  395. colors={['#9e0d18', '#530209']}
  396. start={{ x: 0, y: 0.1 }}
  397. end={{ x: 0.1, y: 1 }}
  398. >
  399. <KeyboardAvoidingView
  400. behavior={'height'}
  401. contentContainerStyle={GENERAL_STYLES.flex}
  402. style={GENERAL_STYLES.flex}
  403. enabled={true}
  404. keyboardVerticalOffset={100}
  405. >
  406. <CollapsibleScrollView headerColors={'transparent'}>
  407. <View style={GENERAL_STYLES.flex}>{this.getMainCard()}</View>
  408. <MascotPopup
  409. visible={mascotDialogVisible}
  410. title={i18n.t('screens.login.mascotDialog.title')}
  411. message={i18n.t('screens.login.mascotDialog.message')}
  412. icon={'help'}
  413. buttons={{
  414. cancel: {
  415. message: i18n.t('screens.login.mascotDialog.button'),
  416. icon: 'check',
  417. onPress: this.hideMascotDialog,
  418. },
  419. }}
  420. emotion={MASCOT_STYLE.NORMAL}
  421. />
  422. <ErrorDialog
  423. visible={dialogVisible}
  424. onDismiss={this.hideErrorDialog}
  425. status={dialogError.status}
  426. code={dialogError.code}
  427. />
  428. </CollapsibleScrollView>
  429. </KeyboardAvoidingView>
  430. </LinearGradient>
  431. );
  432. }
  433. }
  434. export default withTheme(LoginScreen);