diff options
author | Leon Jiang <35908040+leonyjiang@users.noreply.github.com> | 2020-07-08 09:56:17 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-08 12:56:17 -0400 |
commit | e32241734c8cc258812ac12c7727aaa7f947eed5 (patch) | |
tree | 8ef1ab3a5203496641be721a9567173b87c4f551 | |
parent | ff358c8927086a69f6732b6e7e1abb85a9e3cc84 (diff) |
[TMA-60] Registration Page UI & Field Validation (#13)
* remove unused image
* refactor LoginInput component to be more generic
* configure bare registration screen
* create index files for exports
* add yarn typing script
* refactor and re-style LoginInput component
* re-style login screen according to designs
* make LoginInput name more generic, give TaggInput dirty & width props
* add disabled feature to login screen submit button, finalized styles
* add arrow images and create ArrowButton component
* create RegistrationWizard component and move files around
* added disabled & enabled buttons to ArrowButton component
* create dummy terms and conditions text
* create common CenteredView component for re-use
* create custom RadioCheckbox for registration screen
* create TermsConditions & OverlayView components
* update index.ts export files
* build registration page UI with basic validation
* yarn lint/type & add platform-specific styling
* add yarn type item to PR checklist
* add react-native-animatable dependency to project
* add regex variables to constants file
* Add width prop for more flexible styling
* Add types and disable auto-capitalization
* Update email validation regex
* Create linear-gradient background component
* Update password regex and add inline docs
* Refactor code to be more readable
* Add warning prop and animation to TaggInput
* Add wrapper View for vertical margins
* Make JSX more readable & add TaggInput components
* Integrate refactored code into registration page
* Merge in login screen changes
* Lint and fix file syntax
* Fix function docs
* Add ViewProps to CenterView props
* Add KeyboardAvoidingView to Background component
* Add blurOnSubmit for inputs, restore deleted handleLogin code
* Create Verification screen and add it to routes
* Add routing to Verification page upon success
* Add API request upon registration submit
* Trigger warning shaking animation on submit
* Make disabled arrow touchable, tap triggers submit
33 files changed, 1117 insertions, 266 deletions
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d4ef5481..b39e49be 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -29,7 +29,7 @@ <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> <!--- If you're unsure about any of these, don't hesitate to ask! --> - [ ] I thoroughly tested my changes and ensured that they integrate with existing functionality. -- [ ] I ran `yarn lint` to make sure my code adheres to standard style expectations. +- [ ] I ran `yarn lint` and `yarn type`, and I fixed any syntax or type errors that came up. - [ ] I ran `git pull --rebase upstream master` in my branch and resolved any merge conflicts on my end. - [ ] I clearly documented and detailed my code where I deemed it necessary to do so. - [ ] My changes require a change to the documentation... diff --git a/package.json b/package.json index 599c8797..32794328 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "ios": "react-native run-ios", "start": "react-native start", "test": "jest", + "type": "tsc", "lint": "eslint . --ext .js,.jsx,.ts,.tsx --fix" }, "dependencies": { diff --git a/src/assets/images/arrow-backward.png b/src/assets/images/arrow-backward.png Binary files differnew file mode 100644 index 00000000..b6da5b48 --- /dev/null +++ b/src/assets/images/arrow-backward.png diff --git a/src/assets/images/arrow-backward@2x.png b/src/assets/images/arrow-backward@2x.png Binary files differnew file mode 100644 index 00000000..cf09be25 --- /dev/null +++ b/src/assets/images/arrow-backward@2x.png diff --git a/src/assets/images/arrow-backward@3x.png b/src/assets/images/arrow-backward@3x.png Binary files differnew file mode 100644 index 00000000..7e98f51b --- /dev/null +++ b/src/assets/images/arrow-backward@3x.png diff --git a/src/assets/images/arrow-forward-disabled.png b/src/assets/images/arrow-forward-disabled.png Binary files differnew file mode 100644 index 00000000..b1141aa6 --- /dev/null +++ b/src/assets/images/arrow-forward-disabled.png diff --git a/src/assets/images/arrow-forward-disabled@2x.png b/src/assets/images/arrow-forward-disabled@2x.png Binary files differnew file mode 100644 index 00000000..46680148 --- /dev/null +++ b/src/assets/images/arrow-forward-disabled@2x.png diff --git a/src/assets/images/arrow-forward-disabled@3x.png b/src/assets/images/arrow-forward-disabled@3x.png Binary files differnew file mode 100644 index 00000000..d079e693 --- /dev/null +++ b/src/assets/images/arrow-forward-disabled@3x.png diff --git a/src/assets/images/arrow-forward-enabled.png b/src/assets/images/arrow-forward-enabled.png Binary files differnew file mode 100644 index 00000000..2d34c78d --- /dev/null +++ b/src/assets/images/arrow-forward-enabled.png diff --git a/src/assets/images/arrow-forward-enabled@2x.png b/src/assets/images/arrow-forward-enabled@2x.png Binary files differnew file mode 100644 index 00000000..b97a2315 --- /dev/null +++ b/src/assets/images/arrow-forward-enabled@2x.png diff --git a/src/assets/images/arrow-forward-enabled@3x.png b/src/assets/images/arrow-forward-enabled@3x.png Binary files differnew file mode 100644 index 00000000..7a658e4b --- /dev/null +++ b/src/assets/images/arrow-forward-enabled@3x.png diff --git a/src/components/common/CenteredView.tsx b/src/components/common/CenteredView.tsx new file mode 100644 index 00000000..1c5ed399 --- /dev/null +++ b/src/components/common/CenteredView.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import {View, StyleSheet, ViewProps} from 'react-native'; + +interface CenteredViewProps extends ViewProps {} +/** + * A centered view that grows to its parents size. + * @param children - children of this component. + */ +const CenteredView: React.FC<CenteredViewProps> = (props) => { + return ( + <View style={styles.centeredView} {...props}> + {props.children} + </View> + ); +}; + +const styles = StyleSheet.create({ + centeredView: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, +}); + +export default CenteredView; diff --git a/src/components/common/LoginInput.tsx b/src/components/common/LoginInput.tsx deleted file mode 100644 index 2a1768a7..00000000 --- a/src/components/common/LoginInput.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import React from 'react'; -import {TextInput, StyleSheet} from 'react-native'; -import * as Animatable from 'react-native-animatable'; - -const LoginInput = (props: LoginInputProps) => { - return ( - <> - <TextInput - accessibilityLabel={ - props.isUsername - ? 'Username text entry box' - : props.isPassword - ? 'Password text entry box' - : undefined - } - accessibilityHint={ - props.isUsername - ? 'Enter your tagg username here' - : props.isPassword - ? 'Enter your tagg password here' - : undefined - } - style={styles.credentials} - placeholder={ - props.isUsername - ? 'Username' - : props.isPassword - ? 'Password' - : undefined - } - placeholderTextColor="#FFFFFF" - autoCompleteType={ - props.isUsername ? 'username' : props.isPassword ? 'password' : 'off' - } - textContentType={ - props.isUsername ? 'username' : props.isPassword ? 'password' : 'none' - } - returnKeyType={ - props.isUsername ? 'next' : props.isPassword ? 'go' : 'default' - } - keyboardType={ - props.isUsername - ? 'ascii-capable' - : props.isPassword - ? 'default' - : undefined - } - autoCapitalize="none" - onChangeText={(input) => props.onChangeText(input)} - defaultValue={props.type} - onSubmitEditing={props.onSubmitEditing} - blurOnSubmit={ - props.isUsername ? false : props.isPassword ? undefined : undefined - } - secureTextEntry={ - props.isUsername ? false : props.isPassword ? true : false - } - ref={props.input_ref} - /> - {props.attempt_submit && !props.isValid && ( - <Animatable.Text - animation="shake" - duration={500} - style={styles.invalidCredentials}> - {props.validationWarning} - </Animatable.Text> - )} - </> - ); -}; - -const styles = StyleSheet.create({ - credentials: { - top: 175, - width: 248, - height: 40, - fontSize: 20, - color: '#FFFFFF', - borderColor: '#FFFDFD', - borderWidth: 2, - borderRadius: 20, - paddingLeft: 13, - marginVertical: 15, - }, - invalidCredentials: { - top: 165, - color: '#F4DDFF', - paddingHorizontal: 30, - textAlign: 'center', - }, -}); - -interface LoginInputProps { - type: string; - isUsername?: boolean; - isPassword?: boolean; - onChangeText: (input: string) => void; - onSubmitEditing?: () => void; - attempt_submit?: boolean; - input_ref?: object; - isValid?: boolean; - validationWarning?: string; -} - -export default LoginInput; diff --git a/src/components/common/OverlayView.tsx b/src/components/common/OverlayView.tsx new file mode 100644 index 00000000..f0660614 --- /dev/null +++ b/src/components/common/OverlayView.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import {View, StyleSheet} from 'react-native'; + +/** + * A blurred & darkened view that grows to its parents size. Designed to be used with overlaid components. + * @param children - children of this component. + */ +const OverlayView: React.FC = ({children}) => { + return <View style={styles.overlayView}>{children}</View>; +}; + +const styles = StyleSheet.create({ + overlayView: { + flex: 1, + backgroundColor: '#00000080', + }, +}); + +export default OverlayView; diff --git a/src/components/common/RadioCheckbox.tsx b/src/components/common/RadioCheckbox.tsx new file mode 100644 index 00000000..33d50527 --- /dev/null +++ b/src/components/common/RadioCheckbox.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { + View, + StyleSheet, + TouchableOpacity, + TouchableOpacityProps, +} from 'react-native'; + +interface RadioCheckboxProps extends TouchableOpacityProps { + checked: boolean; +} +const RadioCheckbox: React.FC<RadioCheckboxProps> = (props) => { + return ( + <TouchableOpacity {...props}> + <View style={styles.outerCircle}> + {props.checked && <View style={styles.innerCircle} />} + </View> + </TouchableOpacity> + ); +}; + +const styles = StyleSheet.create({ + outerCircle: { + width: 23, + height: 23, + borderRadius: 11.5, + borderWidth: 1, + borderColor: '#fff', + alignItems: 'center', + justifyContent: 'center', + }, + innerCircle: { + width: 17, + height: 17, + borderRadius: 8.5, + backgroundColor: '#04ffff', + }, +}); + +export default RadioCheckbox; diff --git a/src/components/common/TaggInput.tsx b/src/components/common/TaggInput.tsx new file mode 100644 index 00000000..fe11d4f0 --- /dev/null +++ b/src/components/common/TaggInput.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import {View, TextInput, StyleSheet, TextInputProps} from 'react-native'; +import * as Animatable from 'react-native-animatable'; + +interface TaggInputProps extends TextInputProps { + valid?: boolean; + invalidWarning?: string; + attemptedSubmit?: boolean; + width?: number | string; +} +/** + * An input component that receives all props a normal TextInput component does. TaggInput components grow to 60% of their parent's width by default, but this can be set using the `width` prop. + */ +const TaggInput = React.forwardRef((props: TaggInputProps, ref: any) => { + return ( + <View style={styles.container}> + <TextInput + style={[{width: props.width}, styles.input]} + placeholderTextColor="#ddd" + clearButtonMode="while-editing" + ref={ref} + {...props} + /> + {props.attemptedSubmit && !props.valid && ( + <Animatable.Text + animation="shake" + duration={500} + style={styles.warning}> + {props.invalidWarning} + </Animatable.Text> + )} + </View> + ); +}); + +const styles = StyleSheet.create({ + container: { + width: '100%', + alignItems: 'center', + marginVertical: 11, + }, + input: { + minWidth: '60%', + height: 40, + fontSize: 16, + fontWeight: '600', + color: '#fff', + borderColor: '#fffdfd', + borderWidth: 2, + borderRadius: 20, + paddingLeft: 13, + }, + warning: { + fontSize: 14, + marginTop: 5, + color: '#f4ddff', + maxWidth: 350, + textAlign: 'center', + }, +}); + +export default TaggInput; diff --git a/src/components/common/index.ts b/src/components/common/index.ts new file mode 100644 index 00000000..b7041b6d --- /dev/null +++ b/src/components/common/index.ts @@ -0,0 +1,4 @@ +export {default as CenteredView} from './CenteredView'; +export {default as OverlayView} from './OverlayView'; +export {default as RadioCheckbox} from './RadioCheckbox'; +export {default as TaggInput} from './TaggInput'; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..724b14ac --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,2 @@ +export * from './common'; +export * from './onboarding'; diff --git a/src/components/onboarding/ArrowButton.tsx b/src/components/onboarding/ArrowButton.tsx new file mode 100644 index 00000000..bf07c6ac --- /dev/null +++ b/src/components/onboarding/ArrowButton.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import {Image, TouchableOpacity, TouchableOpacityProps} from 'react-native'; + +interface ArrowButtonProps extends TouchableOpacityProps { + direction: 'forward' | 'backward'; + disabled?: boolean; +} +const ArrowButton: React.FC<ArrowButtonProps> = (props: ArrowButtonProps) => { + const arrow = + props.direction === 'forward' + ? props.disabled + ? require('../../assets/images/arrow-forward-disabled.png') + : require('../../assets/images/arrow-forward-enabled.png') + : require('../../assets/images/arrow-backward.png'); + + return ( + <TouchableOpacity {...props}> + <Image source={arrow} /> + </TouchableOpacity> + ); +}; + +export default ArrowButton; diff --git a/src/components/onboarding/Background.tsx b/src/components/onboarding/Background.tsx new file mode 100644 index 00000000..98082022 --- /dev/null +++ b/src/components/onboarding/Background.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import LinearGradient from 'react-native-linear-gradient'; +import { + StyleSheet, + TouchableWithoutFeedback, + Keyboard, + ViewProps, + KeyboardAvoidingView, + View, + Platform, +} from 'react-native'; + +interface BackgroundProps extends ViewProps {} +const Background: React.FC<BackgroundProps> = (props) => { + return ( + <LinearGradient + colors={['#9F00FF', '#27EAE9']} + useAngle={true} + angle={154.72} + angleCenter={{x: 0.5, y: 0.5}} + style={styles.container}> + <KeyboardAvoidingView + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} + style={styles.container}> + <TouchableWithoutFeedback accessible={false} onPress={Keyboard.dismiss}> + <View style={[styles.container, styles.view]} {...props}> + {props.children} + </View> + </TouchableWithoutFeedback> + </KeyboardAvoidingView> + </LinearGradient> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + view: { + alignItems: 'center', + }, +}); + +export default Background; diff --git a/src/components/onboarding/RegistrationWizard.tsx b/src/components/onboarding/RegistrationWizard.tsx new file mode 100644 index 00000000..5d7e6ee2 --- /dev/null +++ b/src/components/onboarding/RegistrationWizard.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import {View, StyleSheet, ViewProps} from 'react-native'; + +interface RegistrationWizardProps extends ViewProps { + step: 'one' | 'two' | 'three'; +} + +const RegistrationWizard = (props: RegistrationWizardProps) => { + const stepStyle = styles.step; + const stepActiveStyle = [styles.step, styles.stepActive]; + return ( + <View {...props}> + <View style={styles.container}> + <View style={props.step === 'one' ? stepActiveStyle : stepStyle} /> + <View style={styles.progress} /> + <View style={props.step === 'two' ? stepActiveStyle : stepStyle} /> + <View style={styles.progress} /> + <View style={props.step === 'three' ? stepActiveStyle : stepStyle} /> + </View> + </View> + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + step: { + height: 18, + width: 18, + borderRadius: 9, + borderWidth: 2, + borderColor: '#e1f0ff', + }, + stepActive: { + backgroundColor: '#e1f0ff', + }, + progress: { + width: '30%', + height: 2, + backgroundColor: '#e1f0ff', + }, +}); + +export default RegistrationWizard; diff --git a/src/components/onboarding/TermsConditions.tsx b/src/components/onboarding/TermsConditions.tsx new file mode 100644 index 00000000..5af1b972 --- /dev/null +++ b/src/components/onboarding/TermsConditions.tsx @@ -0,0 +1,140 @@ +import React, {useState} from 'react'; +import { + Modal, + StyleSheet, + View, + Text, + Button, + TouchableOpacity, + ScrollView, + ViewProps, +} from 'react-native'; + +import {RadioCheckbox, CenteredView, OverlayView} from '../common'; +import {dummyTermsConditions} from '../../constants'; + +interface TermsConditionsProps extends ViewProps { + accepted: boolean; + onChange: (accepted: boolean) => void; +} +const TermsConditions: React.FC<TermsConditionsProps> = (props) => { + // boolean representing if modal is visible + const [modalVisible, setModalVisible] = useState(false); + // destructure props + const {accepted, onChange} = props; + /** + * Hides the modal. + */ + const hideModal = (): void => { + if (modalVisible) { + setModalVisible(false); + } + }; + /** + * Sets `accepted` to `true` and hides the modal. + */ + const handleAccept = (): void => { + onChange(true); + hideModal(); + }; + /** + * Toggles the value of `accepted`. + */ + const toggleAccepted = (): void => { + onChange(!accepted); + }; + + return ( + <View {...props}> + <View style={styles.body}> + <RadioCheckbox checked={accepted} onPress={toggleAccepted} /> + <View style={styles.bodyPrompt}> + <Text style={styles.bodyPromptText}>I accept the </Text> + <TouchableOpacity onPress={() => setModalVisible(true)}> + <Text + style={[styles.bodyPromptText, styles.bodyPromptTextUnderline]}> + terms and conditions. + </Text> + </TouchableOpacity> + </View> + </View> + <Modal visible={modalVisible} transparent={true} animationType="fade"> + <OverlayView> + <CenteredView> + <View style={styles.modalView}> + <ScrollView + style={styles.modalScrollView} + contentContainerStyle={styles.modalScrollViewContent}> + <Text style={styles.tcHeader}>Terms and Conditions</Text> + <Text>{dummyTermsConditions}</Text> + </ScrollView> + <View style={styles.modalActions}> + <Button title="Accept" onPress={handleAccept} /> + <View style={styles.modalActionsDivider} /> + <Button title="Close" onPress={hideModal} /> + </View> + </View> + </CenteredView> + </OverlayView> + </Modal> + </View> + ); +}; + +const styles = StyleSheet.create({ + body: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + bodyPrompt: { + flexDirection: 'row', + marginLeft: 10, + }, + bodyPromptText: { + fontSize: 16, + color: '#fff', + }, + bodyPromptTextUnderline: { + textDecorationLine: 'underline', + }, + modalView: { + width: '85%', + height: '55%', + backgroundColor: '#fff', + shadowColor: '#000', + shadowOpacity: 30, + shadowOffset: {width: 0, height: 2}, + shadowRadius: 5, + borderRadius: 8, + paddingTop: 30, + paddingBottom: 15, + paddingHorizontal: 20, + alignItems: 'center', + justifyContent: 'space-between', + }, + modalScrollViewContent: { + justifyContent: 'center', + alignItems: 'center', + }, + modalScrollView: { + marginBottom: 10, + }, + tcHeader: { + fontSize: 18, + fontWeight: 'bold', + }, + modalActions: { + flexDirection: 'row', + justifyContent: 'space-evenly', + alignItems: 'center', + width: '100%', + }, + modalActionsDivider: { + height: '60%', + backgroundColor: '#0160Ca', + width: 1, + }, +}); + +export default TermsConditions; diff --git a/src/components/onboarding/index.ts b/src/components/onboarding/index.ts new file mode 100644 index 00000000..01255c01 --- /dev/null +++ b/src/components/onboarding/index.ts @@ -0,0 +1,4 @@ +export {default as ArrowButton} from './ArrowButton'; +export {default as Background} from './Background'; +export {default as RegistrationWizard} from './RegistrationWizard'; +export {default as TermsConditions} from './TermsConditions'; diff --git a/src/constants/api.ts b/src/constants/api.ts new file mode 100644 index 00000000..0944eb16 --- /dev/null +++ b/src/constants/api.ts @@ -0,0 +1,4 @@ +export const API_ENDPOINT: string = 'http://127.0.0.1:8000/api/'; +export const LOGIN_ENDPOINT: string = 'http://127.0.0.1:8000/api/login/'; +export const LOGOUT_ENDPOINT: string = 'http://127.0.0.1:8000/api/logout/'; +export const REGISTER_ENDPOINT: string = 'http://127.0.0.1:8000/api/register/'; diff --git a/src/constants/index.ts b/src/constants/index.ts index 0667a187..deb89e57 100644 --- a/src/constants/index.ts +++ b/src/constants/index.ts @@ -1,5 +1,3 @@ -// Backend API constants -export const API_ENDPOINT: string = 'http://127.0.0.1:8000/api/'; -export const LOGIN_ENDPOINT: string = 'http://127.0.0.1:8000/api/login/'; -export const LOGOUT_ENDPOINT: string = 'http://127.0.0.1:8000/api/logout/'; -export const REGISTER_ENDPOINT: string = 'http://127.0.0.1:8000/api/register/'; +export * from './api'; +export * from './regex'; +export * from './termsConditions'; diff --git a/src/constants/regex.ts b/src/constants/regex.ts new file mode 100644 index 00000000..350cb855 --- /dev/null +++ b/src/constants/regex.ts @@ -0,0 +1,21 @@ +/** + * The email regex has complex constraints compliant with RFC 5322 standards. More details can be found [here](https://emailregex.com/). + */ +export const emailRegex: RegExp = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + +/** + * The password regex has the following constraints + * - min. 8 chars, max. 120 chars ({8,120}) + * - at least one numeric digit ([0-9]) + * - at least one lowercase letter ([a-z]) + * - at least one uppercase letter ([A-Z]) + * - at least one special character ([^a-zA-z0-9]) + */ +export const passwordRegex: RegExp = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9])(?!.*\s).{8,120}$/; + +/** + * The username regex has the following constraints + * - min. 6 chars, max. 30 chars ({6,30}) + * - match only alphanumerics, underscores, and periods + */ +export const usernameRegex: RegExp = /^[a-zA-Z0-9_.]{6,30}$/; diff --git a/src/constants/termsConditions.ts b/src/constants/termsConditions.ts new file mode 100644 index 00000000..37c2b0e4 --- /dev/null +++ b/src/constants/termsConditions.ts @@ -0,0 +1,11 @@ +export const dummyTermsConditions = ` +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. A condimentum vitae sapien pellentesque habitant morbi tristique senectus. Bibendum neque egestas congue quisque. Semper risus in hendrerit gravida rutrum. Ullamcorper malesuada proin libero nunc consequat interdum varius sit amet. Ultrices neque ornare aenean euismod elementum nisi. Nunc pulvinar sapien et ligula ullamcorper malesuada proin libero nunc. Auctor neque vitae tempus quam pellentesque nec nam aliquam. Vestibulum mattis ullamcorper velit sed. Leo in vitae turpis massa sed elementum tempus egestas. Interdum consectetur libero id faucibus. Vitae purus faucibus ornare suspendisse sed. Accumsan lacus vel facilisis volutpat. Velit sed ullamcorper morbi tincidunt ornare massa eget egestas purus. Nec feugiat nisl pretium fusce id velit ut. Amet consectetur adipiscing elit duis tristique sollicitudin nibh. Ipsum a arcu cursus vitae congue. Ultricies lacus sed turpis tincidunt id aliquet risus feugiat. Facilisis mauris sit amet massa vitae tortor. + +Molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit sed. Condimentum mattis pellentesque id nibh tortor. Nullam non nisi est sit amet. Ipsum consequat nisl vel pretium lectus quam id leo. Vehicula ipsum a arcu cursus vitae congue mauris rhoncus aenean. Blandit massa enim nec dui nunc mattis. Diam vulputate ut pharetra sit amet. Velit laoreet id donec ultrices. Aenean pharetra magna ac placerat vestibulum lectus mauris. Magna fermentum iaculis eu non. Quis imperdiet massa tincidunt nunc. Et tortor consequat id porta nibh venenatis cras sed. Vel facilisis volutpat est velit egestas dui id. Volutpat lacus laoreet non curabitur gravida arcu ac tortor dignissim. Et netus et malesuada fames ac. Semper eget duis at tellus at. Non curabitur gravida arcu ac tortor dignissim convallis. Dui sapien eget mi proin sed libero enim sed. Neque viverra justo nec ultrices dui sapien. Id neque aliquam vestibulum morbi. + +Diam phasellus vestibulum lorem sed risus ultricies. Faucibus pulvinar elementum integer enim. Senectus et netus et malesuada fames. Vitae proin sagittis nisl rhoncus mattis rhoncus urna neque viverra. Tincidunt lobortis feugiat vivamus at augue eget arcu. Lorem ipsum dolor sit amet consectetur adipiscing elit pellentesque. Gravida in fermentum et sollicitudin ac orci. Vitae nunc sed velit dignissim sodales ut eu sem integer. Euismod quis viverra nibh cras. Massa sed elementum tempus egestas sed sed risus pretium quam. Fermentum odio eu feugiat pretium nibh ipsum consequat nisl. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi. Ipsum dolor sit amet consectetur adipiscing elit pellentesque. Sed faucibus turpis in eu mi bibendum neque egestas. Etiam tempor orci eu lobortis elementum nibh tellus molestie. + +In ante metus dictum at tempor commodo ullamcorper. Pharetra vel turpis nunc eget lorem dolor. Platea dictumst vestibulum rhoncus est pellentesque. Maecenas pharetra convallis posuere morbi. Magna sit amet purus gravida. Quam id leo in vitae turpis. Sodales ut etiam sit amet nisl purus in mollis nunc. Euismod elementum nisi quis eleifend quam. Ut porttitor leo a diam. Et ligula ullamcorper malesuada proin libero. In tellus integer feugiat scelerisque varius morbi. Ultricies mi eget mauris pharetra et ultrices neque ornare. Facilisis sed odio morbi quis commodo odio aenean sed. Consequat id porta nibh venenatis cras sed. Tortor vitae purus faucibus ornare. + +Velit euismod in pellentesque massa placerat duis ultricies. Mollis nunc sed id semper risus in hendrerit gravida. Ultrices neque ornare aenean euismod elementum nisi quis eleifend quam. Nunc mattis enim ut tellus elementum sagittis vitae et leo. Dolor sit amet consectetur adipiscing. Sapien faucibus et molestie ac. Sit amet volutpat consequat mauris nunc congue. Nunc id cursus metus aliquam eleifend. Rutrum quisque non tellus orci ac. Massa id neque aliquam vestibulum morbi blandit cursus. Scelerisque mauris pellentesque pulvinar pellentesque habitant morbi tristique senectus et. Viverra tellus in hac habitasse. Urna porttitor rhoncus dolor purus non enim praesent elementum facilisis. Dignissim sodales ut eu sem integer vitae justo eget magna. Gravida arcu ac tortor dignissim convallis. Massa vitae tortor condimentum lacinia quis. Tellus id interdum velit laoreet id donec ultrices tincidunt. Tellus orci ac auctor augue mauris. Proin sed libero enim sed faucibus turpis in eu mi. +`; diff --git a/src/routes/Routes.tsx b/src/routes/Routes.tsx index d96c1d80..94040c1f 100644 --- a/src/routes/Routes.tsx +++ b/src/routes/Routes.tsx @@ -1,11 +1,12 @@ import React from 'react'; import {createStackNavigator} from '@react-navigation/stack'; -import {Login, Registration} from '../screens'; +import {Login, Registration, Verification} from '../screens'; -export type RootStackParams = { +export type RootStackParamList = { Login: undefined; Registration: undefined; + Verification: undefined; }; const RootStack = createStackNavigator<RootStackParamList>(); @@ -20,7 +21,12 @@ const Routes: React.FC<RoutesProps> = ({}) => { component={Login} options={{headerShown: false}} /> - <RootStack.Screen name="Registration" component={Registration} /> + <RootStack.Screen + name="Registration" + component={Registration} + options={{headerShown: false}} + /> + <RootStack.Screen name="Verification" component={Verification} /> </RootStack.Navigator> ); }; diff --git a/src/screens/Login.tsx b/src/screens/Login.tsx index 5291b643..193ef767 100644 --- a/src/screens/Login.tsx +++ b/src/screens/Login.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {useRef} from 'react'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; import { @@ -9,70 +9,74 @@ import { Image, TouchableOpacity, StyleSheet, - Keyboard, - TouchableWithoutFeedback, } from 'react-native'; -import {RootStackParams} from '../routes'; -import LinearGradient from 'react-native-linear-gradient'; - -import LoginInput from '../components/common/LoginInput'; - -import * as Constants from '../constants'; - -type LoginScreenRouteProp = RouteProp<RootStackParams, 'Login'>; -type LoginScreenNavigationProp = StackNavigationProp<RootStackParams, 'Login'>; +import {RootStackParamList} from '../routes'; +import {Background, TaggInput, CenteredView} from '../components'; +import {usernameRegex, LOGIN_ENDPOINT} from '../constants'; +type LoginScreenRouteProp = RouteProp<RootStackParamList, 'Login'>; +type LoginScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'Login' +>; interface LoginProps { route: LoginScreenRouteProp; navigation: LoginScreenNavigationProp; } - -const Login = ({navigation}: LoginProps) => { - const input_ref = React.createRef(); - const [data, setData] = React.useState({ +/** + * Login screen. + * @param navigation react-navigation navigation object. + */ +const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { + // ref for focusing on input fields + const inputRef = useRef(); + // login form state + const [form, setForm] = React.useState({ username: '', password: '', - isValidUser: true, - isValidPassword: true, + isValidUser: false, + isValidPassword: false, + attemptedSubmit: false, }); - /* - Updates the state of username. Also verifies the input of the Username field. - */ + /** + * Updates the state of username. Also verifies the input of the username field by ensuring proper length and characters. + */ const handleUsernameUpdate = (val: string) => { - let validLength: boolean = val.trim().length >= 6; + let validLength: boolean = val.length >= 6; + let validChars: boolean = usernameRegex.test(val); - if (validLength) { - setData({ - ...data, + if (validLength && validChars) { + setForm({ + ...form, username: val, isValidUser: true, }); } else { - setData({ - ...data, + setForm({ + ...form, username: val, isValidUser: false, }); } }; - /* - Updates the state of password. Also verifies the input of the Password field. - */ + /** + * Updates the state of password. Also verifies the input of the password field by ensuring proper length. + */ const handlePasswordUpdate = (val: string) => { let validLength: boolean = val.trim().length >= 8; if (validLength) { - setData({ - ...data, + setForm({ + ...form, password: val, isValidPassword: true, }); } else { - setData({ - ...data, + setForm({ + ...form, password: val, isValidPassword: false, }); @@ -80,23 +84,39 @@ const Login = ({navigation}: LoginProps) => { }; /* - Handler for the Let's Start button or the Go button on the keyboard. + * Handles tap on username keyboard's "Next" button by focusing on password field. + */ + const handleUsernameSubmit = () => { + const passwordField: any = inputRef.current; + if (passwordField) { + passwordField.focus(); + } + }; + + /** + * Handler for the Let's Start button or the Go button on the keyboard. Makes a POST request to the Django login API and presents Alerts based on the status codes that the backend returns. - */ + */ const handleLogin = async () => { + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } try { - if (data.isValidUser && data.isValidPassword) { - let response = await fetch(Constants.LOGIN_ENDPOINT, { + if (form.isValidUser && form.isValidPassword) { + let response = await fetch(LOGIN_ENDPOINT, { method: 'POST', body: JSON.stringify({ - username: data.username, - password: data.password, + username: form.username, + password: form.password, }), }); let statusCode = response.status; if (statusCode === 200) { - Alert.alert('Successfully logged in! 🥳', `Welcome ${data.username}`); + Alert.alert('Successfully logged in! 🥳', `Welcome ${form.username}`); } else if (statusCode === 401) { Alert.alert( 'Login failed 😔', @@ -108,6 +128,9 @@ const Login = ({navigation}: LoginProps) => { "Would you believe me if I told you that I don't know what happened?", ); } + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); } } catch (error) { Alert.alert( @@ -122,135 +145,157 @@ const Login = ({navigation}: LoginProps) => { }; /* - Handler for the submit button on the Username keyboard - */ - const handleUsernameSubmit = () => { - input_ref.current.focus(); - }; - - const handleRegistration = () => { + * Handles tap on "Get Started" text by resetting fields & navigating to the registration page. + */ + const goToRegistration = () => { navigation.navigate('Registration'); + setForm({...form, attemptedSubmit: false}); }; + /** + * Login screen forgot password button. + */ + const ForgotPassword = () => ( + <TouchableOpacity + accessibilityLabel="Forgot password button" + accessibilityHint="Select this if you forgot your tagg password" + style={styles.forgotPassword} + onPress={() => Alert.alert("tagg! You're it!")}> + <Text style={styles.forgotPasswordText}>Forgot password</Text> + </TouchableOpacity> + ); + + /** + * Login screen login button. + */ + const LoginButton = () => ( + <TouchableOpacity + accessibilityLabel="Let's Start!" + accessibilityHint="Select this after entering your tagg username and password" + onPress={handleLogin} + style={styles.start}> + <Text style={styles.startText}>Let's Start!</Text> + </TouchableOpacity> + ); + + /** + * Login screen registration prompt. + */ + const RegistrationPrompt = () => ( + <View style={styles.newUserContainer}> + <Text + accessible={true} + accessibilityLabel="New to tagg?" + style={styles.newUser}> + New to tagg?{' '} + </Text> + <TouchableOpacity + accessibilityLabel="Get started." + accessibilityHint="Select this if you do not have a tagg account"> + <Text + accessible={true} + accessibilityLabel="Get started" + style={styles.getStarted} + onPress={goToRegistration}> + Get started! + </Text> + </TouchableOpacity> + </View> + ); + return ( - <> + <Background> <StatusBar barStyle="light-content" /> - <TouchableWithoutFeedback - onPress={() => { - Keyboard.dismiss(); - }}> - <View style={styles.container}> - <LinearGradient - colors={['#8F00FF', '#6EE7E7']} - style={styles.linearGradient} - useAngle={true} - angle={154.72} - angleCenter={{x: 0.5, y: 0.5}}> - <Image - source={require('../assets/images/logo.png')} - style={styles.logo} - /> - <LoginInput - type={data.username} - isUsername={true} - onChangeText={(user) => handleUsernameUpdate(user)} - onSubmitEditing={() => handleUsernameSubmit()} - isValid={data.isValidUser} - validationWarning={'Username must be at least 6 characters long.'} - /> - <LoginInput - type={data.password} - isPassword={true} - onChangeText={(user) => handlePasswordUpdate(user)} - onSubmitEditing={() => handleLogin()} - isValid={data.isValidPassword} - validationWarning={'Password must be at least 8 characters long.'} - input_ref={input_ref} - /> - <TouchableOpacity - accessibilityLabel="Forgot password button" - accessibilityHint="Select this if you forgot your tagg password" - style={styles.forgotPassword} - onPress={() => Alert.alert("tagg! You're it!")}> - <Text style={styles.forgotPasswordText}>Forgot password</Text> - </TouchableOpacity> - <TouchableOpacity - accessibilityLabel="Let's start button" - accessibilityHint="Select this after entering your tagg username and password" - style={styles.start} - onPress={() => handleLogin()}> - <Text style={styles.startText}>Let's Start!</Text> - </TouchableOpacity> - <Text - accessible={true} - accessibilityLabel="New to tagg?" - style={styles.newUser}> - New to tagg?{' '} - <Text - accessible={true} - accessibilityLabel="Get started" - accessibilityHint="Select this if you do not have a tagg account" - style={styles.getStarted} - onPress={() => handleRegistration()}> - Get started! - </Text> - </Text> - </LinearGradient> - </View> - </TouchableWithoutFeedback> - </> + <CenteredView> + <Image + source={require('../assets/images/logo.png')} + style={styles.logo} + /> + <TaggInput + accessibilityHint="Enter your tagg username here" + accessibilityLabel="Username text entry box" + placeholder="Username" + autoCompleteType="username" + textContentType="username" + returnKeyType="next" + autoCapitalize="none" + onChangeText={handleUsernameUpdate} + onSubmitEditing={handleUsernameSubmit} + blurOnSubmit={false} + valid={form.isValidUser} + invalidWarning="Username must be at least 6 characters and can only contain letters, numbers, periods, and underscores." + attemptedSubmit={form.attemptedSubmit} + width="100%" + /> + + <TaggInput + accessibilityHint="Enter your tagg password here" + accessibilityLabel="Password text entry box" + placeholder="Password" + autoCompleteType="password" + textContentType="password" + returnKeyType="go" + autoCapitalize="none" + secureTextEntry + onChangeText={handlePasswordUpdate} + onSubmitEditing={handleLogin} + valid={form.isValidPassword} + invalidWarning="Password must be at least 8 characters long." + attemptedSubmit={form.attemptedSubmit} + ref={inputRef} + /> + <ForgotPassword /> + <LoginButton /> + <RegistrationPrompt /> + </CenteredView> + </Background> ); }; const styles = StyleSheet.create({ - container: { - flex: 1, - backgroundColor: 'transparent', - }, - linearGradient: { - flex: 1, - alignItems: 'center', - }, logo: { - top: 165, width: 215, height: 149, + marginBottom: '10%', }, forgotPassword: { - top: 190, - left: -60, + marginTop: 10, + marginBottom: 15, }, forgotPasswordText: { - fontSize: 15, - color: '#FFFFFF', + fontSize: 14, + color: '#fff', textDecorationLine: 'underline', }, start: { - top: 195, width: 144, height: 36, justifyContent: 'center', alignItems: 'center', - backgroundColor: '#FFFFFF', - borderRadius: 20, - marginTop: 15, + backgroundColor: '#fff', + borderRadius: 18, + marginBottom: '10%', + }, + startDisabled: { + backgroundColor: '#ddd', }, startText: { - fontSize: 15, - color: '#78A0EF', + fontSize: 16, + color: '#78a0ef', fontWeight: 'bold', }, - getStarted: { - color: '#FFFFFF', - textDecorationLine: 'underline', + newUserContainer: { + flexDirection: 'row', + color: '#fff', }, newUser: { - top: 240, - color: '#F4DDFF', + fontSize: 14, + color: '#f4ddff', }, - invalidCredentials: { - top: 180, - color: '#F4DDFF', + getStarted: { + fontSize: 14, + color: '#fff', + textDecorationLine: 'underline', }, }); diff --git a/src/screens/Registration.tsx b/src/screens/Registration.tsx index 57b0eb18..52508a76 100644 --- a/src/screens/Registration.tsx +++ b/src/screens/Registration.tsx @@ -1,24 +1,448 @@ -import React from 'react'; -import {View, Text, StyleSheet} from 'react-native'; +import React, {useState, useRef} from 'react'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import { + View, + Text, + StyleSheet, + StatusBar, + Alert, + Platform, + TouchableOpacity, +} from 'react-native'; -interface RegistrationProps {} +import {RootStackParamList} from '../routes'; +import { + ArrowButton, + RegistrationWizard, + TaggInput, + TermsConditions, + Background, + CenteredView, +} from '../components'; +import { + emailRegex, + passwordRegex, + usernameRegex, + REGISTER_ENDPOINT, +} from '../constants'; + +type RegistrationScreenRouteProp = RouteProp< + RootStackParamList, + 'Registration' +>; +type RegistrationScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'Registration' +>; +interface RegistrationProps { + route: RegistrationScreenRouteProp; + navigation: RegistrationScreenNavigationProp; +} +/** + * Registration screen. + * @param navigation react-navigation navigation object + */ +const Registration: React.FC<RegistrationProps> = ({navigation}) => { + // refs for changing focus + const lnameRef = useRef(); + const emailRef = useRef(); + const usernameRef = useRef(); + const passwordRef = useRef(); + const confirmRef = useRef(); + /** + * Handles focus change to the next input field. + * @param field key for field to move focus onto + */ + const handleFocusChange = (field: string): void => { + switch (field) { + case 'lname': + const lnameField: any = lnameRef.current; + lnameField.focus(); + break; + case 'email': + const emailField: any = emailRef.current; + emailField.focus(); + break; + case 'username': + const usernameField: any = usernameRef.current; + usernameField.focus(); + break; + case 'password': + const passwordField: any = passwordRef.current; + passwordField.focus(); + break; + case 'confirm': + const confirmField: any = confirmRef.current; + confirmField.focus(); + break; + default: + return; + } + }; + + // registration form state + const [form, setForm] = useState({ + fname: '', + lname: '', + email: '', + username: '', + password: '', + confirm: '', + isValidFname: false, + isValidLname: false, + isValidEmail: false, + isValidUsername: false, + isValidPassword: false, + passwordsMatch: false, + tcAccepted: false, + attemptedSubmit: false, + }); + + /* + * Handles changes to the first name field value and verifies the input by updating state and running a validation function. + */ + const handleFnameUpdate = (fname: string) => { + let isValidFname: boolean = fname.length > 0; + setForm({ + ...form, + fname, + isValidFname, + }); + }; + /* + * Handles changes to the last name field value and verifies the input by updating state and running a validation function. + */ + const handleLnameUpdate = (lname: string) => { + let isValidLname: boolean = lname.length > 0; + setForm({ + ...form, + lname, + isValidLname, + }); + }; + /* + * Handles changes to the email field value and verifies the input by updating state and running a validation function. + */ + const handleEmailUpdate = (email: string) => { + let isValidEmail: boolean = emailRegex.test(email); + setForm({ + ...form, + email, + isValidEmail, + }); + }; + + /* + * Handles changes to the username field value and verifies the input by updating state and running a validation function. + */ + const handleUsernameUpdate = (username: string) => { + let isValidUsername: boolean = usernameRegex.test(username); + setForm({ + ...form, + username, + isValidUsername, + }); + }; + /* + * Handles changes to the password field value and verifies the input by updating state and running a validation function. + */ + const handlePasswordUpdate = (password: string) => { + let isValidPassword: boolean = passwordRegex.test(password); + setForm({ + ...form, + password, + isValidPassword, + }); + }; + + /* + * Handles changes to the confirm password field value and verifies the input by updating state and running a validation function. + */ + const handleConfirmUpdate = (confirm: string) => { + let passwordsMatch: boolean = form.password === confirm; + setForm({ + ...form, + confirm, + passwordsMatch, + }); + }; + + /** + * Handles changes to the terms and conditions accepted boolean. + * @param tcAccepted the boolean to set the terms and conditions value to + */ + const handleTcUpdate = (tcAccepted: boolean) => { + setForm({ + ...form, + tcAccepted, + }); + }; + + /** + * Handles a click on the "next" arrow button by sending an API request to the backend and displaying the appropriate response. + */ + const handleRegister = async () => { + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } + try { + if ( + form.isValidFname && + form.isValidLname && + form.isValidUsername && + form.isValidPassword && + form.passwordsMatch + ) { + if (form.tcAccepted) { + let response = await fetch(REGISTER_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ + first_name: form.fname, + last_name: form.lname, + email: form.email, + username: form.username, + password: form.password, + }), + }); + let statusCode = response.status; + let data = await response.json(); + if (statusCode === 201) { + navigation.navigate('Verification'); + Alert.alert( + "You've successfully registrated!🥳", + `Welcome, ${form.username}`, + ); + } else if (statusCode === 409) { + Alert.alert('Registration failed 😔', `${data}`); + } else { + Alert.alert( + 'Something went wrong! 😭', + "Would you believe me if I told you that I don't know what happened?", + ); + } + } else { + Alert.alert( + 'Terms and conditions', + 'You must first agree to the terms and conditions.', + ); + } + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + } + } catch (error) { + Alert.alert( + 'Looks like our servers are down. 😓', + "Try again in a couple minutes. We're sorry for the inconvenience.", + ); + return { + name: 'Registration error', + description: error, + }; + } + }; -const Registration: React.FC<RegistrationProps> = ({}) => { return ( - <View style={styles.view}> - <Text style={styles.text}>Registration sequence begins here!</Text> - </View> + <Background style={styles.container}> + <StatusBar barStyle="light-content" /> + <CenteredView> + <RegistrationWizard style={styles.wizard} step="one" /> + <View style={styles.form}> + <Text style={styles.formHeader}>SIGN UP</Text> + <TaggInput + accessibilityHint="Enter your first name." + accessibilityLabel="First name input field." + placeholder="First Name" + autoCompleteType="name" + textContentType="name" + returnKeyType="next" + onChangeText={handleFnameUpdate} + onSubmitEditing={() => handleFocusChange('lname')} + blurOnSubmit={false} + valid={form.isValidFname} + invalidWarning="First name cannot be empty." + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter your last name." + accessibilityLabel="Last name input field." + placeholder="Last Name" + autoCompleteType="name" + textContentType="name" + returnKeyType="next" + onChangeText={handleLnameUpdate} + onSubmitEditing={() => handleFocusChange('email')} + blurOnSubmit={false} + ref={lnameRef} + valid={form.isValidLname} + invalidWarning="Last name cannot be empty." + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter your email." + accessibilityLabel="Email input field." + placeholder="Email" + autoCompleteType="email" + textContentType="emailAddress" + autoCapitalize="none" + returnKeyType="next" + keyboardType="email-address" + onChangeText={handleEmailUpdate} + onSubmitEditing={() => handleFocusChange('username')} + blurOnSubmit={false} + ref={emailRef} + valid={form.isValidEmail} + invalidWarning={'Please enter a valid email address.'} + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter a username." + accessibilityLabel="Username input field." + placeholder="Username" + autoCompleteType="username" + textContentType="username" + autoCapitalize="none" + returnKeyType="next" + onChangeText={handleUsernameUpdate} + onSubmitEditing={() => handleFocusChange('password')} + blurOnSubmit={false} + ref={usernameRef} + valid={form.isValidUsername} + invalidWarning={ + 'Username must be 6 characters long and contain only alphanumeric characters.' + } + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter a password." + accessibilityLabel="Password input field." + placeholder="Password" + autoCompleteType="password" + textContentType="newPassword" + returnKeyType="next" + onChangeText={handlePasswordUpdate} + onSubmitEditing={() => handleFocusChange('confirm')} + blurOnSubmit={false} + secureTextEntry + ref={passwordRef} + valid={form.isValidPassword} + invalidWarning={ + 'Password must be 8 characters long & contain at least one lowercase, one uppercase, a number, and a special character.' + } + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint={'Re-enter your password.'} + accessibilityLabel={'Password confirmation input field.'} + placeholder={'Confirm Password'} + autoCompleteType="password" + textContentType="password" + returnKeyType={form.tcAccepted ? 'go' : 'default'} + onChangeText={handleConfirmUpdate} + onSubmitEditing={handleRegister} + secureTextEntry + ref={confirmRef} + valid={form.passwordsMatch} + invalidWarning={'Passwords must match.'} + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TermsConditions + style={styles.tc} + accepted={form.tcAccepted} + onChange={handleTcUpdate} + /> + </View> + <View style={styles.footer}> + <ArrowButton + direction="backward" + onPress={() => navigation.navigate('Login')} + /> + <TouchableOpacity onPress={handleRegister}> + <ArrowButton + direction="forward" + disabled={ + !( + form.isValidFname && + form.isValidLname && + form.isValidEmail && + form.isValidUsername && + form.isValidPassword && + form.passwordsMatch && + form.tcAccepted + ) + } + onPress={handleRegister} + /> + </TouchableOpacity> + </View> + </CenteredView> + </Background> ); }; const styles = StyleSheet.create({ - view: { + container: { flex: 1, - alignSelf: 'center', - justifyContent: 'center', }, - text: { - fontSize: 18, + wizard: { + ...Platform.select({ + ios: { + marginBottom: '18%', + }, + android: { + marginTop: '20%', + marginBottom: '10%', + }, + }), + }, + form: { + alignItems: 'center', + }, + formHeader: { + color: '#fff', + fontSize: 30, + fontWeight: '600', + ...Platform.select({ + ios: { + marginBottom: '6%', + }, + android: { + marginBottom: '2%', + }, + }), + }, + tc: { + ...Platform.select({ + ios: { + marginTop: '5%', + marginBottom: '20%', + }, + android: { + marginTop: '7%', + marginBottom: '12%', + }, + }), + }, + footer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-around', + ...Platform.select({ + android: { + marginBottom: '22%', + }, + }), }, }); + export default Registration; diff --git a/src/screens/Verification.tsx b/src/screens/Verification.tsx new file mode 100644 index 00000000..92032594 --- /dev/null +++ b/src/screens/Verification.tsx @@ -0,0 +1,28 @@ +import React from 'react'; + +import {RootStackParamList} from '../routes'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import {Background, CenteredView} from '../components'; +import {Text} from 'react-native-animatable'; +type LoginScreenRouteProp = RouteProp<RootStackParamList, 'Login'>; +type LoginScreenNavigationProp = StackNavigationProp< + RootStackParamList, + 'Login' +>; +interface VerificationProps { + route: LoginScreenRouteProp; + navigation: LoginScreenNavigationProp; +} + +const Verification: React.FC<VerificationProps> = ({}) => { + return ( + <Background> + <CenteredView> + <Text>Verification!</Text> + </CenteredView> + </Background> + ); +}; + +export default Verification; diff --git a/src/screens/index.ts b/src/screens/index.ts index 60b26b4c..8c8e7b26 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -1,2 +1,3 @@ export {default as Login} from './Login'; export {default as Registration} from './Registration'; +export {default as Verification} from './Verification'; @@ -5092,6 +5092,13 @@ react-is@^16.12.0, react-is@^16.13.0, react-is@^16.8.1, react-is@^16.8.4, react- resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== +react-native-animatable@^1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/react-native-animatable/-/react-native-animatable-1.3.3.tgz#a13a4af8258e3bb14d0a9d839917e9bb9274ec8a" + integrity sha512-2ckIxZQAsvWn25Ho+DK3d1mXIgj7tITkrS4pYDvx96WyOttSvzzFeQnM2od0+FUMzILbdHDsDEqZvnz1DYNQ1w== + dependencies: + prop-types "^15.7.2" + react-native-gesture-handler@^1.6.1: version "1.6.1" resolved "https://registry.yarnpkg.com/react-native-gesture-handler/-/react-native-gesture-handler-1.6.1.tgz#678e2dce250ed66e93af409759be22cd6375dd17" |