From a307e3d3b21c87314be4ea265227bb625e685919 Mon Sep 17 00:00:00 2001 From: maftalion Date: Tue, 30 Mar 2021 19:40:29 -0700 Subject: [PATCH 01/13] Allow secondary logins --- src/ROUTES.js | 2 + src/libs/API.js | 2 +- .../AppNavigator/ModalStackNavigators.js | 19 +++ src/libs/Navigation/linkingConfig.js | 8 + src/libs/actions/User.js | 19 ++- src/pages/settings/AddLoginPage.js | 155 ++++++++++++++++++ src/pages/settings/ProfilePage/LoginField.js | 73 +++++++++ .../{ProfilePage.js => ProfilePage/index.js} | 103 ++++++++---- src/styles/utilities/spacing.js | 4 + 9 files changed, 352 insertions(+), 33 deletions(-) create mode 100644 src/pages/settings/AddLoginPage.js create mode 100644 src/pages/settings/ProfilePage/LoginField.js rename src/pages/settings/{ProfilePage.js => ProfilePage/index.js} (77%) diff --git a/src/ROUTES.js b/src/ROUTES.js index 0fe51bf894f1..314bf03db7ca 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -8,6 +8,8 @@ export default { SETTINGS_PREFERENCES: 'settings/preferences', SETTINGS_PASSWORD: 'settings/password', SETTINGS_PAYMENTS: 'settings/payments', + SETTINGS_ADD_PHONE: 'settings/add-phone', + SETTINGS_ADD_EMAIL: 'settings/add-email', NEW_GROUP: 'new/group', NEW_CHAT: 'new/chat', REPORT: 'r', diff --git a/src/libs/API.js b/src/libs/API.js index 36a1786b4d89..833c45a89014 100644 --- a/src/libs/API.js +++ b/src/libs/API.js @@ -529,7 +529,7 @@ function Report_UpdateLastRead(parameters) { /** * @param {Object} parameters - * @param {Number} parameters.email + * @param {String} parameters.email * @returns {Promise} */ function ResendValidateCode(parameters) { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 83984d1ccc4c..8596fae54e28 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -13,6 +13,7 @@ import SettingsProfilePage from '../../../pages/settings/ProfilePage'; import SettingsPreferencesPage from '../../../pages/settings/PreferencesPage'; import SettingsPasswordPage from '../../../pages/settings/PasswordPage'; import SettingsPaymentsPage from '../../../pages/settings/PaymentsPage'; +import SettingsAddLoginPage from '../../../pages/settings/AddLoginPage'; // Setup the modal stack navigators so we only have to create them once const SettingsModalStack = createStackNavigator(); @@ -146,6 +147,24 @@ const SettingsModalStackNavigator = () => ( name="Settings_Profile" component={SettingsProfilePage} /> + + { if (response.jsonCode === 200) { const loginList = _.where(response.loginList, {partnerName: 'expensify.com'}); Onyx.merge(ONYXKEYS.USER, {loginList}); + } else { + const error = lodashGet(response, 'message', 'Unable to add phone number. Please try again.'); + Onyx.merge(ONYXKEYS.USER, {error}); } + return response; + }).finally((response) => { + Onyx.merge(ONYXKEYS.ACCOUNT, {loading: false}); + return response; }); } diff --git a/src/pages/settings/AddLoginPage.js b/src/pages/settings/AddLoginPage.js new file mode 100644 index 000000000000..baff75a5fcce --- /dev/null +++ b/src/pages/settings/AddLoginPage.js @@ -0,0 +1,155 @@ +import React, {Component} from 'react'; +import Onyx, {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; +import {View, TextInput} from 'react-native'; +import _ from 'underscore'; +import Str from 'expensify-common/lib/str'; +import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; +import Navigation from '../../libs/Navigation/Navigation'; +import ScreenWrapper from '../../components/ScreenWrapper'; +import Text from '../../components/Text'; +import styles from '../../styles/styles'; +import {setSecondaryLogin} from '../../libs/actions/User'; +import ONYXKEYS from '../../ONYXKEYS'; +import ButtonWithLoader from '../../components/ButtonWithLoader'; +import ROUTES from '../../ROUTES'; + +const propTypes = { + /* Onyx Props */ + // The details about the user that is signed in + user: PropTypes.shape({ + // error associated with adding a secondary login + error: PropTypes.string, + + // Whether the form is being submitted + loading: PropTypes.bool, + + // Whether or not the user is subscribed to news updates + loginList: PropTypes.arrayOf(PropTypes.shape({ + + // Value of partner name + partnerName: PropTypes.string, + + // Phone/Email associated with user + partnerUserID: PropTypes.string, + + // Date of when login was validated + validatedDate: PropTypes.string, + })), + }), + + // Route object from navigation + route: PropTypes.shape({ + params: PropTypes.shape({ + type: PropTypes.string, + }), + }), +}; + +const defaultProps = { + user: {}, + route: {}, +}; + +class AddLoginPage extends Component { + constructor(props) { + super(props); + + this.state = { + login: '', + password: '', + }; + this.formType = props.route.params.type; + this.submitForm = this.submitForm.bind(this); + this.validateForm = this.validateForm.bind(this); + } + + componentWillUnmount() { + Onyx.merge(ONYXKEYS.USER, {error: ''}); + } + + submitForm() { + setSecondaryLogin(this.state.login, this.state.password) + .then((response) => { + if (response.jsonCode === 200) { + Navigation.navigate(ROUTES.SETTINGS_PROFILE); + } + }); + } + + // Determines whether form is valid + validateForm() { + const validationMethod = this.formType === 'phone' ? Str.isValidPhone : Str.isValidEmail; + return !this.state.password || !validationMethod(this.state.login); + } + + render() { + return ( + + Navigation.navigate(ROUTES.SETTINGS_PROFILE)} + onCloseButtonPress={Navigation.dismissModal} + /> + + + + {this.formType === 'phone' + ? 'Enter your preferred phone number and password to send a validation link.' + : 'Enter your preferred email address and password to send a validation link.'} + + + + {this.formType === 'phone' ? 'Phone Number' : 'Email Address'} + + this.setState({login})} + autoFocus + keyboardType={this.formType === 'phone' ? 'phone-pad' : undefined} + returnKeyType="done" + /> + + + Password + this.setState({password})} + secureTextEntry + autoCompleteType="password" + textContentType="password" + onSubmitEditing={this.submitForm} + /> + + {!_.isEmpty(this.props.user.error) && ( + + {this.props.user.error} + + )} + + + + + + + ); + } +} + +AddLoginPage.propTypes = propTypes; +AddLoginPage.defaultProps = defaultProps; +AddLoginPage.displayName = 'AddLoginPage'; + +export default withOnyx({ + user: { + key: ONYXKEYS.USER, + }, +})(AddLoginPage); diff --git a/src/pages/settings/ProfilePage/LoginField.js b/src/pages/settings/ProfilePage/LoginField.js new file mode 100644 index 000000000000..73b9f2222c82 --- /dev/null +++ b/src/pages/settings/ProfilePage/LoginField.js @@ -0,0 +1,73 @@ +import React from 'react'; +import {View, Pressable} from 'react-native'; +import PropTypes from 'prop-types'; +import Text from '../../../components/Text'; +import styles from '../../../styles/styles'; +import {Plus} from '../../../components/Icon/Expensicons'; +import Icon from '../../../components/Icon'; +import ROUTES from '../../../ROUTES'; +import Navigation from '../../../libs/Navigation/Navigation'; +import {resendValidateCode} from '../../../libs/actions/User'; + +const propTypes = { + // Label to display on login form + label: PropTypes.string.isRequired, + + // Type associated with the login + type: PropTypes.oneOf(['email', 'phone']).isRequired, + + // Login associated with the user + login: PropTypes.shape({ + partnerUserID: PropTypes.string, + validatedDate: PropTypes.string, + }).isRequired, +}; +const LoginField = ({ + label, + login, + type, +}) => ( + + {label} + {!login.partnerUserID ? ( + Navigation.navigate(type === 'phone' + ? ROUTES.SETTINGS_ADD_PHONE + : ROUTES.SETTINGS_ADD_EMAIL)} + > + + + + + + + {`Add ${label}`} + + + + + ) : ( + + + {login.partnerUserID} + + {!login.validatedDate && ( + resendValidateCode(login.partnerUserID)} + > + + Resend + + + )} + + )} + +); + +LoginField.propTypes = propTypes; +LoginField.displayName = 'LoginField'; + +export default LoginField; diff --git a/src/pages/settings/ProfilePage.js b/src/pages/settings/ProfilePage/index.js similarity index 77% rename from src/pages/settings/ProfilePage.js rename to src/pages/settings/ProfilePage/index.js index 507a904f6eb7..5100f7c3e28f 100644 --- a/src/pages/settings/ProfilePage.js +++ b/src/pages/settings/ProfilePage/index.js @@ -9,20 +9,22 @@ import { import RNPickerSelect from 'react-native-picker-select'; import Str from 'expensify-common/lib/str'; import moment from 'moment-timezone'; -import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; -import Navigation from '../../libs/Navigation/Navigation'; -import ScreenWrapper from '../../components/ScreenWrapper'; -import {setPersonalDetails} from '../../libs/actions/PersonalDetails'; -import ROUTES from '../../ROUTES'; -import ONYXKEYS from '../../ONYXKEYS'; -import CONST from '../../CONST'; -import Avatar from '../../components/Avatar'; -import styles from '../../styles/styles'; -import Text from '../../components/Text'; -import {DownArrow} from '../../components/Icon/Expensicons'; -import Icon from '../../components/Icon'; -import Checkbox from '../../components/Checkbox'; -import themeColors from '../../styles/themes/default'; +import _ from 'underscore'; +import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton'; +import Navigation from '../../../libs/Navigation/Navigation'; +import ScreenWrapper from '../../../components/ScreenWrapper'; +import {setPersonalDetails} from '../../../libs/actions/PersonalDetails'; +import ROUTES from '../../../ROUTES'; +import ONYXKEYS from '../../../ONYXKEYS'; +import CONST from '../../../CONST'; +import Avatar from '../../../components/Avatar'; +import styles from '../../../styles/styles'; +import Text from '../../../components/Text'; +import {DownArrow} from '../../../components/Icon/Expensicons'; +import Icon from '../../../components/Icon'; +import Checkbox from '../../../components/Checkbox'; +import themeColors from '../../../styles/themes/default'; +import LoginField from './LoginField'; const propTypes = { /* Onyx Props */ @@ -53,10 +55,27 @@ const propTypes = { automatic: PropTypes.bool, }), }), + + // The details about the user that is signed in + user: PropTypes.shape({ + // Whether or not the user is subscribed to news updates + loginList: PropTypes.arrayOf(PropTypes.shape({ + + // Value of partner name + partnerName: PropTypes.string, + + // Phone/Email associated with user + partnerUserID: PropTypes.string, + + // Date of when login was validated + validatedDate: PropTypes.string, + })), + }), }; const defaultProps = { myPersonalDetails: {}, + user: {}, }; const timezones = moment.tz.names() @@ -68,7 +87,6 @@ const timezones = moment.tz.names() class ProfilePage extends Component { constructor(props) { super(props); - const { firstName, lastName, @@ -93,11 +111,23 @@ class ProfilePage extends Component { selfSelectedPronouns: initialSelfSelectedPronouns, selectedTimezone: timezone.selected || CONST.DEFAULT_TIME_ZONE.selected, isAutomaticTimezone: timezone.automatic ?? CONST.DEFAULT_TIME_ZONE.automatic, + logins: this.getLogins(props.user.loginList), }; this.pronounDropdownValues = pronounsList.map(pronoun => ({value: pronoun, label: pronoun})); this.updatePersonalDetails = this.updatePersonalDetails.bind(this); this.setAutomaticTimezone = this.setAutomaticTimezone.bind(this); + this.getLogins = this.getLogins.bind(this); + } + + componentDidUpdate(prevProps) { + // Recalculate logins if loginList has changed + if (this.props.user.loginList !== prevProps.user.loginList) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + logins: this.getLogins(this.props.user.loginList), + }); + } } setAutomaticTimezone(isAutomaticTimezone) { @@ -107,6 +137,31 @@ class ProfilePage extends Component { })); } + // Get the most validated login of each type + getLogins(loginList) { + return loginList.reduce((logins, currLogin) => { + const type = Str.isSMSLogin(currLogin.partnerUserID) ? 'phone' : 'email'; + + // If there's already a login type that's validated and/or currLogin isn't valid then return early + if (!_.isEmpty(logins[type]) && (logins[type].validatedDate || !currLogin.validatedDate)) { + return logins; + } + return { + ...logins, + [type]: { + ...currLogin, + type, + partnerUserID: Str.isSMSLogin(currLogin.partnerUserID) + ? Str.removeSMSDomain(currLogin.partnerUserID) + : currLogin.partnerUserID, + }, + }; + }, { + phone: {}, + email: {}, + }); + } + updatePersonalDetails() { const { firstName, @@ -205,19 +260,8 @@ class ProfilePage extends Component { /> )} - - - {Str.isSMSLogin(this.props.myPersonalDetails.login) - ? 'Phone Number' : 'Email Address'} - - - + + Timezone Date: Tue, 30 Mar 2021 21:04:18 -0700 Subject: [PATCH 02/13] update error message --- src/libs/actions/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index aaa1072ea2e4..1196d3890bf5 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -104,7 +104,7 @@ function setSecondaryLogin(login, password) { const loginList = _.where(response.loginList, {partnerName: 'expensify.com'}); Onyx.merge(ONYXKEYS.USER, {loginList}); } else { - const error = lodashGet(response, 'message', 'Unable to add phone number. Please try again.'); + const error = lodashGet(response, 'message', 'Unable to validate. Please try again.'); Onyx.merge(ONYXKEYS.USER, {error}); } return response; From 338753630a20ed5e4f9f2504f3e2aaeee9b794fd Mon Sep 17 00:00:00 2001 From: maftalion Date: Wed, 31 Mar 2021 01:57:23 -0700 Subject: [PATCH 03/13] Add note to login fields --- src/pages/settings/ProfilePage/LoginField.js | 101 ++++++++++++------- 1 file changed, 64 insertions(+), 37 deletions(-) diff --git a/src/pages/settings/ProfilePage/LoginField.js b/src/pages/settings/ProfilePage/LoginField.js index 73b9f2222c82..5c2dc35bcf72 100644 --- a/src/pages/settings/ProfilePage/LoginField.js +++ b/src/pages/settings/ProfilePage/LoginField.js @@ -26,46 +26,73 @@ const LoginField = ({ label, login, type, -}) => ( - - {label} - {!login.partnerUserID ? ( - Navigation.navigate(type === 'phone' - ? ROUTES.SETTINGS_ADD_PHONE - : ROUTES.SETTINGS_ADD_EMAIL)} - > - - - - - - - {`Add ${label}`} - +}) => { + let note; + if (type === 'phone') { + // No phone number + if (!login.partnerUserID) { + note = 'Add your phone number to settle up via Venmo.'; + + // Has unvalidated phone number + } else if (!login.validatedDate) { + note = 'The number has not yet been validated. Click the button to resend the validation link via text.'; + + // Has verified phone number + } else { + note = 'Use your phone number to settle up via Venmo.'; + } + + // Has unvalidated email + } else if (login.partnerUserID && !login.validatedDate) { + note = 'The email has not yet been validated. Click the button to resend the validation link via text.'; + } + + return ( + + {label} + {!login.partnerUserID ? ( + Navigation.navigate(type === 'phone' + ? ROUTES.SETTINGS_ADD_PHONE + : ROUTES.SETTINGS_ADD_EMAIL)} + > + + + + + + + {`Add ${label}`} + + + + ) : ( + + + {login.partnerUserID} + + {!login.validatedDate && ( + resendValidateCode(login.partnerUserID)} + > + + Resend + + + )} - - ) : ( - - - {login.partnerUserID} + )} + {note && ( + + {note} - {!login.validatedDate && ( - resendValidateCode(login.partnerUserID)} - > - - Resend - - - )} - - )} - -); + )} + + ); +}; LoginField.propTypes = propTypes; LoginField.displayName = 'LoginField'; From 07b67c1ef9811c09c41b681d0651c224bfe9f612 Mon Sep 17 00:00:00 2001 From: maftalion Date: Wed, 31 Mar 2021 02:10:11 -0700 Subject: [PATCH 04/13] refactor to use LOGIN_TYPE enum --- src/CONST.js | 4 ++++ .../Navigation/AppNavigator/ModalStackNavigators.js | 5 +++-- src/pages/settings/AddLoginPage.js | 11 ++++++----- src/pages/settings/ProfilePage/LoginField.js | 7 ++++--- src/pages/settings/ProfilePage/index.js | 2 +- 5 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index f12ddfafe977..8be955fbd712 100644 --- a/src/CONST.js +++ b/src/CONST.js @@ -98,6 +98,10 @@ const CONST = { // at least 8 characters, 1 capital letter, 1 lowercase number, 1 number PASSWORD_COMPLEXITY_REGEX_STRING: '^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}$', + LOGIN_TYPE: { + PHONE: 'phone', + EMAIL: 'email', + }, }; export default CONST; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 8596fae54e28..cc00c826401b 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -2,6 +2,7 @@ import React from 'react'; import {createStackNavigator} from '@react-navigation/stack'; import styles from '../../../styles/styles'; import ROUTES from '../../../ROUTES'; +import CONST from '../../../CONST'; import NewChatPage from '../../../pages/NewChatPage'; import NewGroupPage from '../../../pages/NewGroupPage'; import SearchPage from '../../../pages/SearchPage'; @@ -154,7 +155,7 @@ const SettingsModalStackNavigator = () => ( ...defaultSubRouteOptions, title: 'Add Phone Number', }} - initialParams={{type: 'phone'}} + initialParams={{type: CONST.LOGIN_TYPE.PHONE}} /> ( ...defaultSubRouteOptions, title: 'Add Email Address', }} - initialParams={{type: 'email'}} + initialParams={{type: CONST.LOGIN_TYPE.EMAIL}} /> Navigation.navigate(ROUTES.SETTINGS_PROFILE)} onCloseButtonPress={Navigation.dismissModal} @@ -95,20 +96,20 @@ class AddLoginPage extends Component { - {this.formType === 'phone' + {this.formType === CONST.LOGIN_TYPE.PHONE ? 'Enter your preferred phone number and password to send a validation link.' : 'Enter your preferred email address and password to send a validation link.'} - {this.formType === 'phone' ? 'Phone Number' : 'Email Address'} + {this.formType === CONST.LOGIN_TYPE.PHONE ? 'Phone Number' : 'Email Address'} this.setState({login})} autoFocus - keyboardType={this.formType === 'phone' ? 'phone-pad' : undefined} + keyboardType={this.formType === CONST.LOGIN_TYPE.PHONE ? 'phone-pad' : undefined} returnKeyType="done" /> diff --git a/src/pages/settings/ProfilePage/LoginField.js b/src/pages/settings/ProfilePage/LoginField.js index 5c2dc35bcf72..fa506282b3e8 100644 --- a/src/pages/settings/ProfilePage/LoginField.js +++ b/src/pages/settings/ProfilePage/LoginField.js @@ -6,6 +6,7 @@ import styles from '../../../styles/styles'; import {Plus} from '../../../components/Icon/Expensicons'; import Icon from '../../../components/Icon'; import ROUTES from '../../../ROUTES'; +import CONST from '../../../CONST'; import Navigation from '../../../libs/Navigation/Navigation'; import {resendValidateCode} from '../../../libs/actions/User'; @@ -14,7 +15,7 @@ const propTypes = { label: PropTypes.string.isRequired, // Type associated with the login - type: PropTypes.oneOf(['email', 'phone']).isRequired, + type: PropTypes.oneOf([CONST.LOGIN_TYPE.EMAIL, CONST.LOGIN_TYPE.PHONE]).isRequired, // Login associated with the user login: PropTypes.shape({ @@ -28,7 +29,7 @@ const LoginField = ({ type, }) => { let note; - if (type === 'phone') { + if (type === CONST.LOGIN_TYPE.PHONE) { // No phone number if (!login.partnerUserID) { note = 'Add your phone number to settle up via Venmo.'; @@ -53,7 +54,7 @@ const LoginField = ({ {!login.partnerUserID ? ( Navigation.navigate(type === 'phone' + onPress={() => Navigation.navigate(type === CONST.LOGIN_TYPE.PHONE ? ROUTES.SETTINGS_ADD_PHONE : ROUTES.SETTINGS_ADD_EMAIL)} > diff --git a/src/pages/settings/ProfilePage/index.js b/src/pages/settings/ProfilePage/index.js index 5100f7c3e28f..920191a79b8a 100644 --- a/src/pages/settings/ProfilePage/index.js +++ b/src/pages/settings/ProfilePage/index.js @@ -140,7 +140,7 @@ class ProfilePage extends Component { // Get the most validated login of each type getLogins(loginList) { return loginList.reduce((logins, currLogin) => { - const type = Str.isSMSLogin(currLogin.partnerUserID) ? 'phone' : 'email'; + const type = Str.isSMSLogin(currLogin.partnerUserID) ? CONST.LOGIN_TYPE.PHONE : CONST.LOGIN_TYPE.EMAIL; // If there's already a login type that's validated and/or currLogin isn't valid then return early if (!_.isEmpty(logins[type]) && (logins[type].validatedDate || !currLogin.validatedDate)) { From 7d19d4af8f073bd043729238c3fccee345a14e06 Mon Sep 17 00:00:00 2001 From: maftalion Date: Wed, 31 Mar 2021 19:05:42 -0700 Subject: [PATCH 05/13] always use primary login --- src/pages/settings/ProfilePage/index.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/ProfilePage/index.js b/src/pages/settings/ProfilePage/index.js index 920191a79b8a..cc9bfa58927c 100644 --- a/src/pages/settings/ProfilePage/index.js +++ b/src/pages/settings/ProfilePage/index.js @@ -139,21 +139,24 @@ class ProfilePage extends Component { // Get the most validated login of each type getLogins(loginList) { - return loginList.reduce((logins, currLogin) => { - const type = Str.isSMSLogin(currLogin.partnerUserID) ? CONST.LOGIN_TYPE.PHONE : CONST.LOGIN_TYPE.EMAIL; + return loginList.reduce((logins, currentLogin) => { + const type = Str.isSMSLogin(currentLogin.partnerUserID) ? CONST.LOGIN_TYPE.PHONE : CONST.LOGIN_TYPE.EMAIL; + const login = Str.isSMSLogin(currentLogin.partnerUserID) + ? Str.removeSMSDomain(currentLogin.partnerUserID) : currentLogin.partnerUserID; - // If there's already a login type that's validated and/or currLogin isn't valid then return early - if (!_.isEmpty(logins[type]) && (logins[type].validatedDate || !currLogin.validatedDate)) { + // If there's already a login type that's validated and/or currentLogin isn't valid then return early + if ((login !== this.props.myPersonalDetails.login) && !_.isEmpty(logins[type]) + && (logins[type].validatedDate || !currentLogin.validatedDate)) { return logins; } return { ...logins, [type]: { - ...currLogin, + ...currentLogin, type, - partnerUserID: Str.isSMSLogin(currLogin.partnerUserID) - ? Str.removeSMSDomain(currLogin.partnerUserID) - : currLogin.partnerUserID, + partnerUserID: Str.isSMSLogin(currentLogin.partnerUserID) + ? Str.removeSMSDomain(currentLogin.partnerUserID) + : currentLogin.partnerUserID, }, }; }, { From 20f40497e48c8bfa1c81505f4a65c93de55982dc Mon Sep 17 00:00:00 2001 From: maftalion Date: Wed, 31 Mar 2021 19:05:57 -0700 Subject: [PATCH 06/13] update error message --- src/libs/actions/User.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 1196d3890bf5..92a67ba9aeee 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -104,7 +104,7 @@ function setSecondaryLogin(login, password) { const loginList = _.where(response.loginList, {partnerName: 'expensify.com'}); Onyx.merge(ONYXKEYS.USER, {loginList}); } else { - const error = lodashGet(response, 'message', 'Unable to validate. Please try again.'); + const error = lodashGet(response, 'message', 'Unable to add secondary login. Please try again.'); Onyx.merge(ONYXKEYS.USER, {error}); } return response; From 79278fe619ceb653b77f4c920107d18ab6522276 Mon Sep 17 00:00:00 2001 From: maftalion Date: Wed, 31 Mar 2021 19:06:25 -0700 Subject: [PATCH 07/13] rename file, style changes --- .../Navigation/AppNavigator/ModalStackNavigators.js | 6 +++--- .../{AddLoginPage.js => AddSecondaryLoginPage.js} | 10 +++++----- src/pages/settings/ProfilePage/LoginField.js | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) rename src/pages/settings/{AddLoginPage.js => AddSecondaryLoginPage.js} (96%) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index cc00c826401b..238f75f7fc76 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -14,7 +14,7 @@ import SettingsProfilePage from '../../../pages/settings/ProfilePage'; import SettingsPreferencesPage from '../../../pages/settings/PreferencesPage'; import SettingsPasswordPage from '../../../pages/settings/PasswordPage'; import SettingsPaymentsPage from '../../../pages/settings/PaymentsPage'; -import SettingsAddLoginPage from '../../../pages/settings/AddLoginPage'; +import SettingsAddSecondaryLoginPage from '../../../pages/settings/AddSecondaryLoginPage'; // Setup the modal stack navigators so we only have to create them once const SettingsModalStack = createStackNavigator(); @@ -150,7 +150,7 @@ const SettingsModalStackNavigator = () => ( /> ( /> {!login.validatedDate && ( resendValidateCode(login.partnerUserID)} > From 3366fb78e01292b3652d7375582b389081b11a04 Mon Sep 17 00:00:00 2001 From: maftalion Date: Thu, 1 Apr 2021 19:25:43 -0700 Subject: [PATCH 08/13] update to dynamic route --- src/ROUTES.js | 4 ++-- .../AppNavigator/ModalStackNavigators.js | 17 +---------------- src/libs/Navigation/linkingConfig.js | 9 ++------- src/libs/actions/User.js | 8 +++++++- 4 files changed, 12 insertions(+), 26 deletions(-) diff --git a/src/ROUTES.js b/src/ROUTES.js index 314bf03db7ca..b447820eb929 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -8,8 +8,8 @@ export default { SETTINGS_PREFERENCES: 'settings/preferences', SETTINGS_PASSWORD: 'settings/password', SETTINGS_PAYMENTS: 'settings/payments', - SETTINGS_ADD_PHONE: 'settings/add-phone', - SETTINGS_ADD_EMAIL: 'settings/add-email', + SETTINGS_ADD_LOGIN: 'settings/addlogin/:type', + getSettingsAddLoginRoute: type => `settings/addlogin/${type}`, NEW_GROUP: 'new/group', NEW_CHAT: 'new/chat', REPORT: 'r', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 238f75f7fc76..d2e1c605ae9f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -2,7 +2,6 @@ import React from 'react'; import {createStackNavigator} from '@react-navigation/stack'; import styles from '../../../styles/styles'; import ROUTES from '../../../ROUTES'; -import CONST from '../../../CONST'; import NewChatPage from '../../../pages/NewChatPage'; import NewGroupPage from '../../../pages/NewGroupPage'; import SearchPage from '../../../pages/SearchPage'; @@ -149,22 +148,8 @@ const SettingsModalStackNavigator = () => ( component={SettingsProfilePage} /> - Date: Thu, 1 Apr 2021 19:26:08 -0700 Subject: [PATCH 09/13] add more feedback on code sent --- src/pages/settings/ProfilePage/LoginField.js | 156 +++++++++++-------- 1 file changed, 91 insertions(+), 65 deletions(-) diff --git a/src/pages/settings/ProfilePage/LoginField.js b/src/pages/settings/ProfilePage/LoginField.js index 834f6b00a154..d7c237f20508 100644 --- a/src/pages/settings/ProfilePage/LoginField.js +++ b/src/pages/settings/ProfilePage/LoginField.js @@ -1,9 +1,10 @@ -import React from 'react'; +import React, {Component} from 'react'; import {View, Pressable} from 'react-native'; import PropTypes from 'prop-types'; import Text from '../../../components/Text'; import styles from '../../../styles/styles'; -import {Plus} from '../../../components/Icon/Expensicons'; +import colors from '../../../styles/colors'; +import {Plus, Checkmark} from '../../../components/Icon/Expensicons'; import Icon from '../../../components/Icon'; import ROUTES from '../../../ROUTES'; import CONST from '../../../CONST'; @@ -24,77 +25,102 @@ const propTypes = { }).isRequired, }; -const LoginField = ({ - label, - login, - type, -}) => { - let note; - if (type === CONST.LOGIN_TYPE.PHONE) { - // No phone number - if (!login.partnerUserID) { - note = 'Add your phone number to settle up via Venmo.'; +export class LoginField extends Component { + constructor(props) { + super(props); + this.state = { + showCheckmarkIcon: false, + }; + this.timeout = null; + this.onResendClicked = this.onResendClicked.bind(this); + } - // Has unvalidated phone number - } else if (!login.validatedDate) { - note = 'The number has not yet been validated. Click the button to resend the validation link via text.'; + onResendClicked() { + resendValidateCode(this.props.login.partnerUserID); + this.setState({showCheckmarkIcon: true}); - // Has verified phone number - } else { - note = 'Use your phone number to settle up via Venmo.'; + // Revert checkmark back to "Resend" after 5seconds + if (!this.timeout) { + this.timeout = setTimeout(() => { + if (this.timeout) { + this.setState({showCheckmarkIcon: false}); + this.timeout = null; + } + }, 5000); } - - // Has unvalidated email - } else if (login.partnerUserID && !login.validatedDate) { - note = 'The email has not yet been validated. Click the button to resend the validation link via text.'; } - return ( - - {label} - {!login.partnerUserID ? ( - Navigation.navigate(type === CONST.LOGIN_TYPE.PHONE - ? ROUTES.SETTINGS_ADD_PHONE - : ROUTES.SETTINGS_ADD_EMAIL)} - > - - - - - - - {`Add ${label}`} - + render() { + let note; + if (this.props.type === CONST.LOGIN_TYPE.PHONE) { + // No phone number + if (!this.props.login.partnerUserID) { + note = 'Add your phone number to settle up via Venmo.'; + + // Has unvalidated phone number + } else if (!this.props.login.validatedDate) { + // eslint-disable-next-line max-len + note = 'The number has not yet been validated. Click the button to resend the validation link via text.'; + + // Has verified phone number + } else { + note = 'Use your phone number to settle up via Venmo.'; + } + + // Has unvalidated email + } else if (this.props.login.partnerUserID && !this.props.login.validatedDate) { + note = 'The email has not yet been validated. Click the button to resend the validation link via text.'; + } + + return ( + + {this.props.label} + {!this.props.login.partnerUserID ? ( + Navigation.navigate(ROUTES.getSettingsAddLoginRoute(this.props.type))} + > + + + + + + + {`Add ${this.props.label}`} + + + + ) : ( + + + {this.props.login.partnerUserID} + + {!this.props.login.validatedDate && ( + + {this.state.showCheckmarkIcon ? ( + + ) : ( + + Resend + + )} + + )} - - ) : ( - - - {login.partnerUserID} + )} + {note && ( + + {note} - {!login.validatedDate && ( - resendValidateCode(login.partnerUserID)} - > - - Resend - - - )} - - )} - {note && ( - - {note} - - )} - - ); -}; + )} + + ); + } +} LoginField.propTypes = propTypes; LoginField.displayName = 'LoginField'; From 86ec5878fb228b692a05f0f3b2d922ff7bd18ab6 Mon Sep 17 00:00:00 2001 From: maftalion Date: Thu, 1 Apr 2021 19:30:22 -0700 Subject: [PATCH 10/13] fix export --- src/pages/settings/ProfilePage/LoginField.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/settings/ProfilePage/LoginField.js b/src/pages/settings/ProfilePage/LoginField.js index d7c237f20508..8e3a94a872f0 100644 --- a/src/pages/settings/ProfilePage/LoginField.js +++ b/src/pages/settings/ProfilePage/LoginField.js @@ -25,7 +25,7 @@ const propTypes = { }).isRequired, }; -export class LoginField extends Component { +export default class LoginField extends Component { constructor(props) { super(props); this.state = { @@ -124,5 +124,3 @@ export class LoginField extends Component { LoginField.propTypes = propTypes; LoginField.displayName = 'LoginField'; - -export default LoginField; From 640e64b55716067466c1d88d59fff13ded86b90e Mon Sep 17 00:00:00 2001 From: maftalion Date: Fri, 2 Apr 2021 15:53:17 -0700 Subject: [PATCH 11/13] small fixes --- src/pages/settings/ProfilePage/LoginField.js | 6 +++--- src/pages/settings/ProfilePage/index.js | 7 ++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/ProfilePage/LoginField.js b/src/pages/settings/ProfilePage/LoginField.js index 8e3a94a872f0..bcaa201e3ddb 100644 --- a/src/pages/settings/ProfilePage/LoginField.js +++ b/src/pages/settings/ProfilePage/LoginField.js @@ -57,17 +57,17 @@ export default class LoginField extends Component { if (!this.props.login.partnerUserID) { note = 'Add your phone number to settle up via Venmo.'; - // Has unvalidated phone number + // Has unvalidated phone number } else if (!this.props.login.validatedDate) { // eslint-disable-next-line max-len note = 'The number has not yet been validated. Click the button to resend the validation link via text.'; - // Has verified phone number + // Has verified phone number } else { note = 'Use your phone number to settle up via Venmo.'; } - // Has unvalidated email + // Has unvalidated email } else if (this.props.login.partnerUserID && !this.props.login.validatedDate) { note = 'The email has not yet been validated. Click the button to resend the validation link via text.'; } diff --git a/src/pages/settings/ProfilePage/index.js b/src/pages/settings/ProfilePage/index.js index 60485491bfed..3eea6852c437 100644 --- a/src/pages/settings/ProfilePage/index.js +++ b/src/pages/settings/ProfilePage/index.js @@ -145,8 +145,7 @@ class ProfilePage extends Component { getLogins(loginList) { return loginList.reduce((logins, currentLogin) => { const type = Str.isSMSLogin(currentLogin.partnerUserID) ? CONST.LOGIN_TYPE.PHONE : CONST.LOGIN_TYPE.EMAIL; - const login = Str.isSMSLogin(currentLogin.partnerUserID) - ? Str.removeSMSDomain(currentLogin.partnerUserID) : currentLogin.partnerUserID; + const login = Str.removeSMSDomain(currentLogin.partnerUserID); // If there's already a login type that's validated and/or currentLogin isn't valid then return early if ((login !== this.props.myPersonalDetails.login) && !_.isEmpty(logins[type]) @@ -158,9 +157,7 @@ class ProfilePage extends Component { [type]: { ...currentLogin, type, - partnerUserID: Str.isSMSLogin(currentLogin.partnerUserID) - ? Str.removeSMSDomain(currentLogin.partnerUserID) - : currentLogin.partnerUserID, + partnerUserID: Str.removeSMSDomain(currentLogin.partnerUserID), }, }; }, { From e54a3151d4ee9d14064f2450b03e0947d96fbfd7 Mon Sep 17 00:00:00 2001 From: maftalion Date: Mon, 5 Apr 2021 15:46:49 -0700 Subject: [PATCH 12/13] use consts for keyboard types --- src/CONST.js | 4 ++++ src/pages/settings/AddSecondaryLoginPage.js | 3 ++- src/pages/signin/PasswordForm.js | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/CONST.js b/src/CONST.js index bd141906dc1d..b481235818fe 100644 --- a/src/CONST.js +++ b/src/CONST.js @@ -98,6 +98,10 @@ const CONST = { PHONE: 'phone', EMAIL: 'email', }, + KEYBOARD_TYPE: { + NUMERIC: 'numeric', + PHONE_PAD: 'phone-pad', + }, }; export default CONST; diff --git a/src/pages/settings/AddSecondaryLoginPage.js b/src/pages/settings/AddSecondaryLoginPage.js index f6e5f7e16614..24b339414da1 100644 --- a/src/pages/settings/AddSecondaryLoginPage.js +++ b/src/pages/settings/AddSecondaryLoginPage.js @@ -109,7 +109,8 @@ class AddSecondaryLoginPage extends Component { value={this.state.login} onChangeText={login => this.setState({login})} autoFocus - keyboardType={this.formType === CONST.LOGIN_TYPE.PHONE ? 'phone-pad' : undefined} + keyboardType={this.formType === CONST.LOGIN_TYPE.PHONE + ? CONST.KEYBOARD_TYPE.PHONE_PAD : undefined} returnKeyType="done" /> diff --git a/src/pages/signin/PasswordForm.js b/src/pages/signin/PasswordForm.js index 8c9b95b9f284..61439df1783e 100644 --- a/src/pages/signin/PasswordForm.js +++ b/src/pages/signin/PasswordForm.js @@ -9,6 +9,7 @@ import ButtonWithLoader from '../../components/ButtonWithLoader'; import themeColors from '../../styles/themes/default'; import {signIn} from '../../libs/actions/Session'; import ONYXKEYS from '../../ONYXKEYS'; +import CONST from '../../CONST'; import ChangeExpensifyLoginLink from './ChangeExpensifyLoginLink'; const propTypes = { @@ -88,7 +89,7 @@ class PasswordForm extends React.Component { placeholderTextColor={themeColors.placeholderText} onChangeText={text => this.setState({twoFactorAuthCode: text})} onSubmitEditing={this.validateAndSubmitForm} - keyboardType="numeric" + keyboardType={CONST.KEYBOARD_TYPE.NUMERIC} /> )} From 4ce089298a24ffbca391fb4e6700638dc45aaafc Mon Sep 17 00:00:00 2001 From: maftalion Date: Mon, 5 Apr 2021 15:46:59 -0700 Subject: [PATCH 13/13] small fixes --- src/libs/actions/User.js | 1 - src/pages/settings/ProfilePage/LoginField.js | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 7a5b5c019edb..1fce0fb041e2 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -91,7 +91,6 @@ function setExpensifyNewsStatus(subscribed) { * @param {String} login * @param {String} password * @returns {Promise} - * */ function setSecondaryLogin(login, password) { Onyx.merge(ONYXKEYS.ACCOUNT, {error: '', loading: true}); diff --git a/src/pages/settings/ProfilePage/LoginField.js b/src/pages/settings/ProfilePage/LoginField.js index bcaa201e3ddb..95d63c632835 100644 --- a/src/pages/settings/ProfilePage/LoginField.js +++ b/src/pages/settings/ProfilePage/LoginField.js @@ -39,7 +39,7 @@ export default class LoginField extends Component { resendValidateCode(this.props.login.partnerUserID); this.setState({showCheckmarkIcon: true}); - // Revert checkmark back to "Resend" after 5seconds + // Revert checkmark back to "Resend" after 5 seconds if (!this.timeout) { this.timeout = setTimeout(() => { if (this.timeout) {