From 5eef95e7320d572b594dae145d656fbbdca1f84a Mon Sep 17 00:00:00 2001 From: Annie Wang Date: Mon, 8 Jun 2020 00:45:48 -0400 Subject: [PATCH] Set password after registering in-store, phone number input formatting (#163) * Set password + phone number input formatting * Update set password copy * Add error message on reCaptcha fail * Update settings styling and logout * Clarifying 'Set a Password' pages copy + alerts * Hamburger drawer design updates * Navigation fixes + alerts --- components/AuthTextField.js | 4 +- components/resources/CategoryBar.js | 20 ++- components/rewards/RewardsFooter.js | 2 +- components/rewards/RewardsHome.js | 2 +- components/settings/SettingsCard.js | 26 ++- constants/Rewards.js | 1 + lib/authUtils.js | 24 ++- navigation/AppNavigator.js | 18 ++- navigation/DrawerContent.js | 84 +++++++--- package.json | 2 + screens/auth/LogInScreen.js | 99 +++++++++--- screens/auth/PasswordResetScreen.js | 166 ++++++++++++++++---- screens/auth/SignUpScreen.js | 126 ++++++++++++--- screens/auth/WelcomeScreen.js | 20 ++- screens/auth/validation.js | 2 +- screens/resources/ResourcesScreen.js | 4 +- screens/rewards/RewardsScreen.js | 2 +- screens/settings/PhoneNumberChangeScreen.js | 65 +++++--- screens/settings/SettingsScreen.js | 62 +++++--- styled/resources.js | 5 +- yarn.lock | 13 ++ 21 files changed, 573 insertions(+), 174 deletions(-) diff --git a/components/AuthTextField.js b/components/AuthTextField.js index 111ac8c5..905b075f 100644 --- a/components/AuthTextField.js +++ b/components/AuthTextField.js @@ -37,13 +37,13 @@ function AuthTextField({ keyboardType={ fieldType.includes('Phone Number') || fieldType === 'Verification Code' - ? 'numeric' + ? 'number-pad' : undefined } maxLength={ // eslint-disable-next-line no-nested-ternary fieldType.includes('Phone Number') - ? 10 + ? 14 : fieldType === 'Verification Code' ? 6 : null diff --git a/components/resources/CategoryBar.js b/components/resources/CategoryBar.js index f734a27c..c3bc9a62 100644 --- a/components/resources/CategoryBar.js +++ b/components/resources/CategoryBar.js @@ -8,12 +8,14 @@ import CircleIcon from '../CircleIcon'; function CategoryBar({ icon, title }) { return ( - - + {icon !== '' && ( + + )} + {title} @@ -21,8 +23,12 @@ function CategoryBar({ icon, title }) { } CategoryBar.propTypes = { - icon: PropTypes.string.isRequired, + icon: PropTypes.string, title: PropTypes.string.isRequired, }; +CategoryBar.defaultProps = { + icon: '', +}; + export default CategoryBar; diff --git a/components/rewards/RewardsFooter.js b/components/rewards/RewardsFooter.js index ba1d9b60..c514c3ae 100644 --- a/components/rewards/RewardsFooter.js +++ b/components/rewards/RewardsFooter.js @@ -75,7 +75,7 @@ export default class RewardsFooter extends React.Component { ? `${this.state.rewards} rewards` : this.state.customer.points === 1 ? `${this.state.customer.points} point` - : `${this.state.customer.points} points`} + : `${this.state.customer.points || 0} points`} )} diff --git a/components/rewards/RewardsHome.js b/components/rewards/RewardsHome.js index 907d0292..b56684e5 100644 --- a/components/rewards/RewardsHome.js +++ b/components/rewards/RewardsHome.js @@ -36,7 +36,7 @@ function RewardsHome({ customer, participating }) { Reward Progress - {`${pointsToNext} / ${rewardPointValue}`} + {`${pointsToNext || 0} / ${rewardPointValue}`} navigation()}> @@ -14,6 +25,15 @@ function SettingsCard({ title, description, navigation, titleColor }) { {description} )} + {rightIcon !== '' && ( + + + + )} ); @@ -24,11 +44,13 @@ SettingsCard.propTypes = { titleColor: PropTypes.string, description: PropTypes.string, navigation: PropTypes.func.isRequired, + rightIcon: PropTypes.string, }; SettingsCard.defaultProps = { titleColor: Colors.activeText, description: '', + rightIcon: '', }; export default SettingsCard; diff --git a/constants/Rewards.js b/constants/Rewards.js index 9b4b94ab..ecce6c4d 100644 --- a/constants/Rewards.js +++ b/constants/Rewards.js @@ -1,2 +1,3 @@ export const rewardDollarValue = 5; export const rewardPointValue = 500; +export const signUpBonus = 500; diff --git a/lib/authUtils.js b/lib/authUtils.js index c4954d50..adc050ad 100644 --- a/lib/authUtils.js +++ b/lib/authUtils.js @@ -1,4 +1,8 @@ import * as Crypto from 'expo-crypto'; +import { + formatIncompletePhoneNumber, + parsePhoneNumberFromString, +} from 'libphonenumber-js'; import { createPushTokens, updateCustomers } from './airtable/request'; import { logErrorToSentry } from './logUtils'; // Fields @@ -54,11 +58,21 @@ export async function updateCustomerPushTokens(customer, newToken) { } } +// Formats full phone numbers by (...) ...-.... or returns null if the number is not 10 digits export function formatPhoneNumber(phoneNumber) { - const onlyNumeric = phoneNumber.replace('[^0-9]', ''); - const formatted = `(${onlyNumeric.slice(0, 3)}) ${onlyNumeric.slice( - 3, - 6 - )}-${onlyNumeric.slice(6, 10)}`; + const parsedPhoneNumber = parsePhoneNumberFromString(`+1${phoneNumber}`); + if (parsedPhoneNumber && parsedPhoneNumber.isPossible()) { + return parsedPhoneNumber.formatNational(); + } + return null; +} + +// Automatically formats phone number input to (...) ...-.... as the user types +export function formatPhoneNumberInput(text) { + let formatted = formatIncompletePhoneNumber(text, 'US'); + // Workaround for a bug that doesn't allow backspacing the closing parenthesis + if (formatted.slice(-1) === ')') { + formatted = formatted.slice(0, -1); + } return formatted; } diff --git a/navigation/AppNavigator.js b/navigation/AppNavigator.js index 07974881..5e393bec 100644 --- a/navigation/AppNavigator.js +++ b/navigation/AppNavigator.js @@ -29,7 +29,6 @@ function DrawerNavigator() { } - drawerStyle={{ width: 189 }} drawerContentOptions={{ labelStyle: { fontFamily: 'poppins-medium', @@ -38,7 +37,12 @@ function DrawerNavigator() { color: Colors.activeText, }, activeTintColor: Colors.primaryGreen, - itemStyle: { marginVertical: 0, marginHorizontal: 0, borderRadius: 0 }, + itemStyle: { + marginVertical: 4, + marginHorizontal: 0, + paddingLeft: 16, + borderRadius: 0, + }, }}> ); diff --git a/navigation/DrawerContent.js b/navigation/DrawerContent.js index f9db6fb7..ff7c853b 100644 --- a/navigation/DrawerContent.js +++ b/navigation/DrawerContent.js @@ -1,30 +1,33 @@ import { DrawerItemList } from '@react-navigation/drawer'; -import { useFocusEffect, useNavigation } from '@react-navigation/native'; -import { Updates } from 'expo'; +import { useFocusEffect } from '@react-navigation/native'; import * as Analytics from 'expo-firebase-analytics'; import PropTypes from 'prop-types'; import React from 'react'; import { Alert, AsyncStorage, Linking, View } from 'react-native'; import * as Sentry from 'sentry-expo'; -import { ButtonContainer, Title } from '../components/BaseComponents'; +import { + BigTitle, + ButtonContainer, + ButtonLabel, + FilledButtonContainer, + Subtitle, + Title, +} from '../components/BaseComponents'; import Colors from '../constants/Colors'; import { getCustomersById } from '../lib/airtable/request'; import { logErrorToSentry } from '../lib/logUtils'; +import { ColumnContainer, SpaceBetweenRowContainer } from '../styled/shared'; function DrawerContent(props) { const [customer, setCustomer] = React.useState(null); const [link, _] = React.useState('http://tiny.cc/RewardsFeedback'); const [isLoading, setIsLoading] = React.useState(true); - const navigation = useNavigation(); const logout = async () => { + props.navigation.navigate('Stores'); await AsyncStorage.clear(); Sentry.configureScope((scope) => scope.clear()); - setTimeout(() => { - navigation.navigate('Auth'); - }, 500); - props.navigation.closeDrawer(); - Updates.reload(); + props.navigation.navigate('Auth', { screen: 'LogIn' }); }; useFocusEffect( @@ -96,6 +99,8 @@ function DrawerContent(props) { return null; } + const isGuest = customer.name === 'Guest'; + return ( - {customer.name} + + + + {customer.name} + + {isGuest && ( + { + logout(); + }}> + Log In + + )} + + {isGuest && ( + + Log in to start saving with Healthy Rewards + + )} + - { props.navigation.goBack(); - props.navigation.navigate('RewardsOverlay'); + setTimeout( + () => + props.navigation.navigate('Stores', { screen: 'RewardsOverlay' }), + 700 + ); }}> - Rewards - - Linking.openURL(link)}> - Feedback + Healthy Rewards + logout()}> - Log Out + style={{ paddingLeft: 24, paddingVertical: 13 }} + onPress={() => Linking.openURL(link)}> + Submit feedback diff --git a/package.json b/package.json index 43c8e75a..3751f7e1 100644 --- a/package.json +++ b/package.json @@ -77,6 +77,7 @@ "geolib": "^3.1.0", "global": "^4.4.0", "install": "^0.13.0", + "libphonenumber-js": "^1.7.52", "npm": "^6.13.4", "prop-types": "^15.0", "react": "16.9.0", @@ -84,6 +85,7 @@ "react-geocode": "^0.2.0", "react-is": "^16.12.0", "react-native": "https://github.com/expo/react-native/archive/sdk-37.0.0.tar.gz", + "react-native-alert-async": "^1.0.5", "react-native-elements": "^1.2.6", "react-native-firebase": "^5.6.0", "react-native-gesture-handler": "~1.6.0", diff --git a/screens/auth/LogInScreen.js b/screens/auth/LogInScreen.js index 672ca62c..a869c980 100644 --- a/screens/auth/LogInScreen.js +++ b/screens/auth/LogInScreen.js @@ -1,4 +1,5 @@ import { FontAwesome5 } from '@expo/vector-icons'; +import { StackActions } from '@react-navigation/native'; import { Notifications } from 'expo'; import Constants from 'expo-constants'; import * as Analytics from 'expo-firebase-analytics'; @@ -10,16 +11,18 @@ import * as Sentry from 'sentry-expo'; import AuthTextField from '../../components/AuthTextField'; import { BigTitle, + Body, ButtonContainer, ButtonLabel, Caption, FilledButtonContainer, + Subtitle, } from '../../components/BaseComponents'; import Colors from '../../constants/Colors'; import { getCustomersByPhoneNumber } from '../../lib/airtable/request'; import { encryptPassword, - formatPhoneNumber, + formatPhoneNumberInput, inputFields, updateCustomerPushTokens, } from '../../lib/authUtils'; @@ -30,7 +33,7 @@ import { BackButton, FormContainer, } from '../../styled/auth'; -import { JustifyCenterContainer } from '../../styled/shared'; +import { JustifyCenterContainer, RowContainer } from '../../styled/shared'; import validate from './validation'; export default class LogInScreen extends React.Component { @@ -90,18 +93,37 @@ export default class LogInScreen extends React.Component { const phoneNumber = values[inputFields.PHONENUM]; const password = values[inputFields.PASSWORD]; - const formattedPhoneNumber = formatPhoneNumber(phoneNumber); - try { let error = ''; let customer = null; // Find customer in Airtable - const customers = await getCustomersByPhoneNumber(formattedPhoneNumber); + const customers = await getCustomersByPhoneNumber(phoneNumber); // Phone number is registered if (customers.length === 1) { [customer] = customers; + if (!customer.password) { + Alert.alert( + 'Phone number registered without a password', + `${ + this.state.values[inputFields.PHONENUM] + } does not have a password yet. Set a password to finish setting up your account.`, + [ + { + text: 'Set a password', + onPress: () => + this.props.navigation.navigate('Reset', { + forgot: false, + }), + }, + { + text: 'Cancel', + style: 'cancel', + }, + ] + ); + } // Check if password is correct // We use the record ID from Airtable as the salt to encrypt @@ -128,7 +150,7 @@ export default class LogInScreen extends React.Component { logAuthErrorToSentry({ screen: 'LogInScreen', action: 'handleSubmit', - attemptedPhone: formattedPhoneNumber, + attemptedPhone: phoneNumber, attemptedPass: password, error, }); @@ -142,7 +164,7 @@ export default class LogInScreen extends React.Component { Sentry.configureScope((scope) => { scope.setUser({ id: customer.id, - phoneNumber: formattedPhoneNumber, + phoneNumber, username: customer.name, }); Sentry.captureMessage('Log In Successful'); @@ -156,7 +178,7 @@ export default class LogInScreen extends React.Component { logAuthErrorToSentry({ screen: 'loginScreen', action: 'handleSubmit', - attemptedPhone: formattedPhoneNumber, + attemptedPhone: phoneNumber, attemptedPass: password, error: err, }); @@ -197,10 +219,21 @@ export default class LogInScreen extends React.Component { // Only update error if there is currently an error // Unless field is password, since it is generally the last field to be filled out if (this.state.errors[inputField]) { - await this.updateError(text, inputField); + await this.updateError( + inputField === inputFields.PHONENUM + ? formatPhoneNumberInput(text) + : text, + inputField + ); } else { this.setState((prevState) => ({ - values: { ...prevState.values, [inputField]: text }, + values: { + ...prevState.values, + [inputField]: + inputField === inputFields.PHONENUM + ? formatPhoneNumberInput(text) + : text, + }, // Clear submission error errors: { ...prevState.errors, submit: '' }, })); @@ -231,6 +264,18 @@ export default class LogInScreen extends React.Component { Log In + + {'If you registered in person with your phone number, '} + + this.props.navigation.navigate('Reset', { forgot: false }) + }> + click here to set a password + + {' to finish setting up your account.'} + + + + this.props.navigation.navigate('Reset', { forgot: true }) + }> + + Forgot Password? + + @@ -260,7 +314,7 @@ export default class LogInScreen extends React.Component { Log in - this.props.navigation.navigate('Reset')}> - - Forgot Password? - - + + {`Don't have an account? `} + + this.props.navigation.dispatch(StackActions.replace('SignUp')) + }> + + Sign Up + + + diff --git a/screens/auth/PasswordResetScreen.js b/screens/auth/PasswordResetScreen.js index 67c0d213..92beecd5 100644 --- a/screens/auth/PasswordResetScreen.js +++ b/screens/auth/PasswordResetScreen.js @@ -1,9 +1,10 @@ import { FontAwesome5 } from '@expo/vector-icons'; +import { StackActions } from '@react-navigation/native'; import { FirebaseRecaptchaVerifierModal } from 'expo-firebase-recaptcha'; import * as firebase from 'firebase'; import PropTypes from 'prop-types'; import React from 'react'; -import { View } from 'react-native'; +import { Alert, View } from 'react-native'; import AuthTextField from '../../components/AuthTextField'; import { BigTitle, @@ -13,6 +14,7 @@ import { Subtitle, } from '../../components/BaseComponents'; import Colors from '../../constants/Colors'; +import { signUpBonus } from '../../constants/Rewards'; import firebaseConfig from '../../firebase'; import { getCustomersByPhoneNumber, @@ -20,7 +22,7 @@ import { } from '../../lib/airtable/request'; import { encryptPassword, - formatPhoneNumber, + formatPhoneNumberInput, inputFields, } from '../../lib/authUtils'; import { logErrorToSentry } from '../../lib/logUtils'; @@ -36,6 +38,7 @@ import VerificationScreen from './VerificationScreen'; export default class PasswordResetScreen extends React.Component { constructor(props) { super(props); + const { forgot } = this.props.route.params; const recaptchaVerifier = React.createRef(); this.state = { customer: null, @@ -45,7 +48,7 @@ export default class PasswordResetScreen extends React.Component { verificationId: null, verified: false, confirmed: false, - formattedPhoneNumber: '', + forgot, values: { [inputFields.PHONENUM]: '', [inputFields.NEWPASSWORD]: '', @@ -55,6 +58,7 @@ export default class PasswordResetScreen extends React.Component { [inputFields.PHONENUM]: '', [inputFields.NEWPASSWORD]: '', [inputFields.VERIFYPASSWORD]: '', + submit: '', }, }; } @@ -86,10 +90,11 @@ export default class PasswordResetScreen extends React.Component { console.log('Not reached'); } this.setState((prevState) => ({ - errors: { ...prevState.errors, [inputField]: errorMsg }, + errors: { ...prevState.errors, [inputField]: errorMsg, submit: '' }, values: { ...prevState.values, [inputField]: text }, confirmed: // Compare with new verifyPassword value + !prevState.errors[inputFields.NEWPASSWORD] && inputField === inputFields.VERIFYPASSWORD ? prevState.verified && prevState.values[inputFields.NEWPASSWORD] === text && @@ -117,10 +122,21 @@ export default class PasswordResetScreen extends React.Component { inputField === inputFields.NEWPASSWORD || inputFields.VERIFYPASSWORD ) { - await this.updateError(text, inputField); + await this.updateError( + inputField === inputFields.PHONENUM + ? formatPhoneNumberInput(text) + : text, + inputField + ); } else { this.setState((prevState) => ({ - values: { ...prevState.values, [inputField]: text }, + values: { + ...prevState.values, + [inputField]: + inputField === inputFields.PHONENUM + ? formatPhoneNumberInput(text) + : text, + }, })); } }; @@ -145,7 +161,12 @@ export default class PasswordResetScreen extends React.Component { this.setState({ verificationId }); this.setModalVisible(true); } catch (err) { - this.setModalVisible(true); + this.setState({ + errors: { + submit: `Error: You must complete the verification pop-up. Make sure your phone number is valid and try again.`, + }, + }); + this.setModalVisible(false); console.log(err); logErrorToSentry({ screen: 'PasswordResetScreen', @@ -177,17 +198,39 @@ export default class PasswordResetScreen extends React.Component { }; findCustomer = async () => { - const formattedPhoneNumber = formatPhoneNumber( - // eslint-disable-next-line react/no-access-state-in-setstate - this.state.values[inputFields.PHONENUM] - ); - this.setState({ formattedPhoneNumber }); try { let customer = null; - const customers = await getCustomersByPhoneNumber(formattedPhoneNumber); + const customers = await getCustomersByPhoneNumber( + this.state.values[inputFields.PHONENUM] + ); if (customers.length === 1) { [customer] = customers; + if (!this.state.forgot) { + if (customer.password) { + Alert.alert( + '', + 'This phone number already has a password set. Log in to access your account.', + [ + { + text: 'Log In', + onPress: () => this.props.navigation.navigate('LogIn'), + }, + { + text: 'Cancel', + style: 'cancel', + }, + ] + ); + return false; + } + } this.setState({ customer }); + } else { + const errorMsg = 'No account registered with this number'; + this.setState((prevState) => ({ + errors: { ...prevState.errors, [inputFields.PHONENUM]: errorMsg }, + })); + return false; } } catch (err) { console.log(err); @@ -208,11 +251,18 @@ export default class PasswordResetScreen extends React.Component { ); // Update the created record with the encrypted password await updateCustomers(this.state.customer.id, { password: encrypted }); + if (!this.state.forgot) { + await updateCustomers(this.state.customer.id, { + points: (this.state.customer.points || 0) + signUpBonus, + }); + } this.setState({ success: true }); }; render() { - const validNumber = !this.state.errors[inputFields.PHONENUM]; + const { errors, values } = this.state; + const validNumber = !errors[inputFields.PHONENUM]; + return ( {this.state.modalVisible && ( )} - this.props.navigation.goBack()}> - + { + if (this.state.verified) { + Alert.alert( + 'Are you sure you want to go back? You will have to verify your phone number again.', + '', + [ + { + text: 'Go back', + onPress: () => this.props.navigation.goBack(), + }, + { + text: 'Cancel', + style: 'cancel', + }, + ] + ); + } else { + this.props.navigation.goBack(); + } + }}> + {!this.state.success && ( + + )} {this.state.verified && !this.state.success && ( - Set New Password + + {this.state.forgot ? 'Set New Password' : 'Set a Password'} + { this.updateError(value, inputFields.NEWPASSWORD); this.scrollView.scrollToEnd({ animated: true }); @@ -250,18 +324,18 @@ export default class PasswordResetScreen extends React.Component { changeTextCallback={(text) => this.onTextChange(text, inputFields.NEWPASSWORD) } - error={this.state.errors[inputFields.NEWPASSWORD]} + error={errors[inputFields.NEWPASSWORD]} /> this.updateError(value, inputFields.VERIFYPASSWORD) } changeTextCallback={(text) => this.onTextChange(text, inputFields.VERIFYPASSWORD) } - error={this.state.errors[inputFields.VERIFYPASSWORD]} + error={errors[inputFields.VERIFYPASSWORD]} /> this.resetPassword()} disabled={!this.state.confirmed}> - - Reset Password - + Set Password )} {!this.state.verified && !this.state.success && ( - Forgot Password + + {this.state.forgot ? 'Forgot Password' : 'Set a password'} + - Enter the phone number connected to your account to reset your - password. + {this.state.forgot + ? 'Enter the phone number connected to your account to reset your password.' + : 'Enter the phone number you used to register in person to finish setting up your account.'} - You will recieve a text containing a 6-digit code to verify your - phone number. Msg & data rates may apply. + You will recieve a text containing a 6-digit verification code. + Msg & data rates may apply. this.updateError(value, inputFields.PHONENUM) } @@ -303,8 +378,13 @@ export default class PasswordResetScreen extends React.Component { this.scrollView.scrollToEnd({ animated: true }); this.onTextChange(text, inputFields.PHONENUM); }} - error={this.state.errors[inputFields.PHONENUM]} + error={errors[inputFields.PHONENUM]} /> + + {errors.submit} + Success! - Your new password was successfully set. + {this.state.forgot + ? 'Your new password was successfully set.' + : 'Your account is fully set up! Next time, you can go straight to Log In to access your account.'} this.props.navigation.navigate('LogIn')}> + onPress={() => { + if (this.state.forgot) { + this.props.navigation.navigate('Auth', { + screen: 'LogIn', + }); + } else { + this.props.navigation.dispatch( + StackActions.replace('LogIn') + ); + } + this.setState({ + success: false, + verified: false, + forgot: true, + }); + }}> Go to Log In @@ -342,4 +439,5 @@ export default class PasswordResetScreen extends React.Component { PasswordResetScreen.propTypes = { navigation: PropTypes.object.isRequired, + route: PropTypes.object.isRequired, }; diff --git a/screens/auth/SignUpScreen.js b/screens/auth/SignUpScreen.js index 93c01b05..e9cbaa6d 100644 --- a/screens/auth/SignUpScreen.js +++ b/screens/auth/SignUpScreen.js @@ -1,4 +1,5 @@ import { FontAwesome5 } from '@expo/vector-icons'; +import { StackActions } from '@react-navigation/native'; import { Notifications } from 'expo'; import Constants from 'expo-constants'; import * as Analytics from 'expo-firebase-analytics'; @@ -12,11 +13,16 @@ import * as Sentry from 'sentry-expo'; import AuthTextField from '../../components/AuthTextField'; import { BigTitle, + Body, + ButtonContainer, ButtonLabel, + Caption, FilledButtonContainer, + Subtitle, } from '../../components/BaseComponents'; import Colors from '../../constants/Colors'; import RecordIds from '../../constants/RecordIds'; +import { signUpBonus } from '../../constants/Rewards'; import { env } from '../../environment'; import firebaseConfig from '../../firebase'; import { @@ -27,7 +33,7 @@ import { } from '../../lib/airtable/request'; import { encryptPassword, - formatPhoneNumber, + formatPhoneNumberInput, inputFields, } from '../../lib/authUtils'; import { logAuthErrorToSentry, logErrorToSentry } from '../../lib/logUtils'; @@ -37,6 +43,7 @@ import { BackButton, FormContainer, } from '../../styled/auth'; +import { RowContainer } from '../../styled/shared'; import validate from './validation'; import VerificationScreen from './VerificationScreen'; @@ -47,7 +54,6 @@ export default class SignUpScreen extends React.Component { super(props); const recaptchaVerifier = React.createRef(); this.state = { - formattedPhoneNumber: '', modalVisible: false, recaptchaVerifier, verificationId: null, @@ -60,6 +66,7 @@ export default class SignUpScreen extends React.Component { [inputFields.NAME]: '', [inputFields.PHONENUM]: '', [inputFields.PASSWORD]: '', + submit: '', }, token: '', }; @@ -151,7 +158,7 @@ export default class SignUpScreen extends React.Component { name, phoneNumber, // 2020/4/29 update for Nam's launch - points: 500, + points: signUpBonus, pushTokenIds: pushTokenId ? [pushTokenId] : null, }); @@ -192,21 +199,40 @@ export default class SignUpScreen extends React.Component { handleSubmit = async () => { try { // Check for duplicates first - const formattedPhoneNumber = formatPhoneNumber( - // eslint-disable-next-line react/no-access-state-in-setstate - this.state.values[inputFields.PHONENUM] - ); - this.setState({ formattedPhoneNumber }); const duplicateCustomers = await getCustomersByPhoneNumber( - formattedPhoneNumber + this.state.values[inputFields.PHONENUM] ); if (duplicateCustomers.length !== 0) { console.log('Duplicate customer'); - const errorMsg = 'Phone number already in use.'; + const errorMsg = 'Phone number already in use'; + if ( + duplicateCustomers.length === 1 && + !duplicateCustomers[0].password + ) { + Alert.alert( + 'Phone number registered without a password', + `${ + this.state.values[inputFields.PHONENUM] + } does not have a password yet. Set a password to finish setting up your account.`, + [ + { + text: 'Set a password', + onPress: () => + this.props.navigation.navigate('Reset', { + forgot: false, + }), + }, + { + text: 'Cancel', + style: 'cancel', + }, + ] + ); + } logAuthErrorToSentry({ screen: 'SignUpScreen', action: 'handleSubmit', - attemptedPhone: formattedPhoneNumber, + attemptedPhone: this.state.values[inputFields.PHONENUM], attemptedPass: null, error: errorMsg, }); @@ -235,6 +261,9 @@ export default class SignUpScreen extends React.Component { }; openRecaptcha = async () => { + // const number = '+1'.concat( + // this.state.values[inputFields.PHONENUM].replace(/\D/g, '') + // ); const number = '+1'.concat(this.state.values[inputFields.PHONENUM]); const phoneProvider = new firebase.auth.PhoneAuthProvider(); try { @@ -246,7 +275,12 @@ export default class SignUpScreen extends React.Component { this.setState({ verificationId }); this.setModalVisible(true); } catch (err) { - this.setModalVisible(true); + this.setState({ + errors: { + submit: `Error: You must complete the verification pop-up. Make sure your phone number is valid and try again.`, + }, + }); + this.setModalVisible(false); console.log(err); logErrorToSentry({ screen: 'SignUpScreen', @@ -307,7 +341,7 @@ export default class SignUpScreen extends React.Component { } this.setState((prevState) => ({ - errors: { ...prevState.errors, [inputField]: errorMsg }, + errors: { ...prevState.errors, [inputField]: errorMsg, submit: '' }, values: { ...prevState.values, [inputField]: text }, })); @@ -325,10 +359,21 @@ export default class SignUpScreen extends React.Component { // Only update error if there is currently an error // Unless field is password, since it is generally the last field to be filled out if (this.state.errors[inputField] || inputField === inputFields.PASSWORD) { - await this.updateError(text, inputField); + await this.updateError( + inputField === inputFields.PHONENUM + ? formatPhoneNumberInput(text) + : text, + inputField + ); } else { this.setState((prevState) => ({ - values: { ...prevState.values, [inputField]: text }, + values: { + ...prevState.values, + [inputField]: + inputField === inputFields.PHONENUM + ? formatPhoneNumberInput(text) + : text, + }, })); } }; @@ -357,7 +402,7 @@ export default class SignUpScreen extends React.Component { }}> {this.state.modalVisible && ( - this.props.navigation.goBack(null)}> + this.props.navigation.goBack()}> Sign Up + + {'If you registered in person with your phone number, '} + + this.props.navigation.navigate('Reset', { forgot: false }) + }> + click here to set a password + + {' to finish setting up your account.'} + { this.updateError(value, inputFields.NAME); this.scrollView.scrollToEnd({ animated: true }); @@ -384,12 +440,12 @@ export default class SignUpScreen extends React.Component { changeTextCallback={async (text) => this.onTextChange(text, inputFields.NAME) } - error={this.state.errors[inputFields.NAME]} + error={errors[inputFields.NAME]} /> { this.updateError(value, inputFields.PHONENUM); this.scrollView.scrollToEnd({ animated: true }); @@ -397,31 +453,51 @@ export default class SignUpScreen extends React.Component { changeTextCallback={(text) => this.onTextChange(text, inputFields.PHONENUM) } - error={this.state.errors[inputFields.PHONENUM]} + error={errors[inputFields.PHONENUM]} /> this.updateError(value, inputFields.PASSWORD) } changeTextCallback={(text) => this.onTextChange(text, inputFields.PASSWORD) } - error={this.state.errors[inputFields.PASSWORD]} + error={errors[inputFields.PASSWORD]} /> + + {errors.submit} + this.handleSubmit()} disabled={!signUpPermission}> - Sign Up + Continue + + + {`Already have an account? `} + + this.props.navigation.dispatch(StackActions.replace('LogIn')) + }> + + Log In + + + {env === 'dev' && (