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' && (
{
@@ -61,9 +63,25 @@ export default class WelcomeScreen extends React.Component {
style={{ marginTop: 4, padding: 12 }}
onPress={async () => this.guestLogin()}>
- Continue without an account
+ Continue as guest
+
+ Registered in person?
+
+ this.props.navigation.navigate('Reset', { forgot: false })
+ }>
+
+ Set a password
+
+
+
);
diff --git a/screens/auth/validation.js b/screens/auth/validation.js
index fd3963dc..b548c296 100644
--- a/screens/auth/validation.js
+++ b/screens/auth/validation.js
@@ -17,7 +17,7 @@ const validation = {
message: '^Phone number cannot be blank',
},
length: {
- is: 10,
+ is: 14,
message: '^Must be a valid phone number',
},
// To check for only numbers in the future
diff --git a/screens/resources/ResourcesScreen.js b/screens/resources/ResourcesScreen.js
index 3f1f6142..18b95e4f 100644
--- a/screens/resources/ResourcesScreen.js
+++ b/screens/resources/ResourcesScreen.js
@@ -69,8 +69,8 @@ export default class ResourcesScreen extends React.Component {
this.props.navigation.goBack(null)}>
-
+ onPress={() => this.props.navigation.toggleDrawer()}>
+
Resources
diff --git a/screens/rewards/RewardsScreen.js b/screens/rewards/RewardsScreen.js
index b1c65c24..8d966fae 100644
--- a/screens/rewards/RewardsScreen.js
+++ b/screens/rewards/RewardsScreen.js
@@ -74,7 +74,7 @@ export default class RewardsScreen extends React.Component {
}
_logout = async () => {
- this.props.navigation.goBack();
+ this.props.navigation.navigate('Stores');
await AsyncStorage.clear();
Sentry.configureScope((scope) => scope.clear());
this.props.navigation.navigate('Auth', { screen: 'SignUp' });
diff --git a/screens/settings/PhoneNumberChangeScreen.js b/screens/settings/PhoneNumberChangeScreen.js
index c5fbf2d2..5b328aa0 100644
--- a/screens/settings/PhoneNumberChangeScreen.js
+++ b/screens/settings/PhoneNumberChangeScreen.js
@@ -9,6 +9,7 @@ import AuthTextField from '../../components/AuthTextField';
import {
BigTitle,
ButtonLabel,
+ Caption,
FilledButtonContainer,
Subtitle,
} from '../../components/BaseComponents';
@@ -19,7 +20,7 @@ import {
getCustomersByPhoneNumber,
updateCustomers,
} from '../../lib/airtable/request';
-import { formatPhoneNumber, inputFields } from '../../lib/authUtils';
+import { formatPhoneNumberInput, inputFields } from '../../lib/authUtils';
import { logAuthErrorToSentry, logErrorToSentry } from '../../lib/logUtils';
import {
AuthScreenContainer,
@@ -39,12 +40,12 @@ export default class PhoneNumberChangeScreen extends React.Component {
modalVisible: false,
recaptchaVerifier,
verificationId: null,
- formattedPhoneNumber: '',
values: {
[inputFields.PHONENUM]: '',
},
errors: {
[inputFields.PHONENUM]: '',
+ submit: '',
},
};
}
@@ -82,7 +83,7 @@ export default class PhoneNumberChangeScreen 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 },
}));
@@ -99,10 +100,21 @@ export default class PhoneNumberChangeScreen extends React.Component {
onTextChange = async (text, inputField) => {
// Only update error if there is currently an error
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,
+ },
}));
}
};
@@ -112,24 +124,18 @@ export default class PhoneNumberChangeScreen extends React.Component {
};
openRecaptcha = async () => {
- const formattedPhoneNumber = formatPhoneNumber(
- // eslint-disable-next-line react/no-access-state-in-setstate
- this.state.values[inputFields.PHONENUM]
- );
- this.setState({ formattedPhoneNumber });
-
try {
// Update the created record with the new number
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';
logAuthErrorToSentry({
screen: 'PhoneNumberChangeScreen',
action: 'updatePhoneNumber',
- attemptedPhone: formattedPhoneNumber,
+ attemptedPhone: this.state.values[inputFields.PHONENUM],
attemptedPass: null,
error: errorMsg,
});
@@ -149,7 +155,7 @@ export default class PhoneNumberChangeScreen extends React.Component {
logAuthErrorToSentry({
screen: 'PhoneNumberChangeScreen',
action: 'checkDuplicateCustomers',
- attemptedPhone: formattedPhoneNumber,
+ attemptedPhone: this.state.values[inputFields.PHONENUM],
attemptedPass: null,
error: err,
});
@@ -166,7 +172,12 @@ export default class PhoneNumberChangeScreen 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: 'PhoneNumberChangeScreen',
@@ -198,10 +209,9 @@ export default class PhoneNumberChangeScreen extends React.Component {
};
updatePhoneNumber = async () => {
- const { formattedPhoneNumber } = this.state;
try {
await updateCustomers(this.state.customer.id, {
- phoneNumber: this.state.formattedPhoneNumber,
+ phoneNumber: this.state.values[inputFields.PHONENUM],
});
this.setState({ success: true });
} catch (err) {
@@ -212,7 +222,7 @@ export default class PhoneNumberChangeScreen extends React.Component {
logAuthErrorToSentry({
screen: 'PhoneNumberChangeScreen',
action: 'updatePhoneNumber',
- attemptedPhone: formattedPhoneNumber,
+ attemptedPhone: this.state.values[inputFields.PHONENUM],
attemptedPass: null,
error: err,
});
@@ -233,7 +243,7 @@ export default class PhoneNumberChangeScreen extends React.Component {
/>
{this.state.modalVisible && (
Change Phone Number
+
+ You will recieve a text containing a 6-digit verification code.
+ Msg & data rates may apply.
+
+
+ {errors.submit}
+
this.openRecaptcha()}
disabled={!permission}>
- Change Number
+ Verify Number
)}
@@ -276,7 +295,9 @@ export default class PhoneNumberChangeScreen extends React.Component {
Success!
- {`Your phone number was successfully changed to\n ${this.state.formattedPhoneNumber}. Refresh to see changes.`}
+ {`Your phone number was successfully changed to ${
+ this.state.values[inputFields.PHONENUM]
+ }. Refresh to see changes.`}
{
- this.props.navigation.goBack();
- await AsyncStorage.clear();
- Sentry.configureScope((scope) => scope.clear());
- this.props.navigation.navigate('Auth', { screen: 'Welcome' });
- if (signUp) {
- this.props.navigation.navigate('SignUp');
+ let confirm = false;
+ if (!signUp) {
+ confirm = await AlertAsync(
+ `Are you sure you want to ${
+ this.state.isGuest ? 'exit Guest Mode' : 'log out'
+ }?`,
+ '',
+ [
+ {
+ text: this.state.isGuest ? 'Exit' : 'Log Out',
+ onPress: () => true,
+ },
+ {
+ text: 'Cancel',
+ onPress: () => false,
+ style: 'cancel',
+ },
+ ],
+ { cancelable: false }
+ );
+ } else {
+ confirm = true;
+ }
+ if (confirm) {
+ this.props.navigation.navigate('Stores');
+ await AsyncStorage.clear();
+ Sentry.configureScope((scope) => scope.clear());
+ this.props.navigation.navigate(
+ 'Auth',
+ signUp ? { screen: 'SignUp' } : { screen: 'Welcome' }
+ );
}
};
@@ -62,17 +88,18 @@ export default class SettingsScreen extends React.Component {
this.props.navigation.goBack(null)}>
-
+ onPress={() => this.props.navigation.toggleDrawer()}>
+
Settings
-
+
{this.state.isGuest && (
this._logout(true)}
/>
)}
@@ -80,6 +107,7 @@ export default class SettingsScreen extends React.Component {
this.props.navigation.navigate('Name')}
/>
)}
@@ -87,10 +115,11 @@ export default class SettingsScreen extends React.Component {
this.props.navigation.navigate('Number')}
/>
)}
-
+
-
+
Click here to learn more at calblueprint.org