diff options
Diffstat (limited to 'src')
19 files changed, 1486 insertions, 255 deletions
diff --git a/src/assets/icons/purple-plus.png b/src/assets/icons/purple-plus.png Binary files differnew file mode 100644 index 00000000..8b2ce903 --- /dev/null +++ b/src/assets/icons/purple-plus.png diff --git a/src/components/onboarding/BirthDatePicker.tsx b/src/components/onboarding/BirthDatePicker.tsx index 6bef5798..c3a975dc 100644 --- a/src/components/onboarding/BirthDatePicker.tsx +++ b/src/components/onboarding/BirthDatePicker.tsx @@ -46,7 +46,7 @@ const BirthDatePicker = React.forwardRef( {...props}> {(updated || props.showPresetdate) && date ? moment(date).format('MM-DD-YYYY') - : 'Date of Birth'} + : 'Birthday'} </Text> </TouchableOpacity> <Modal visible={!hidden} transparent={true} animationType="fade"> @@ -92,7 +92,7 @@ const styles = StyleSheet.create({ input: { height: 40, fontSize: 16, - paddingTop: '2%', + paddingTop: 8, fontWeight: '600', borderColor: '#fffdfd', borderWidth: 2, diff --git a/src/components/onboarding/RegistrationWizard.tsx b/src/components/onboarding/RegistrationWizard.tsx index 437e7cfb..3c6ca80e 100644 --- a/src/components/onboarding/RegistrationWizard.tsx +++ b/src/components/onboarding/RegistrationWizard.tsx @@ -37,16 +37,6 @@ const RegistrationWizard = (props: RegistrationWizardProps) => { <View style={props.step === 'three' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'four' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'five' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'six' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View - style={props.step === 'seven' ? stepActiveStyle : stepStyle} - /> </View> </Animatable.View> )} @@ -60,16 +50,6 @@ const RegistrationWizard = (props: RegistrationWizardProps) => { <View style={props.step === 'three' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'four' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'five' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View style={props.step === 'six' ? stepActiveStyle : stepStyle} /> - <View style={styles.progress} /> - <View - style={props.step === 'seven' ? stepActiveStyle : stepStyle} - /> </View> </Animatable.View> )} @@ -94,7 +74,7 @@ const styles = StyleSheet.create({ backgroundColor: '#e1f0ff', }, progress: { - width: '10%', + width: '35%', height: 2, backgroundColor: '#e1f0ff', }, diff --git a/src/constants/strings.ts b/src/constants/strings.ts index 353e0d02..104cc198 100644 --- a/src/constants/strings.ts +++ b/src/constants/strings.ts @@ -14,6 +14,7 @@ export const ERROR_DELETED_OBJECT = 'Oh sad! Looks like the comment / moment was export const ERROR_DOUBLE_CHECK_CONNECTION = 'Please double-check your network connection and retry'; export const ERROR_DUP_OLD_PWD = 'You may not use a previously used password'; export const ERROR_EMAIL_IN_USE = 'Email already in use, please try another one'; +export const ERROR_PHONE_IN_USE = 'Phone already in use, please try another one'; export const ERROR_FAILED_LOGIN_INFO = 'Login failed, please try re-entering your login information'; export const ERROR_FAILED_TO_COMMENT = 'Unable to post comment, refresh and try again!'; export const ERROR_FAILED_TO_DELETE_COMMENT = 'Unable to delete comment, refresh and try again!'; @@ -31,7 +32,10 @@ export const ERROR_PROFILE_CREATION_SHORT = 'Profile creation failed π'; export const ERROR_PWD_ACCOUNT = (str: string) => `Please make sure that the email / username entered is registered with us. You may contact our customer support at ${str}`; export const ERROR_REGISTRATION = (str: string) => `Registration failed π, ${str}`; export const ERROR_SELECT_CLASS_YEAR = 'Please select your Class Year'; +export const ERROR_SELECT_BIRTHDAY = 'Please select your birthday'; +export const ERROR_SELECT_GENDER = 'Please select your gender'; export const ERROR_SERVER_DOWN = 'mhm, looks like our servers are down, please refresh and try again in a few mins'; +export const ERROR_TWILIO_SERVER_ERROR = 'mhm, looks like that is an invalid phone number or our servers are down, please try again in a few mins'; export const ERROR_SOMETHING_WENT_WRONG = 'Oh dear, donβt worry someone will be held responsible for this error, In the meantime refresh the app'; export const ERROR_SOMETHING_WENT_WRONG_REFRESH = "Ha, looks like this one's on us, please refresh and try again"; export const ERROR_SOMETHING_WENT_WRONG_RELOAD = "You broke it, Just kidding! we don't know what happened... Please reload the app and try again"; @@ -50,6 +54,8 @@ export const SUCCESS_LINK = (str: string) => `Successfully linked ${str} π`; export const SUCCESS_PIC_UPLOAD = 'Beautiful, the picture was uploaded successfully!'; export const SUCCESS_PWD_RESET = 'Your password was reset successfully!'; export const SUCCESS_VERIFICATION_CODE_SENT = 'New verification code sent! Check your phone messages for your code'; +export const SUCCESS_INVITATION_CODE = 'Perfect! You entered a valid invitation code, you are now able to login and explore Tagg!'; +export const ERROR_NOT_ONBOARDED = 'You are now on waitlist, please enter your invitation code if you have one'; export const UP_TO_DATE = 'Up-to-Date!'; export const UPLOAD_MOMENT_PROMPT_ONE_MESSAGE = 'Post your first moment to\n continue building your digital\nidentity!'; export const UPLOAD_MOMENT_PROMPT_THREE_HEADER = 'Continue to build your profile'; diff --git a/src/routes/main/MainStackScreen.tsx b/src/routes/main/MainStackScreen.tsx index acf0cd28..04f73985 100644 --- a/src/routes/main/MainStackScreen.tsx +++ b/src/routes/main/MainStackScreen.tsx @@ -80,20 +80,6 @@ const MainStackScreen: React.FC<MainStackProps> = ({route}) => { } })(); - const modalStyle: StackNavigationOptions = { - cardStyle: {backgroundColor: 'rgba(80,80,80,0.9)'}, - gestureDirection: 'vertical', - cardOverlayEnabled: true, - cardStyleInterpolator: ({current: {progress}}) => ({ - cardStyle: { - opacity: progress.interpolate({ - inputRange: [0, 0.5, 0.9, 1], - outputRange: [0, 0.25, 0.7, 1], - }), - }, - }), - }; - const tutorialModalStyle: StackNavigationOptions = { cardStyle: {backgroundColor: 'rgba(0, 0, 0, 0.5)'}, gestureDirection: 'vertical', @@ -263,7 +249,7 @@ export const headerBarOptions: ( ), }); -const modalStyle: StackNavigationOptions = { +export const modalStyle: StackNavigationOptions = { cardStyle: {backgroundColor: 'rgba(80,80,80,0.6)'}, gestureDirection: 'vertical', cardOverlayEnabled: true, diff --git a/src/routes/onboarding/OnboardingStackNavigator.tsx b/src/routes/onboarding/OnboardingStackNavigator.tsx index 9f614f7c..0cdeecdf 100644 --- a/src/routes/onboarding/OnboardingStackNavigator.tsx +++ b/src/routes/onboarding/OnboardingStackNavigator.tsx @@ -1,46 +1,41 @@ import {createStackNavigator} from '@react-navigation/stack'; -import { - CategorySelectionScreenType, - TaggPopupType, - UserType, - VerificationScreenType, -} from '../../types'; +import {TaggPopupType, VerificationScreenType} from '../../types'; export type OnboardingStackParams = { - WelcomeScreen: undefined; Login: undefined; + WelcomeScreen: undefined; PasswordResetRequest: undefined; - PasswordReset: { - value: string; - }; - InvitationCodeVerification: undefined; - RegistrationOne: undefined; - RegistrationTwo: {phone: string}; - RegistrationThree: { - firstName: string; - lastName: string; - phone: string; - email: string; - }; - Checkpoint: {username: string; userId: string}; + PasswordReset: {value: string}; Verification: {id: string; screenType: VerificationScreenType}; - ProfileOnboarding: {username: string; userId: string}; - SocialMedia: {username: string; userId: string}; - CategorySelection: { - screenType: CategorySelectionScreenType; - user: UserType; - newCustomCategory: string | undefined; - }; - CreateCustomCategory: { - screenType: CategorySelectionScreenType; - user: UserType; - existingCategories: string[]; - }; + // RegistrationOne: undefined; + // RegistrationTwo: {phone: string}; + // RegistrationThree: { + // firstName: string; + // lastName: string; + // phone: string; + // email: string; + // }; + // Checkpoint: {username: string; userId: string}; + // ProfileOnboarding: {username: string; userId: string}; + // SocialMedia: {username: string; userId: string}; + // CategorySelection: { + // screenType: CategorySelectionScreenType; + // user: UserType; + // newCustomCategory: string | undefined; + // }; + // CreateCustomCategory: { + // screenType: CategorySelectionScreenType; + // user: UserType; + // existingCategories: string[]; + // }; TaggPopup: { popupProps: TaggPopupType; }; - AddWaitlistUser: undefined; - WaitlistSuccess: undefined; + OnboardingStepOne: undefined; + PhoneVerification: {firstName: string; lastName: string; phone: string}; + OnboardingStepTwo: {firstName: string; lastName: string; phone: string}; + OnboardingStepThree: {userId: string; username: string}; + InvitationCodeVerification: {userId: string}; }; export const OnboardingStack = createStackNavigator<OnboardingStackParams>(); diff --git a/src/routes/onboarding/OnboardingStackScreen.tsx b/src/routes/onboarding/OnboardingStackScreen.tsx index 78f113cc..79171efd 100644 --- a/src/routes/onboarding/OnboardingStackScreen.tsx +++ b/src/routes/onboarding/OnboardingStackScreen.tsx @@ -1,24 +1,20 @@ +import {StackCardInterpolationProps} from '@react-navigation/stack'; import React from 'react'; -import {OnboardingStack} from './OnboardingStackNavigator'; +import TaggPopup from '../../components/common/TaggPopup'; import { - Login, InvitationCodeVerification, - RegistrationOne, - RegistrationTwo, - RegistrationThree, - Verification, - ProfileOnboarding, - Checkpoint, - SocialMedia, - PasswordResetRequest, + Login, + OnboardingStepThree, + OnboardingStepTwo, PasswordReset, + PasswordResetRequest, + PhoneVerification, + Verification, WelcomeScreen, - CategorySelection, - AddWaitlistUserScreen, - WaitlistSuccessScreen, } from '../../screens'; -import {StackCardInterpolationProps} from '@react-navigation/stack'; -import TaggPopup from '../../components/common/TaggPopup'; +import OnboardingStepOne from '../../screens/onboarding/OnboardingStepOne'; +import {modalStyle} from '../main'; +import {OnboardingStack} from './OnboardingStackNavigator'; const forFade = ({current}: StackCardInterpolationProps) => ({ cardStyle: { @@ -37,6 +33,7 @@ const Onboarding: React.FC = () => { options={{ gestureEnabled: false, cardStyleInterpolator: forFade, + ...modalStyle, }} /> <OnboardingStack.Screen @@ -46,37 +43,13 @@ const Onboarding: React.FC = () => { gestureEnabled: false, }} /> - <OnboardingStack.Screen - name="WelcomeScreen" - component={WelcomeScreen} - options={{ - gestureEnabled: false, - }} - /> - <OnboardingStack.Screen - name="CategorySelection" - component={CategorySelection} - options={{ - gestureEnabled: false, - }} - /> + <OnboardingStack.Screen name="WelcomeScreen" component={WelcomeScreen} /> <OnboardingStack.Screen name="TaggPopup" component={TaggPopup} options={{ gestureEnabled: false, - cardStyle: { - backgroundColor: 'transparent', - }, - cardOverlayEnabled: true, - cardStyleInterpolator: ({current: {progress}}) => ({ - cardStyle: { - opacity: progress.interpolate({ - inputRange: [0, 0.5, 0.9, 1], - outputRange: [0, 0.25, 0.7, 1], - }), - }, - }), + ...modalStyle, }} /> <OnboardingStack.Screen @@ -84,42 +57,42 @@ const Onboarding: React.FC = () => { component={PasswordReset} options={{ gestureEnabled: false, + ...modalStyle, }} /> <OnboardingStack.Screen - name="InvitationCodeVerification" - component={InvitationCodeVerification} - /> - <OnboardingStack.Screen - name="AddWaitlistUser" - component={AddWaitlistUserScreen} + name="Verification" + component={Verification} + options={{ + ...modalStyle, + }} /> <OnboardingStack.Screen - name="WaitlistSuccess" - component={WaitlistSuccessScreen} + name="OnboardingStepOne" + component={OnboardingStepOne} /> <OnboardingStack.Screen - name="RegistrationOne" - component={RegistrationOne} + name="PhoneVerification" + component={PhoneVerification} + options={{...modalStyle}} /> <OnboardingStack.Screen - name="RegistrationTwo" - component={RegistrationTwo} + name="OnboardingStepTwo" + component={OnboardingStepTwo} + options={{...modalStyle}} /> <OnboardingStack.Screen - name="RegistrationThree" - component={RegistrationThree} + name="OnboardingStepThree" + component={OnboardingStepThree} + options={{...modalStyle}} /> - <OnboardingStack.Screen name="Checkpoint" component={Checkpoint} /> - <OnboardingStack.Screen name="Verification" component={Verification} /> <OnboardingStack.Screen - name="ProfileOnboarding" - component={ProfileOnboarding} + name="InvitationCodeVerification" + component={InvitationCodeVerification} options={{ - gestureEnabled: false, + ...modalStyle, }} /> - <OnboardingStack.Screen name="SocialMedia" component={SocialMedia} /> </OnboardingStack.Navigator> ); }; diff --git a/src/screens/onboarding/InvitationCodeVerification.tsx b/src/screens/onboarding/InvitationCodeVerification.tsx index 903a9912..41d17f29 100644 --- a/src/screens/onboarding/InvitationCodeVerification.tsx +++ b/src/screens/onboarding/InvitationCodeVerification.tsx @@ -1,20 +1,7 @@ -import React from 'react'; -import {OnboardingStackParams} from '../../routes'; +import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; - -import { - Background, - RegistrationWizard, - SubmitButton, - ArrowButton, - LoadingIndicator, -} from '../../components'; - -import { - TAGG_LIGHT_PURPLE, - VERIFY_INVITATION_CODE_ENDPOUNT, -} from '../../constants'; - +import React from 'react'; +import {Alert, KeyboardAvoidingView, StyleSheet, View} from 'react-native'; import {Text} from 'react-native-animatable'; import { CodeField, @@ -23,28 +10,35 @@ import { useClearByFocusCell, } from 'react-native-confirmation-code-field'; import { - StyleSheet, - View, - KeyboardAvoidingView, - Alert, - Platform, -} from 'react-native'; - -import {BackgroundGradientType} from '../../types'; + ArrowButton, + Background, + LoadingIndicator, + SubmitButton, +} from '../../components'; +import {VERIFY_INVITATION_CODE_ENDPOUNT} from '../../constants'; import { ERROR_DOUBLE_CHECK_CONNECTION, ERROR_INVALID_INVITATION_CODE, ERROR_INVLAID_CODE, ERROR_VERIFICATION_FAILED_SHORT, + SUCCESS_INVITATION_CODE, } from '../../constants/strings'; +import {OnboardingStackParams} from '../../routes'; +import {BackgroundGradientType} from '../../types'; +import {SCREEN_WIDTH} from '../../utils'; -type InvitationCodeVerificationScreenNavigationProp = StackNavigationProp< +type InvitationCodeVerificationRouteProp = RouteProp< + OnboardingStackParams, + 'InvitationCodeVerification' +>; +type InvitationCodeVerificationNavigationProp = StackNavigationProp< OnboardingStackParams, 'InvitationCodeVerification' >; interface InvitationCodeVerificationProps { - navigation: InvitationCodeVerificationScreenNavigationProp; + navigation: InvitationCodeVerificationNavigationProp; + route: InvitationCodeVerificationRouteProp; } /** @@ -53,6 +47,7 @@ interface InvitationCodeVerificationProps { */ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({ + route, navigation, }) => { const [value, setValue] = React.useState(''); @@ -66,19 +61,28 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({ if (value.length === 6) { try { let verifyInviteCodeResponse = await fetch( - VERIFY_INVITATION_CODE_ENDPOUNT + value + '/', + VERIFY_INVITATION_CODE_ENDPOUNT + + value + + '/?user_id=' + + route.params.userId, { method: 'DELETE', }, ); if (verifyInviteCodeResponse.status === 200) { - navigation.navigate('RegistrationOne'); + navigation.navigate('Login'); + setTimeout(() => { + Alert.alert(SUCCESS_INVITATION_CODE); + }, 500); } else { Alert.alert(ERROR_INVALID_INVITATION_CODE); } } catch (error) { - Alert.alert(ERROR_VERIFICATION_FAILED_SHORT, ERROR_DOUBLE_CHECK_CONNECTION); + Alert.alert( + ERROR_VERIFICATION_FAILED_SHORT, + ERROR_DOUBLE_CHECK_CONNECTION, + ); return { name: 'Verification error', description: error, @@ -89,10 +93,6 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({ } }; - const navigateToAddWaitList = () => { - navigation.navigate('AddWaitlistUser'); - }; - const Footer = () => ( <View style={styles.footer}> <ArrowButton @@ -107,13 +107,8 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({ centered style={styles.container} gradientType={BackgroundGradientType.Light}> - <RegistrationWizard style={styles.wizard} step="one" /> <KeyboardAvoidingView behavior="padding" style={styles.form}> - <Text style={styles.formHeader}>Enter the code</Text> - <Text style={styles.description}> - Please enter the invitation code provided to you by us / your friend. - (Use all caps.) - </Text> + <Text style={styles.formHeader}>Enter Your Invitation Code</Text> <CodeField ref={ref} {...valueProps} @@ -141,13 +136,10 @@ const InvitationCodeVerification: React.FC<InvitationCodeVerificationProps> = ({ accessibilityHint="Select this after entering your invitation code" onPress={handleInvitationCodeVerification} /> - <View style={styles.noInviteCode}> - <Text style={styles.inviteCodeText}>Don't have an invite? </Text> - <Text style={styles.inviteCodeLink} onPress={navigateToAddWaitList}> - {' '} - Join the Waitlist - </Text> - </View> + <Text style={styles.youveBeenAddedLabel}> + You've been added to the waitlist! We'll notify you when you're at the + front of the line! + </Text> <LoadingIndicator /> </KeyboardAvoidingView> <Footer /> @@ -160,29 +152,17 @@ const styles = StyleSheet.create({ flex: 1, alignItems: 'center', justifyContent: 'center', - }, - wizard: { - marginTop: '3.5%', - flex: 1, - justifyContent: 'center', + borderWidth: 1, }, form: { alignItems: 'center', justifyContent: 'flex-start', - flex: 3, }, formHeader: { color: '#fff', fontSize: 20, fontWeight: 'bold', alignSelf: 'flex-start', - marginBottom: '6%', - marginHorizontal: '10%', - }, - description: { - color: '#fff', - fontWeight: '600', - fontSize: 17, marginHorizontal: '10%', }, codeFieldRoot: { @@ -214,22 +194,19 @@ const styles = StyleSheet.create({ width: '100%', flexDirection: 'row', justifyContent: 'space-around', - ...Platform.select({ - ios: { - bottom: '20%', - }, - android: { - bottom: '10%', - }, - }), }, noInviteCode: { flexDirection: 'row', justifyContent: 'center', }, - inviteCodeText: { - color: TAGG_LIGHT_PURPLE, + youveBeenAddedLabel: { + marginVertical: '5%', + width: SCREEN_WIDTH * 0.8, + color: 'white', + textAlign: 'center', fontSize: 18, + fontWeight: '500', + marginBottom: '10%', }, inviteCodeLink: { color: 'white', diff --git a/src/screens/onboarding/Login.tsx b/src/screens/onboarding/Login.tsx index 450c5039..2ca4172b 100644 --- a/src/screens/onboarding/Login.tsx +++ b/src/screens/onboarding/Login.tsx @@ -1,7 +1,7 @@ import AsyncStorage from '@react-native-community/async-storage'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; -import React, {useEffect, useRef, useState} from 'react'; +import React, {useEffect, useRef} from 'react'; import { Alert, Image, @@ -21,12 +21,13 @@ import { ERROR_FAILED_LOGIN_INFO, ERROR_INVALID_LOGIN, ERROR_LOGIN_FAILED, + ERROR_NOT_ONBOARDED, ERROR_SOMETHING_WENT_WRONG_REFRESH, } from '../../constants/strings'; import {OnboardingStackParams} from '../../routes/onboarding'; import {fcmService} from '../../services'; import {RootState} from '../../store/rootReducer'; -import {BackgroundGradientType, UserType} from '../../types'; +import {BackgroundGradientType} from '../../types'; import {normalize, userLogin} from '../../utils'; import UpdateRequired from './UpdateRequired'; @@ -155,7 +156,7 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { let statusCode = response.status; let data = await response.json(); - if (statusCode === 200) { + if (statusCode === 200 && data.isOnboarded) { //Stores token received in the response into client's AsynStorage try { await AsyncStorage.setItem('token', data.token); @@ -167,6 +168,13 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { console.log(data); Alert.alert(ERROR_INVALID_LOGIN); } + } else if (statusCode === 200 && !data.isOnboarded) { + navigation.navigate('InvitationCodeVerification', { + userId: data.UserID, + }); + setTimeout(() => { + Alert.alert(ERROR_NOT_ONBOARDED); + }, 500); } else if (statusCode === 401) { Alert.alert(ERROR_FAILED_LOGIN_INFO); } else { @@ -192,7 +200,6 @@ const Login: React.FC<LoginProps> = ({navigation}: LoginProps) => { navigation.navigate('WelcomeScreen'); setForm({...form, attemptedSubmit: false}); }; - /** * Login screen forgot password button. */ diff --git a/src/screens/onboarding/OnboardingStepOne.tsx b/src/screens/onboarding/OnboardingStepOne.tsx new file mode 100644 index 00000000..0fa7a6a5 --- /dev/null +++ b/src/screens/onboarding/OnboardingStepOne.tsx @@ -0,0 +1,263 @@ +import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useMemo, useRef, useState} from 'react'; +import { + Alert, + KeyboardAvoidingView, + Platform, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import { + ArrowButton, + Background, + RegistrationWizard, + TaggInput, +} from '../../components'; +import {nameRegex, phoneRegex} from '../../constants'; +import { + ERROR_NEXT_PAGE, + ERROR_PHONE_IN_USE, + ERROR_TWILIO_SERVER_ERROR, +} from '../../constants/strings'; +import {OnboardingStackParams} from '../../routes'; +import {sendOtpStatusCode} from '../../services'; +import {BackgroundGradientType} from '../../types'; +import {SCREEN_HEIGHT} from '../../utils'; + +type OnboardingStepOneNavigationProp = StackNavigationProp< + OnboardingStackParams, + 'OnboardingStepOne' +>; +interface OnboardingStepOneProps { + navigation: OnboardingStepOneNavigationProp; +} + +const OnboardingStepOne: React.FC<OnboardingStepOneProps> = ({navigation}) => { + const lnameRef = useRef(); + const emailRef = useRef(); + const phoneRef = useRef(); + + 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 'phone': + const phoneField: any = phoneRef.current; + phoneField.focus(); + break; + default: + return; + } + }; + + const [form, setForm] = useState({ + fname: '', + lname: '', + phone: '', + isValidFname: false, + isValidLname: false, + isValidPhone: false, + attemptedSubmit: false, + token: '', + }); + + const handleFnameUpdate = (fname: string) => { + fname = fname.trim(); + let isValidFname: boolean = nameRegex.test(fname); + setForm({ + ...form, + fname, + isValidFname, + }); + }; + + const handleLnameUpdate = (lname: string) => { + lname = lname.trim(); + let isValidLname: boolean = nameRegex.test(lname); + setForm({ + ...form, + lname, + isValidLname, + }); + }; + + const handlePhoneUpdate = (phone: string) => { + phone = phone.trim(); + let isValidPhone: boolean = phoneRegex.test(phone); + setForm({ + ...form, + phone, + isValidPhone, + }); + }; + + const goNext = async () => { + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } + try { + if (form.isValidFname && form.isValidLname && form.isValidPhone) { + const code = await sendOtpStatusCode(form.phone); + if (code) { + switch (code) { + case 200: + navigation.navigate('PhoneVerification', { + firstName: form.fname, + lastName: form.lname, + phone: form.phone, + }); + break; + case 409: + Alert.alert(ERROR_PHONE_IN_USE); + break; + default: + Alert.alert(ERROR_TWILIO_SERVER_ERROR); + } + } + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + } + } catch (error) { + Alert.alert(ERROR_NEXT_PAGE); + return { + name: 'Navigation error', + description: error, + }; + } + }; + + const footer = useMemo( + () => ( + <View style={styles.footer}> + <ArrowButton + direction="backward" + onPress={() => navigation.navigate('Login')} + /> + <TouchableOpacity onPress={goNext}> + <ArrowButton + direction="forward" + disabled={ + !(form.isValidFname && form.isValidLname && form.isValidPhone) + } + onPress={goNext} + /> + </TouchableOpacity> + </View> + ), + [form.isValidFname, form.isValidLname, form.isValidPhone], + ); + + return ( + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> + <StatusBar barStyle="light-content" /> + <RegistrationWizard style={styles.wizard} step="one" /> + <KeyboardAvoidingView + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} + style={styles.container}> + <View> + <Text style={styles.formHeader}>ENTER NAME</Text> + </View> + <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="Please enter a valid first name." + 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} + blurOnSubmit={false} + ref={lnameRef} + valid={form.isValidLname} + invalidWarning="Please enter a valid last name." + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + maxLength={10} // currently only support US phone numbers + accessibilityHint="Enter your phone number." + accessibilityLabel="Phone number input field." + placeholder="Phone Number" + autoCompleteType="tel" + textContentType="telephoneNumber" + autoCapitalize="none" + keyboardType="number-pad" + onChangeText={handlePhoneUpdate} + blurOnSubmit={false} + ref={phoneRef} + valid={form.isValidPhone} + invalidWarning={'Please enter a valid 10 digit number.'} + attemptedSubmit={form.attemptedSubmit} + width={280} + onSubmitEditing={goNext} + /> + </KeyboardAvoidingView> + {footer} + </Background> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + wizard: { + position: 'absolute', + top: SCREEN_HEIGHT * 0.1, + }, + formHeader: { + color: '#fff', + fontSize: 30, + fontWeight: '600', + marginBottom: '16%', + }, + load: { + top: '5%', + }, + footer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-around', + ...Platform.select({ + ios: { + bottom: '20%', + }, + android: { + bottom: '10%', + }, + }), + }, +}); + +export default OnboardingStepOne; diff --git a/src/screens/onboarding/OnboardingStepThree.tsx b/src/screens/onboarding/OnboardingStepThree.tsx new file mode 100644 index 00000000..f832539d --- /dev/null +++ b/src/screens/onboarding/OnboardingStepThree.tsx @@ -0,0 +1,411 @@ +import AsyncStorage from '@react-native-community/async-storage'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import moment from 'moment'; +import React, {useMemo} from 'react'; +import { + Alert, + Image, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import ImagePicker from 'react-native-image-crop-picker'; +import Animated from 'react-native-reanimated'; +import { + Background, + BirthDatePicker, + RegistrationWizard, + TaggDropDown, + TaggInput, +} from '../../components'; +import { + CLASS_YEAR_LIST, + EDIT_PROFILE_ENDPOINT, + genderRegex, + TAGG_PURPLE, +} from '../../constants'; +import { + ERROR_DOUBLE_CHECK_CONNECTION, + ERROR_PROFILE_CREATION_SHORT, + ERROR_SELECT_BIRTHDAY, + ERROR_SELECT_CLASS_YEAR, + ERROR_SELECT_GENDER, + ERROR_SOMETHING_WENT_WRONG_REFRESH, + ERROR_UPLOAD_SMALL_PROFILE_PIC, +} from '../../constants/strings'; +import {OnboardingStackParams} from '../../routes/onboarding'; +import {BackgroundGradientType} from '../../types'; +import {normalize, SCREEN_HEIGHT, SCREEN_WIDTH} from '../../utils'; + +type OnboardingStepThreeRouteProp = RouteProp< + OnboardingStackParams, + 'OnboardingStepThree' +>; +type OnboardingStepThreeNavigationProp = StackNavigationProp< + OnboardingStackParams, + 'OnboardingStepThree' +>; +interface OnboardingStepThreeProps { + route: OnboardingStepThreeRouteProp; + navigation: OnboardingStepThreeNavigationProp; +} + +const OnboardingStepThree: React.FC<OnboardingStepThreeProps> = ({ + route, + navigation, +}) => { + const {userId, username} = route.params; + let emptyDate: string | undefined; + const [form, setForm] = React.useState({ + smallPic: '', + birthdate: emptyDate, + gender: '', + isValidGender: true, + classYear: -1, + attemptedSubmit: false, + }); + const [customGender, setCustomGender] = React.useState(false); + + const classYearList = CLASS_YEAR_LIST.map((value) => ({ + label: value, + value, + })); + + /** + * Profile screen "Add profile picture" button + */ + const SmallProfilePic = () => ( + <TouchableOpacity + accessible={true} + accessibilityLabel="ADD PROFILE PICTURE" + onPress={goToGallerySmallPic} + style={styles.smallProfileUploader}> + {form.smallPic ? ( + <Image source={{uri: form.smallPic}} style={styles.smallProfilePic} /> + ) : ( + <Text style={styles.smallProfileText}>ADD PROFILE PICTURE</Text> + )} + </TouchableOpacity> + ); + + const goToGallerySmallPic = () => { + ImagePicker.openPicker({ + smartAlbums: [ + 'Favorites', + 'RecentlyAdded', + 'SelfPortraits', + 'Screenshots', + 'UserLibrary', + ], + width: 580, + height: 580, + cropping: true, + cropperToolbarTitle: 'Select Profile Picture', + mediaType: 'photo', + cropperCircleOverlay: true, + }).then((picture) => { + if ('path' in picture) { + setForm({ + ...form, + smallPic: picture.path, + }); + } + }); + }; + + const handleGenderUpdate = (gender: string) => { + if (gender === 'custom') { + setCustomGender(true); + } else { + setCustomGender(false); + let isValidGender: boolean = true; + setForm({ + ...form, + gender, + isValidGender, + }); + } + }; + + const handleClassYearUpdate = (value: string) => { + console.log('foooooo'); + const classYear = Number.parseInt(value); + setForm({ + ...form, + classYear, + }); + }; + + const handleCustomGenderUpdate = (gender: string) => { + let isValidGender: boolean = genderRegex.test(gender); + gender = gender.replace(' ', '-'); + setForm({ + ...form, + gender, + isValidGender, + }); + }; + + const handleBirthdateUpdate = (birthdate: Date) => { + setForm({ + ...form, + birthdate: birthdate && moment(birthdate).format('YYYY-MM-DD'), + }); + }; + + const handleSubmit = async () => { + if (!form.smallPic) { + Alert.alert(ERROR_UPLOAD_SMALL_PROFILE_PIC); + return; + } + if (form.classYear === -1) { + Alert.alert(ERROR_SELECT_CLASS_YEAR); + return; + } + if (form.birthdate === emptyDate) { + Alert.alert(ERROR_SELECT_BIRTHDAY); + return; + } + if (form.gender === '') { + Alert.alert(ERROR_SELECT_GENDER); + return; + } + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } + let invalidFields: boolean = false; + const request = new FormData(); + if (form.smallPic) { + request.append('smallProfilePicture', { + uri: form.smallPic, + name: 'small_profile_pic.jpg', + type: 'image/jpg', + }); + } + + if (form.birthdate) { + request.append('birthday', form.birthdate); + } + + if (customGender) { + if (form.isValidGender) { + request.append('gender', form.gender); + } else { + setForm({...form, attemptedSubmit: false}); + setTimeout(() => setForm({...form, attemptedSubmit: true})); + invalidFields = true; + } + } else { + if (form.isValidGender) { + request.append('gender', form.gender); + } + } + + if (form.classYear !== -1) { + request.append('university_class', form.classYear); + } + + if (invalidFields) { + return; + } + + const endpoint = EDIT_PROFILE_ENDPOINT + `${userId}/`; + try { + const token = await AsyncStorage.getItem('token'); + let response = await fetch(endpoint, { + method: 'PATCH', + headers: { + 'Content-Type': 'multipart/form-data', + Authorization: 'Token ' + token, + }, + body: request, + }); + console.log(route.params.userId); + let statusCode = response.status; + let data = await response.json(); + if (statusCode === 200) { + navigation.navigate('InvitationCodeVerification', { + userId: route.params.userId, + }); + } else if (statusCode === 400) { + Alert.alert( + 'Profile update failed. π', + data.error || 'Something went wrong! π', + ); + } else { + Alert.alert(ERROR_SOMETHING_WENT_WRONG_REFRESH); + } + } catch (error) { + Alert.alert(ERROR_PROFILE_CREATION_SHORT, ERROR_DOUBLE_CHECK_CONNECTION); + return { + name: 'Profile creation error', + description: error, + }; + } + }; + + const profilePics = useMemo(() => { + return ( + <View style={styles.profile}> + <SmallProfilePic /> + <Image + source={require('../../assets/icons/purple-plus.png')} + style={styles.purplePlus} + /> + </View> + ); + }, [form.largePic, form.smallPic]); + + return ( + <Animated.ScrollView bounces={false}> + <Background + centered + gradientType={BackgroundGradientType.Light} + style={styles.container}> + <StatusBar barStyle="light-content" /> + <RegistrationWizard style={styles.wizard} step="three" /> + {profilePics} + <View style={styles.contentContainer}> + <TaggDropDown + onValueChange={(value: string) => handleClassYearUpdate(value)} + items={classYearList} + placeholder={{ + label: 'Class Year', + value: null, + color: '#ddd', + }} + /> + <BirthDatePicker + handleBDUpdate={handleBirthdateUpdate} + width={280} + date={form.birthdate} + showPresetdate={false} + /> + {customGender && ( + <TaggInput + accessibilityHint="Custom" + accessibilityLabel="Gender input field." + placeholder="Enter your gender" + autoCompleteType="off" + textContentType="none" + autoCapitalize="none" + returnKeyType="next" + blurOnSubmit={false} + onChangeText={handleCustomGenderUpdate} + onSubmitEditing={() => handleSubmit()} + valid={form.isValidGender} + attemptedSubmit={form.attemptedSubmit} + invalidWarning={ + 'Custom field can only contain letters and hyphens' + } + width={280} + /> + )} + <TaggDropDown + onValueChange={(value: string) => handleGenderUpdate(value)} + items={[ + {label: 'Male', value: 'male'}, + {label: 'Female', value: 'female'}, + {label: 'Custom', value: 'custom'}, + ]} + placeholder={{ + label: 'Gender', + value: null, + color: '#ddd', + }} + /> + </View> + <View style={styles.footer}> + <TouchableOpacity onPress={handleSubmit} style={styles.submitBtn}> + <Text style={styles.submitBtnLabel}>Let's start!</Text> + </TouchableOpacity> + </View> + </Background> + </Animated.ScrollView> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + height: SCREEN_HEIGHT, + }, + profile: { + marginTop: '10%', + marginBottom: '5%', + }, + contentContainer: { + position: 'relative', + width: 280, + }, + smallProfileUploader: { + justifyContent: 'center', + alignItems: 'center', + padding: 20, + backgroundColor: '#E1F0FF', + height: normalize(150), + width: normalize(150), + borderRadius: normalize(150), + }, + smallProfileText: { + textAlign: 'center', + fontSize: 14, + fontWeight: 'bold', + color: '#806DF4', + }, + smallProfilePic: { + height: normalize(150), + width: normalize(150), + borderRadius: normalize(150), + borderWidth: 2, + borderColor: 'white', + }, + submitBtn: { + backgroundColor: TAGG_PURPLE, + justifyContent: 'center', + alignItems: 'center', + width: SCREEN_WIDTH / 2.5, + height: SCREEN_WIDTH / 10, + borderRadius: 5, + marginTop: '5%', + alignSelf: 'center', + }, + submitBtnLabel: { + fontSize: 16, + fontWeight: '500', + color: '#fff', + }, + goBack: { + textDecorationLine: 'underline', + color: '#fff', + fontSize: 15, + fontWeight: '600', + }, + footer: { + marginTop: '3%', + alignItems: 'center', + justifyContent: 'space-around', + height: SCREEN_HEIGHT * 0.15, + }, + wizard: { + position: 'absolute', + top: SCREEN_HEIGHT * 0.1, + }, + purplePlus: { + position: 'absolute', + height: normalize(40), + width: normalize(40), + bottom: 0, + right: 0, + }, +}); + +export default OnboardingStepThree; diff --git a/src/screens/onboarding/OnboardingStepTwo.tsx b/src/screens/onboarding/OnboardingStepTwo.tsx new file mode 100644 index 00000000..de869c99 --- /dev/null +++ b/src/screens/onboarding/OnboardingStepTwo.tsx @@ -0,0 +1,369 @@ +import AsyncStorage from '@react-native-community/async-storage'; +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useMemo, useRef, useState} from 'react'; +import { + Alert, + KeyboardAvoidingView, + Platform, + StatusBar, + StyleSheet, + Text, + TouchableOpacity, + View, +} from 'react-native'; +import { + ArrowButton, + Background, + LoadingIndicator, + RegistrationWizard, + TaggInput, + TermsConditions, +} from '../../components'; +import {emailRegex, passwordRegex, usernameRegex} from '../../constants'; +import { + ERROR_DOUBLE_CHECK_CONNECTION, + ERROR_REGISTRATION, + ERROR_SOMETHING_WENT_WRONG_REFRESH, +} from '../../constants/strings'; +import {OnboardingStackParams} from '../../routes'; +import {sendRegister} from '../../services'; +import {BackgroundGradientType} from '../../types'; +import {SCREEN_HEIGHT} from '../../utils'; + +type OnboardingStepTwoRouteProp = RouteProp< + OnboardingStackParams, + 'OnboardingStepTwo' +>; +type OnboardingStepTwoNavigationProp = StackNavigationProp< + OnboardingStackParams, + 'OnboardingStepTwo' +>; +interface OnboardingStepTwoProps { + route: OnboardingStepTwoRouteProp; + navigation: OnboardingStepTwoNavigationProp; +} + +const OnboardingStepTwo: React.FC<OnboardingStepTwoProps> = ({ + route, + navigation, +}) => { + const emailRef = useRef(); + const usernameRef = useRef(); + const passwordRef = useRef(); + const confirmRef = useRef(); + + const handleFocusChange = (field: string): void => { + switch (field) { + 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({ + email: '', + username: '', + password: '', + confirm: '', + isValidEmail: false, + isValidUsername: false, + isValidPassword: false, + passwordsMatch: false, + tcAccepted: false, + attemptedSubmit: false, + }); + + const handleEmailUpdate = (email: string) => { + email = email.trim(); + let isValidEmail: boolean = emailRegex.test(email); + setForm({ + ...form, + email, + isValidEmail, + }); + }; + + const handleUsernameUpdate = (username: string) => { + let isValidUsername: boolean = usernameRegex.test(username); + setForm({ + ...form, + username, + isValidUsername, + }); + }; + + const handlePasswordUpdate = (password: string) => { + let isValidPassword: boolean = passwordRegex.test(password); + let passwordsMatch: boolean = form.password === form.confirm; + setForm({ + ...form, + password, + isValidPassword, + passwordsMatch, + }); + }; + + const handleConfirmUpdate = (confirm: string) => { + let passwordsMatch: boolean = form.password === confirm; + setForm({ + ...form, + confirm, + passwordsMatch, + }); + }; + + const handleTcUpdate = (tcAccepted: boolean) => { + setForm({ + ...form, + tcAccepted, + }); + }; + + const handleRegister = async () => { + if (!form.attemptedSubmit) { + setForm({ + ...form, + attemptedSubmit: true, + }); + } + try { + if ( + form.isValidEmail && + form.isValidUsername && + form.isValidPassword && + form.passwordsMatch + ) { + if (form.tcAccepted) { + const response = await sendRegister( + route.params.firstName, + route.params.lastName, + route.params.phone, + form.email, + form.username, + form.password, + ); + if (response) { + const data = await response.json(); + switch (response.status) { + case 201: + await AsyncStorage.setItem('token', data.token); + navigation.navigate('OnboardingStepThree', { + userId: data.UserID, + username: form.username, + }); + break; + case 400: + Alert.alert(ERROR_REGISTRATION(Object.values(data))); + break; + default: + console.log('fooo'); + Alert.alert(ERROR_SOMETHING_WENT_WRONG_REFRESH); + break; + } + } else { + console.log('barrr'); + Alert.alert(ERROR_SOMETHING_WENT_WRONG_REFRESH); + } + } 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(ERROR_REGISTRATION(ERROR_DOUBLE_CHECK_CONNECTION)); + return { + name: 'Registration error', + description: error, + }; + } + }; + + const footer = useMemo( + () => ( + <View style={styles.footer}> + <ArrowButton + direction="backward" + onPress={() => + navigation.navigate('PhoneVerification', {...route.params}) + } + /> + <TouchableOpacity onPress={handleRegister}> + <ArrowButton + direction="forward" + disabled={ + !( + form.isValidUsername && + form.isValidPassword && + form.passwordsMatch && + form.tcAccepted + ) + } + onPress={handleRegister} + /> + </TouchableOpacity> + </View> + ), + [ + form.isValidEmail, + form.isValidUsername, + form.isValidPassword, + form.passwordsMatch, + form.tcAccepted, + ], + ); + + return ( + <Background + style={styles.container} + gradientType={BackgroundGradientType.Light}> + <StatusBar barStyle="light-content" /> + <RegistrationWizard style={styles.wizard} step="two" /> + <KeyboardAvoidingView + behavior={Platform.OS === 'ios' ? 'padding' : 'height'} + style={styles.container}> + <View> + <Text style={styles.formHeader}>SIGN UP</Text> + </View> + <TaggInput + accessibilityHint="Enter your email." + accessibilityLabel="Email input field." + placeholder="Email" + autoCompleteType="email" + textContentType="emailAddress" + autoCapitalize="none" + returnKeyType="next" + keyboardType="email-address" + onChangeText={handleEmailUpdate} + 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Β at least 6 characters and contain only alphanumerics.' + } + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <TaggInput + accessibilityHint="Enter a password." + accessibilityLabel="Password input field." + placeholder="Password" + autoCompleteType="password" + textContentType="oneTimeCode" + returnKeyType="next" + onChangeText={handlePasswordUpdate} + onSubmitEditing={() => handleFocusChange('confirm')} + blurOnSubmit={false} + secureTextEntry + ref={passwordRef} + valid={form.isValidPassword} + invalidWarning={ + 'Password must be at least 8 characters & contain at least one of a-z, A-Z, 0-9, 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="oneTimeCode" + returnKeyType={form.tcAccepted ? 'go' : 'default'} + onChangeText={handleConfirmUpdate} + onSubmitEditing={handleRegister} + secureTextEntry + ref={confirmRef} + valid={form.passwordsMatch} + invalidWarning={'Passwords must match.'} + attemptedSubmit={form.attemptedSubmit} + width={280} + /> + <LoadingIndicator /> + <TermsConditions + style={styles.tc} + accepted={form.tcAccepted} + onChange={handleTcUpdate} + /> + </KeyboardAvoidingView> + {footer} + </Background> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + wizard: { + position: 'absolute', + top: SCREEN_HEIGHT * 0.1, + }, + formHeader: { + color: '#fff', + fontSize: 30, + fontWeight: '600', + marginBottom: '16%', + }, + tc: { + marginVertical: '5%', + }, + load: { + top: '5%', + }, + footer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-around', + ...Platform.select({ + ios: { + bottom: '20%', + }, + android: { + bottom: '10%', + }, + }), + }, +}); + +export default OnboardingStepTwo; diff --git a/src/screens/onboarding/PasswordReset.tsx b/src/screens/onboarding/PasswordReset.tsx index 11ca60d5..fab77b72 100644 --- a/src/screens/onboarding/PasswordReset.tsx +++ b/src/screens/onboarding/PasswordReset.tsx @@ -227,6 +227,7 @@ const styles = StyleSheet.create({ fontWeight: '600', fontSize: 17, marginHorizontal: '10%', + marginBottom: '10%', }, footer: { width: '100%', diff --git a/src/screens/onboarding/PasswordResetRequest.tsx b/src/screens/onboarding/PasswordResetRequest.tsx index cf086f59..a63eae81 100644 --- a/src/screens/onboarding/PasswordResetRequest.tsx +++ b/src/screens/onboarding/PasswordResetRequest.tsx @@ -1,28 +1,25 @@ -import React, {useState, useRef} from 'react'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useState} from 'react'; import { - View, - Text, - StyleSheet, - StatusBar, Alert, + KeyboardAvoidingView, Platform, + StatusBar, + StyleSheet, + Text, TouchableOpacity, - KeyboardAvoidingView, + View, } from 'react-native'; - -import {OnboardingStackParams} from '../../routes'; - +import {trackPromise} from 'react-promise-tracker'; import { ArrowButton, - TaggInput, Background, LoadingIndicator, + TaggInput, } from '../../components'; - -import {trackPromise} from 'react-promise-tracker'; import {emailRegex, usernameRegex} from '../../constants'; +import {OnboardingStackParams} from '../../routes'; import {handlePasswordResetRequest} from '../../services'; import {BackgroundGradientType, VerificationScreenType} from '../../types'; @@ -123,14 +120,12 @@ const PasswordResetRequest: React.FC<PasswordResetRequestProps> = ({ behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container}> <View> - <Text style={styles.description}> - Enter your registered username / email - </Text> + <Text style={styles.description}>Enter your registered username</Text> </View> <TaggInput - accessibilityHint="Enter a username / email" + accessibilityHint="Enter a username" accessibilityLabel="Input field." - placeholder="Username / Email" + placeholder="Username" autoCompleteType="username" textContentType="username" autoCapitalize="none" diff --git a/src/screens/onboarding/PhoneVerification.tsx b/src/screens/onboarding/PhoneVerification.tsx new file mode 100644 index 00000000..6ec511b3 --- /dev/null +++ b/src/screens/onboarding/PhoneVerification.tsx @@ -0,0 +1,225 @@ +import {RouteProp} from '@react-navigation/native'; +import {StackNavigationProp} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; +import { + Alert, + KeyboardAvoidingView, + Platform, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native'; +import {Text} from 'react-native-animatable'; +import { + CodeField, + Cursor, + useBlurOnFulfill, + useClearByFocusCell, +} from 'react-native-confirmation-code-field'; +import {trackPromise} from 'react-promise-tracker'; +import { + ArrowButton, + Background, + LoadingIndicator, + RegistrationWizard, + SubmitButton, +} from '../../components'; +import {codeRegex} from '../../constants'; +import { + ERROR_INVALID_VERIFICATION_CODE_FORMAT, + ERROR_SOMETHING_WENT_WRONG, +} from '../../constants/strings'; +import {OnboardingStackParams} from '../../routes'; +import {sendOtp, verifyOtp} from '../../services'; +import {BackgroundGradientType} from '../../types'; +import {SCREEN_HEIGHT} from '../../utils'; + +type PhoneVerificationRouteProp = RouteProp< + OnboardingStackParams, + 'PhoneVerification' +>; +type PhoneVerificationNavigationProp = StackNavigationProp< + OnboardingStackParams, + 'PhoneVerification' +>; +interface PhoneVerificationProps { + route: PhoneVerificationRouteProp; + navigation: PhoneVerificationNavigationProp; +} + +const PhoneVerification: React.FC<PhoneVerificationProps> = ({ + route, + navigation, +}) => { + const [value, setValue] = React.useState(''); + const ref = useBlurOnFulfill({value, cellCount: 6}); + const [valueProps, getCellOnLayoutHandler] = useClearByFocusCell({ + value, + setValue, + }); + const {phone} = route.params; + + const handleVerification = async () => { + if (!codeRegex.test(value)) { + Alert.alert(ERROR_INVALID_VERIFICATION_CODE_FORMAT); + return; + } + try { + const success = await trackPromise(verifyOtp(phone, value)); + if (success) { + navigation.navigate('OnboardingStepTwo', { + ...route.params, + }); + } + } catch (error) { + console.log(error); + Alert.alert(ERROR_SOMETHING_WENT_WRONG); + } + }; + + const footer = useMemo( + () => ( + <View style={styles.footer}> + <ArrowButton + direction="backward" + onPress={() => navigation.navigate('OnboardingStepOne')} + /> + </View> + ), + [], + ); + + return ( + <Background + centered + style={styles.container} + gradientType={BackgroundGradientType.Light}> + <RegistrationWizard style={styles.wizard} step="one" /> + <KeyboardAvoidingView behavior="padding" style={styles.form}> + <Text style={styles.formHeader}>Enter 6 digit code</Text> + <Text style={styles.description}> + We sent a 6 digit verification code to the phone number you provided. + </Text> + <CodeField + ref={ref} + {...valueProps} + value={value} + onChangeText={setValue} + cellCount={6} + rootStyle={styles.codeFieldRoot} + keyboardType="number-pad" + textContentType="oneTimeCode" + renderCell={({index, symbol, isFocused}) => ( + <View + onLayout={getCellOnLayoutHandler(index)} + key={index} + style={[styles.cellRoot, isFocused && styles.focusCell]}> + <Text style={styles.cellText}> + {symbol || (isFocused ? <Cursor /> : null)} + </Text> + </View> + )} + /> + <SubmitButton + text="Verify" + color="#fff" + style={styles.button} + accessibilityLabel="Verify" + accessibilityHint="Select this after entering your phone number verification code" + onPress={handleVerification} + /> + <TouchableOpacity onPress={() => sendOtp(phone)}> + <Text style={styles.resend}>Resend Code</Text> + </TouchableOpacity> + <LoadingIndicator /> + </KeyboardAvoidingView> + {footer} + </Background> + ); +}; + +const styles = StyleSheet.create({ + container: { + flex: 1, + alignItems: 'center', + justifyContent: 'center', + }, + wizard: { + position: 'absolute', + top: SCREEN_HEIGHT * 0.1, + }, + form: { + top: '20%', + alignItems: 'center', + justifyContent: 'flex-start', + flex: 3, + }, + formPasswordVerification: { + alignItems: 'center', + justifyContent: 'flex-start', + flex: 3, + top: '35%', + }, + formHeader: { + color: '#fff', + fontSize: 20, + fontWeight: 'bold', + alignSelf: 'flex-start', + marginBottom: '6%', + marginHorizontal: '10%', + }, + description: { + color: '#fff', + fontWeight: '600', + fontSize: 17, + marginHorizontal: '10%', + }, + resend: { + textDecorationLine: 'underline', + color: '#fff', + fontSize: 15, + fontWeight: '600', + }, + codeFieldRoot: { + width: 280, + marginHorizontal: 'auto', + marginVertical: '15%', + }, + cellRoot: { + width: 40, + height: 60, + justifyContent: 'center', + alignItems: 'center', + borderBottomColor: '#fff', + borderBottomWidth: 1, + }, + cellText: { + color: '#fff', + fontSize: 48, + textAlign: 'center', + }, + focusCell: { + borderBottomColor: '#78a0ef', + borderBottomWidth: 2, + }, + button: { + marginVertical: '5%', + }, + loadingIndicator: { + marginVertical: '5%', + }, + footer: { + width: '100%', + flexDirection: 'row', + justifyContent: 'space-around', + ...Platform.select({ + ios: { + bottom: '20%', + }, + android: { + bottom: '10%', + }, + }), + }, +}); +export default PhoneVerification; diff --git a/src/screens/onboarding/Verification.tsx b/src/screens/onboarding/Verification.tsx index 0fbe0d91..dda18364 100644 --- a/src/screens/onboarding/Verification.tsx +++ b/src/screens/onboarding/Verification.tsx @@ -1,16 +1,14 @@ -import React from 'react'; - -import {OnboardingStackParams} from '../../routes'; import {RouteProp} from '@react-navigation/native'; import {StackNavigationProp} from '@react-navigation/stack'; +import React from 'react'; import { - Background, - RegistrationWizard, - SubmitButton, - ArrowButton, - LoadingIndicator, -} from '../../components'; - + Alert, + KeyboardAvoidingView, + Platform, + StyleSheet, + TouchableOpacity, + View, +} from 'react-native'; import {Text} from 'react-native-animatable'; import { CodeField, @@ -18,22 +16,27 @@ import { useBlurOnFulfill, useClearByFocusCell, } from 'react-native-confirmation-code-field'; -import { - StyleSheet, - View, - TouchableOpacity, - KeyboardAvoidingView, - Alert, - Platform, -} from 'react-native'; import {trackPromise} from 'react-promise-tracker'; -import {BackgroundGradientType, VerificationScreenType} from '../../types'; +import { + ArrowButton, + Background, + LoadingIndicator, + RegistrationWizard, + SubmitButton, +} from '../../components'; +import {codeRegex} from '../../constants'; +import { + ERROR_INVALID_VERIFICATION_CODE_FORMAT, + ERROR_SOMETHING_WENT_WRONG, +} from '../../constants/strings'; +import {OnboardingStackParams} from '../../routes'; import { handlePasswordCodeVerification, + handlePasswordResetRequest, sendOtp, verifyOtp, - handlePasswordResetRequest, } from '../../services'; +import {BackgroundGradientType, VerificationScreenType} from '../../types'; type VerificationScreenRouteProp = RouteProp< OnboardingStackParams, @@ -48,12 +51,6 @@ interface VerificationProps { navigation: VerificationScreenNavigationProp; } -import {codeRegex} from '../../constants'; -import { - ERROR_INVALID_VERIFICATION_CODE_FORMAT, - ERROR_SOMETHING_WENT_WRONG, -} from '../../constants/strings'; - const Verification: React.FC<VerificationProps> = ({route, navigation}) => { const [value, setValue] = React.useState(''); const ref = useBlurOnFulfill({value, cellCount: 6}); @@ -217,7 +214,7 @@ const styles = StyleSheet.create({ alignItems: 'center', justifyContent: 'flex-start', flex: 3, - top: '35%', + top: '25%', }, formHeader: { color: '#fff', diff --git a/src/screens/onboarding/WelcomeScreen.tsx b/src/screens/onboarding/WelcomeScreen.tsx index ae31f933..c36a6e05 100644 --- a/src/screens/onboarding/WelcomeScreen.tsx +++ b/src/screens/onboarding/WelcomeScreen.tsx @@ -16,9 +16,6 @@ interface WelcomeScreenProps { } const WelcomeScreen: React.FC<WelcomeScreenProps> = ({navigation}) => { - const handleNext = () => { - navigation.navigate('InvitationCodeVerification'); - }; return ( <Background style={styles.container} @@ -37,7 +34,9 @@ const WelcomeScreen: React.FC<WelcomeScreenProps> = ({navigation}) => { </Text> </View> <TaggSquareButton - onPress={handleNext} + onPress={() => { + navigation.navigate('OnboardingStepOne'); + }} title={'Next'} buttonStyle={'large'} buttonColor={'purple'} diff --git a/src/screens/onboarding/index.ts b/src/screens/onboarding/index.ts index 596683e5..49d7cfb9 100644 --- a/src/screens/onboarding/index.ts +++ b/src/screens/onboarding/index.ts @@ -15,3 +15,7 @@ export {default as AddWaitlistUserScreen} from './AddWaitlistUserScreen'; export {default as WaitlistSuccessScreen} from './WaitlistSuccessScreen'; export {default as CreateCustomCategory} from './CreateCustomCategory'; export {default as UpdateRequired} from './UpdateRequired'; +export {default as OnboardingStepOne} from './OnboardingStepOne'; +export {default as PhoneVerification} from './PhoneVerification'; +export {default as OnboardingStepTwo} from './OnboardingStepTwo'; +export {default as OnboardingStepThree} from './OnboardingStepThree'; diff --git a/src/services/UserProfileService.ts b/src/services/UserProfileService.ts index bfc4933f..dd77db9f 100644 --- a/src/services/UserProfileService.ts +++ b/src/services/UserProfileService.ts @@ -11,6 +11,7 @@ import { PROFILE_INFO_ENDPOINT, PROFILE_PHOTO_ENDPOINT, PROFILE_PHOTO_THUMBNAIL_ENDPOINT, + REGISTER_ENDPOINT, SEND_OTP_ENDPOINT, TAGG_CUSTOMER_SUPPORT, VERIFY_OTP_ENDPOINT, @@ -292,7 +293,6 @@ export const verifyOtp = async (phone: string, otp: string) => { export const sendOtp = async (phone: string) => { try { - console.log(phone); let response = await fetch(SEND_OTP_ENDPOINT, { method: 'POST', body: JSON.stringify({ @@ -313,3 +313,46 @@ export const sendOtp = async (phone: string) => { return false; } }; + +export const sendOtpStatusCode = async (phone: string) => { + try { + let response = await fetch(SEND_OTP_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ + phone_number: '+1' + phone, + }), + }); + + return response.status; + } catch (error) { + console.log(error); + return undefined; + } +}; + +export const sendRegister = async ( + firstName: string, + lastName: string, + phone: string, + email: string, + username: string, + password: string, +) => { + try { + const response = await fetch(REGISTER_ENDPOINT, { + method: 'POST', + body: JSON.stringify({ + first_name: firstName, + last_name: lastName, + email: email, + phone_number: phone, + username: username, + password: password, + }), + }); + return response; + } catch (error) { + console.log(error); + return undefined; + } +}; |