From 8d4b71e8a6f79b39b8d1ec70a9e84724cf307af6 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 16 Feb 2023 13:03:55 +0200 Subject: [PATCH 01/55] New green brick indicator status --- src/CONST.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/CONST.js b/src/CONST.js index 10f2a7e63dec..3ad06ad8d9e3 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -881,6 +881,7 @@ const CONST = { BRICK_ROAD_INDICATOR_STATUS: { ERROR: 'error', INFO: 'info', + GREEN: 'green', }, REPORT_DETAILS_MENU_ITEM: { MEMBERS: 'member', From 8e2741ed0391267974193a192c17b073dde67c98 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 16 Feb 2023 13:04:27 +0200 Subject: [PATCH 02/55] New contact method details page --- src/ROUTES.js | 2 ++ src/libs/Navigation/AppNavigator/ModalStackNavigators.js | 7 +++++++ src/libs/Navigation/linkingConfig.js | 3 +++ .../Profile/Contacts/ContactMethodDetailsPage.js | 9 +++++++++ 4 files changed, 21 insertions(+) create mode 100644 src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js diff --git a/src/ROUTES.js b/src/ROUTES.js index 9276d665ebe0..b0c0e2dc84d0 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -51,6 +51,8 @@ export default { SETTINGS_PERSONAL_DETAILS_DATE_OF_BIRTH: `${SETTINGS_PERSONAL_DETAILS}/date-of-birth`, SETTINGS_PERSONAL_DETAILS_ADDRESS: `${SETTINGS_PERSONAL_DETAILS}/address`, SETTINGS_CONTACT_METHODS, + SETTINGS_CONTACT_METHOD_DETAILS: `${SETTINGS_CONTACT_METHODS}/:contactMethod/details`, + getEditContactMethodRoute: contactMethod => `${SETTINGS_CONTACT_METHODS}/${encodeURIComponent(contactMethod)}/details`, NEW_GROUP: 'new/group', NEW_CHAT: 'new/chat', REPORT, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 07c67bec110e..a2b481df2d82 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -280,6 +280,13 @@ const SettingsModalStackNavigator = createModalStackNavigator([ }, name: 'Settings_ContactMethods', }, + { + getComponent: () => { + const SettingsContactMethodDetailsPage = require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default; + return SettingsContactMethodDetailsPage; + }, + name: 'Settings_ContactMethodDetails', + }, { getComponent: () => { const SettingsAddSecondaryLoginPage = require('../../../pages/settings/Profile/Contacts/AddSecondaryLoginPage').default; diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 361655c31439..80e043c1b40d 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -124,6 +124,9 @@ export default { path: ROUTES.SETTINGS_CONTACT_METHODS, exact: true, }, + Settings_ContactMethodDetails: { + path: ROUTES.SETTINGS_CONTACT_METHOD_DETAILS, + }, Settings_Add_Secondary_Login: { path: ROUTES.SETTINGS_ADD_LOGIN, }, diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js new file mode 100644 index 000000000000..7e48e6dc66de --- /dev/null +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -0,0 +1,9 @@ +import React from 'react'; + +const ContactMethodDetailsPage = props => { + return ( +
ContactMethodDetailsPage
+ ) +}; + +export default ContactMethodDetailsPage; From af51025acb9f12356f4a7f1ca36fe3f8e341ae38 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 16 Feb 2023 13:04:51 +0200 Subject: [PATCH 03/55] Dynamically show all of a user's logins --- .../Profile/Contacts/ContactMethodsPage.js | 158 +++++++++--------- 1 file changed, 80 insertions(+), 78 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js index edb998047c11..60cc05ed5939 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js @@ -1,13 +1,12 @@ import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {Component} from 'react'; +import React from 'react'; +import {View} from 'react-native'; import {ScrollView} from 'react-native-gesture-handler'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import HeaderWithCloseButton from '../../../../components/HeaderWithCloseButton'; import ScreenWrapper from '../../../../components/ScreenWrapper'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '../../../../components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize'; import CONST from '../../../../CONST'; import compose from '../../../../libs/compose'; @@ -15,6 +14,10 @@ import Navigation from '../../../../libs/Navigation/Navigation'; import ONYXKEYS from '../../../../ONYXKEYS'; import ROUTES from '../../../../ROUTES'; import LoginField from './LoginField'; +import MenuItem from '../../../../components/MenuItem'; +import Text from '../../../../components/Text'; +import styles from '../../../../styles/styles'; +import CopyTextToClipboard from '../../../../components/CopyTextToClipboard'; const propTypes = { /* Onyx Props */ @@ -31,108 +34,107 @@ const propTypes = { validatedDate: PropTypes.string, }), + /** Current user session */ + session: PropTypes.shape({ + email: PropTypes.string.isRequired, + }).isRequired, + ...withLocalizePropTypes, - ...withCurrentUserPersonalDetailsPropTypes, }; const defaultProps = { loginList: {}, - ...withCurrentUserPersonalDetailsDefaultProps, }; -class ContactMethodsPage extends Component { - constructor(props) { - super(props); - - this.state = { - logins: this.getLogins(), - }; - - this.getLogins = this.getLogins.bind(this); - } +const ContactMethodsPage = props => { + let hasPhoneNumberLogin = false; + let hasEmailLogin = false; - componentDidUpdate(prevProps) { - let stateToUpdate = {}; - - // Recalculate logins if loginList has changed - if (_.keys(this.props.loginList).length !== _.keys(prevProps.loginList).length) { - stateToUpdate = {logins: this.getLogins()}; + const loginMenuItems = _.map(props.loginList, (login) => { + let description = ''; + if (props.session.email === login.partnerUserID) { + description = props.translate('contacts.getInTouch'); + } else if (!login.validatedDate) { + description = props.translate('contacts.pleaseVerify'); } - - if (_.isEmpty(stateToUpdate)) { - return; + let indicator = null; + if (!_.isEmpty(login.errorFields)) { + indicator = CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + } else if (!login.validatedDate) { + indicator = CONST.BRICK_ROAD_INDICATOR_STATUS.GREEN; } - // eslint-disable-next-line react/no-did-update-set-state - this.setState(stateToUpdate); - } - - /** - * Get the most validated login of each type - * - * @returns {Object} - */ - getLogins() { - return _.reduce(_.values(this.props.loginList), (logins, currentLogin) => { - const type = Str.isSMSLogin(currentLogin.partnerUserID) ? CONST.LOGIN_TYPE.PHONE : CONST.LOGIN_TYPE.EMAIL; - 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 !== lodashGet(this.props.currentUserPersonalDetails, 'login')) && !_.isEmpty(logins[type]) - && (logins[type].validatedDate || !currentLogin.validatedDate)) { - return logins; - } - return { - ...logins, - [type]: { - ...currentLogin, - type, - partnerUserID: Str.removeSMSDomain(currentLogin.partnerUserID), - }, - }; - }, { - phone: {}, - email: {}, - }); - } - - render() { + // Temporary checks to determine if we need to show specific LoginField + // components. This check will be removed soon. + if (Str.isValidPhone(login.partnerUserID)) { + hasPhoneNumberLogin = true; + } else if (Str.isValidEmail(login.partnerUserID)) { + hasEmailLogin = true; + } return ( - - Navigation.navigate(ROUTES.SETTINGS_PROFILE)} - onCloseButtonPress={() => Navigation.dismissModal(true)} - /> - + Navigation.navigate(ROUTES.getEditContactMethodRoute(login.partnerUserID))} + brickRoadIndicator={indicator} + shouldShowBasicTitle + shouldShowRightIcon + /> + ) + }); + + return ( + + Navigation.navigate(ROUTES.SETTINGS_PROFILE)} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + + + + {props.translate('contacts.helpTextBeforeEmail')} + + {props.translate('contacts.helpTextAfterEmail')} + + + {loginMenuItems} + {/* The below fields will be removed soon, when we implement the new Add Contact Method page */} + {!hasEmailLogin && ( + )} + {!hasPhoneNumberLogin && ( - - - ); - } -} + )} + + + ); +}; ContactMethodsPage.propTypes = propTypes; ContactMethodsPage.defaultProps = defaultProps; +ContactMethodsPage.displayName = 'ContactMethodsPage'; export default compose( withLocalize, - withCurrentUserPersonalDetails, withOnyx({ loginList: { key: ONYXKEYS.LOGIN_LIST, }, + session: { + key: ONYXKEYS.SESSION, + }, }), )(ContactMethodsPage); From 7f82353f7bf9a1e50f87e7e130e2ac8c07c15d89 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 16 Feb 2023 13:05:11 +0200 Subject: [PATCH 04/55] Initial copy for contact methods page --- src/languages/en.js | 4 ++++ src/languages/es.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/languages/en.js b/src/languages/en.js index 1e254c64f53e..d8f4a562d897 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -342,6 +342,10 @@ export default { contacts: { contactMethod: 'Contact method', contactMethods: 'Contact methods', + helpTextBeforeEmail: 'Add more ways for people to find you, and forward receipts to ', + helpTextAfterEmail: ' from multiple email addresses.', + pleaseVerify: 'Please verify this contact method', + getInTouch: "Whenever we need to get in touch with you, we'll use this contact method.", }, pronouns: { coCos: 'Co / Cos', diff --git a/src/languages/es.js b/src/languages/es.js index 0ecabf17c462..23cb466f1dd6 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -342,6 +342,10 @@ export default { contacts: { contactMethod: 'Método de contacto', contactMethods: 'Métodos de contacto', + helpTextBeforeEmail: '', + helpTextAfterEmail: '', + pleaseVerify: '', + getInTouch: '', }, pronouns: { coCos: 'Co / Cos', From 7889bae7046def767630b527d312269d95fc94d7 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 16 Feb 2023 13:11:25 +0200 Subject: [PATCH 05/55] Add new possible value for brickRoadIndicator --- src/components/menuItemPropTypes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js index 949d1ca26f1a..2159268a2534 100644 --- a/src/components/menuItemPropTypes.js +++ b/src/components/menuItemPropTypes.js @@ -80,7 +80,7 @@ const propTypes = { floatRightAvatars: PropTypes.arrayOf(PropTypes.string), /** The type of brick road indicator to show. */ - brickRoadIndicator: PropTypes.oneOf([CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR, CONST.BRICK_ROAD_INDICATOR_STATUS.INFO, '']), + brickRoadIndicator: PropTypes.oneOf([CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR, CONST.BRICK_ROAD_INDICATOR_STATUS.INFO, CONST.BRICK_ROAD_INDICATOR_STATUS.GREEN, '']), /** Prop to identify if we should load avatars vertically instead of diagonally */ shouldStackHorizontally: PropTypes.bool, From 4e8474b9afae9f5a45f04bd57a03672bcdd94f35 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 16 Feb 2023 14:24:23 +0200 Subject: [PATCH 06/55] Add key to array of items being rendered --- src/pages/settings/Profile/Contacts/ContactMethodsPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js index 60cc05ed5939..694bf6a98615 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js @@ -79,6 +79,7 @@ const ContactMethodsPage = props => { brickRoadIndicator={indicator} shouldShowBasicTitle shouldShowRightIcon + key={login.partnerUserID} /> ) }); From f097519e2259df8b3d2e6f3cfd99ed6f7408e31e Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Thu, 16 Feb 2023 14:24:38 +0200 Subject: [PATCH 07/55] Populate contact method details page --- src/languages/en.js | 2 + src/languages/es.js | 2 + .../Contacts/ContactMethodDetailsPage.js | 95 ++++++++++++++++++- 3 files changed, 97 insertions(+), 2 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index d8f4a562d897..6b8cd8e1e690 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -346,6 +346,8 @@ export default { helpTextAfterEmail: ' from multiple email addresses.', pleaseVerify: 'Please verify this contact method', getInTouch: "Whenever we need to get in touch with you, we'll use this contact method.", + clickVerificationLink: 'Click the verification link we sent you to verify this contact method, or tap above to resend.', + tempContactVerifiedText: 'This contact method is verified so there is nothing to do here at the moment. Soon you will be able to delete this contact method if desired.', }, pronouns: { coCos: 'Co / Cos', diff --git a/src/languages/es.js b/src/languages/es.js index 23cb466f1dd6..2b93552df9a0 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -346,6 +346,8 @@ export default { helpTextAfterEmail: '', pleaseVerify: '', getInTouch: '', + clickVerificationLink: '', + tempContactVerifiedText: '', }, pronouns: { coCos: 'Co / Cos', diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index 7e48e6dc66de..eac441f57170 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -1,9 +1,100 @@ +import Str from 'expensify-common/lib/str'; import React from 'react'; +import {View, ScrollView} from 'react-native'; +import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; +import Navigation from '../../../../libs/Navigation/Navigation'; +import ROUTES from '../../../../ROUTES'; +import ScreenWrapper from '../../../../components/ScreenWrapper'; +import HeaderWithCloseButton from '../../../../components/HeaderWithCloseButton'; +import compose from '../../../../libs/compose'; +import {withOnyx} from 'react-native-onyx'; +import ONYXKEYS from '../../../../ONYXKEYS'; +import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize'; +import MenuItem from '../../../../components/MenuItem'; +import DotIndicatorMessage from '../../../../components/DotIndicatorMessage'; +import styles from '../../../../styles/styles'; +import * as Expensicons from '../../../../components/Icon/Expensicons'; +import Text from '../../../../components/Text'; + +const propTypes = { + /* Onyx Props */ + + /** Login list for the user that is signed in */ + loginList: 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 params */ + route: PropTypes.shape({ + params: PropTypes.shape({ + /** passed via route /settings/profile/contact-methods/:contactMethod/details */ + contactMethod: PropTypes.string, + }), + }), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + loginList: {}, +}; const ContactMethodDetailsPage = props => { + const contactMethod = decodeURIComponent(lodashGet(props.route, 'params.contactMethod')); + const login = props.loginList[contactMethod]; + if (!contactMethod || !login) { + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); + } + + console.log({contactMethod, login}) + return ( -
ContactMethodDetailsPage
+ + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS)} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + + {login.validatedDate ? ( + + {props.translate('contacts.tempContactVerifiedText')} + + ) : ( + <> + + + + )} + + ) }; -export default ContactMethodDetailsPage; +ContactMethodDetailsPage.propTypes = propTypes; +ContactMethodDetailsPage.defaultProps = defaultProps; +ContactMethodDetailsPage.displayName = 'ContactMethodDetailsPage'; + +export default compose( + withLocalize, + withOnyx({ + loginList: { + key: ONYXKEYS.LOGIN_LIST, + }, + }), +)(ContactMethodDetailsPage); From 5a1de67b45b4930b0dc7ef912925037082c08c4c Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 21 Feb 2023 12:30:06 +0200 Subject: [PATCH 08/55] New API call for deleting contact methods --- src/libs/actions/User.js | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index e9f4bcf6cb24..fe260745ef02 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -19,6 +19,7 @@ import * as SequentialQueue from '../Network/SequentialQueue'; import PusherUtils from '../PusherUtils'; import * as Report from './Report'; import * as ReportActionsUtils from '../ReportActionsUtils'; +import DateUtils from '../DateUtils'; let currentUserAccountID = ''; Onyx.connect({ @@ -161,6 +162,59 @@ function setSecondaryLoginAndNavigate(login, password) { }); } +/** + * Delete a specific contact method + * + * @param {String} contactMethod - the contact method being deleted + * @param {Object} oldLoginData + */ +function deleteContactMethod(contactMethod, oldLoginData) { + const optimisticData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: { + partnerUserID: null, + errorFields: { + deletedLogin: null, + }, + pendingFields: { + deletedLogin: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }, + }, + }]; + const successData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: null, + }, + }]; + const failureData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: { + ...oldLoginData, + errorFields: { + deletedLogin: { + [DateUtils.getMicroseconds()]: 'Generic failure message', + }, + }, + pendingFields: { + deletedLogin: null, + }, + }, + }, + }]; + + API.write('DeleteContactMethod', { + partnerUserID: contactMethod, + }, {optimisticData, successData, failureData}); + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); +} + /** * Validates a login given an accountID and validation code * @@ -475,6 +529,7 @@ export { resendValidateCode, updateNewsletterSubscription, setSecondaryLoginAndNavigate, + deleteContactMethod, validateLogin, isBlockedFromConcierge, subscribeToUserEvents, From c2bd0ac924c81d8ac51473ce82cb745935773c3a Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 21 Feb 2023 12:30:17 +0200 Subject: [PATCH 09/55] New function for clearing contact method errors --- src/libs/actions/User.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index fe260745ef02..ab047eef5fa4 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -215,6 +215,19 @@ function deleteContactMethod(contactMethod, oldLoginData) { Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); } +function clearContactMethodErrors(contactMethod, fieldName) { + Onyx.merge(ONYXKEYS.LOGIN_LIST, { + [contactMethod]: { + errorFields: { + [fieldName]: null, + }, + pendingFields: { + [fieldName]: null, + }, + }, + }); +} + /** * Validates a login given an accountID and validation code * @@ -530,6 +543,7 @@ export { updateNewsletterSubscription, setSecondaryLoginAndNavigate, deleteContactMethod, + clearContactMethodErrors, validateLogin, isBlockedFromConcierge, subscribeToUserEvents, From 8845a752908a01f44e70bf8170dbab7e16f010f4 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 21 Feb 2023 12:30:53 +0200 Subject: [PATCH 10/55] Show & connect remove button --- .../Contacts/ContactMethodDetailsPage.js | 52 +++++++++++++++---- 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index eac441f57170..5011b6ebb35b 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -1,8 +1,8 @@ import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; import React from 'react'; import {View, ScrollView} from 'react-native'; import PropTypes from 'prop-types'; -import lodashGet from 'lodash/get'; import Navigation from '../../../../libs/Navigation/Navigation'; import ROUTES from '../../../../ROUTES'; import ScreenWrapper from '../../../../components/ScreenWrapper'; @@ -16,6 +16,9 @@ import DotIndicatorMessage from '../../../../components/DotIndicatorMessage'; import styles from '../../../../styles/styles'; import * as Expensicons from '../../../../components/Icon/Expensicons'; import Text from '../../../../components/Text'; +import OfflineWithFeedback from '../../../../components/OfflineWithFeedback'; +import ConfirmModal from '../../../../components/ConfirmModal'; +import * as User from '../../../../libs/actions/User'; const propTypes = { /* Onyx Props */ @@ -32,6 +35,11 @@ const propTypes = { validatedDate: PropTypes.string, }), + /** Current user session */ + session: PropTypes.shape({ + email: PropTypes.string.isRequired, + }).isRequired, + /** Route params */ route: PropTypes.shape({ params: PropTypes.shape({ @@ -49,12 +57,12 @@ const defaultProps = { const ContactMethodDetailsPage = props => { const contactMethod = decodeURIComponent(lodashGet(props.route, 'params.contactMethod')); - const login = props.loginList[contactMethod]; - if (!contactMethod || !login) { + const loginData = props.loginList[contactMethod]; + if (!contactMethod || !loginData) { Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); } - console.log({contactMethod, login}) + const isDefaultContactMethod = (props.session.email === loginData.partnerUserID); return ( @@ -65,22 +73,45 @@ const ContactMethodDetailsPage = props => { onCloseButtonPress={() => Navigation.dismissModal(true)} /> - {login.validatedDate ? ( - - {props.translate('contacts.tempContactVerifiedText')} + User.deleteContactMethod(contactMethod)} + onCancel={() => console.log('hi')} + prompt="Are you sure you want to remove this contact method? This action cannot be undone." + confirmText="Yes, continue" + isVisible={false} + /> + {isDefaultContactMethod && ( + + This is your current default contact method. You will not be able to delete this contact method until you set an alternative default by selecting another contact method and pressing “Set as default”. - ) : ( + )} + {!loginData.validatedDate && ( <> console.log('hi')} shouldShowRightIcon success /> - + )} + User.clearContactMethodErrors(contactMethod, 'deletedLogin')} + > + User.deleteContactMethod(contactMethod, loginData)} + disabled={isDefaultContactMethod} + /> + ) @@ -96,5 +127,8 @@ export default compose( loginList: { key: ONYXKEYS.LOGIN_LIST, }, + session: { + key: ONYXKEYS.SESSION, + }, }), )(ContactMethodDetailsPage); From aeb85ff8f6378c465cb0f545debe2c141f420b0f Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 21 Feb 2023 13:39:25 +0200 Subject: [PATCH 11/55] Convert page to class for confirmation modal --- src/libs/actions/User.js | 1 - .../Contacts/ContactMethodDetailsPage.js | 141 +++++++++++------- 2 files changed, 85 insertions(+), 57 deletions(-) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index ab047eef5fa4..c3be431f4c25 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -212,7 +212,6 @@ function deleteContactMethod(contactMethod, oldLoginData) { API.write('DeleteContactMethod', { partnerUserID: contactMethod, }, {optimisticData, successData, failureData}); - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); } function clearContactMethodErrors(contactMethod, fieldName) { diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index 5011b6ebb35b..ef3705d1eb7f 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -1,6 +1,6 @@ import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; -import React from 'react'; +import React, {Component} from 'react'; import {View, ScrollView} from 'react-native'; import PropTypes from 'prop-types'; import Navigation from '../../../../libs/Navigation/Navigation'; @@ -55,71 +55,100 @@ const defaultProps = { loginList: {}, }; -const ContactMethodDetailsPage = props => { - const contactMethod = decodeURIComponent(lodashGet(props.route, 'params.contactMethod')); - const loginData = props.loginList[contactMethod]; - if (!contactMethod || !loginData) { +class ContactMethodDetailsPage extends Component { + constructor(props) { + super(props); + + this.toggleDeleteModal = this.toggleDeleteModal.bind(this); + + this.state = { + isDeleteModalOpen: false, + }; + } + + /** + * Toggle delete confirm modal visibility + * @param {Boolean} shouldOpen + */ + toggleDeleteModal(shouldOpen) { + this.setState({isDeleteModalOpen: shouldOpen}); + } + + /** + * Delete the contact method and hide the modal + */ + confirmDeleteAndHideModal() { + User.deleteContactMethod(contactMethod) + this.toggleDeleteModal(false); Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); } - const isDefaultContactMethod = (props.session.email === loginData.partnerUserID); - - return ( - - Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS)} - onCloseButtonPress={() => Navigation.dismissModal(true)} - /> - - User.deleteContactMethod(contactMethod)} - onCancel={() => console.log('hi')} - prompt="Are you sure you want to remove this contact method? This action cannot be undone." - confirmText="Yes, continue" - isVisible={false} + render() { + const contactMethod = decodeURIComponent(lodashGet(this.props.route, 'params.contactMethod')); + const loginData = this.props.loginList[contactMethod]; + if (!contactMethod || !loginData) { + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); + } + + const isDefaultContactMethod = (this.props.session.email === loginData.partnerUserID); + + return ( + + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS)} + onCloseButtonPress={() => Navigation.dismissModal(true)} /> - {isDefaultContactMethod && ( - - This is your current default contact method. You will not be able to delete this contact method until you set an alternative default by selecting another contact method and pressing “Set as default”. - - )} - {!loginData.validatedDate && ( - <> + + this.toggleDeleteModal(false)} + prompt="Are you sure you want to remove this contact method? This action cannot be undone." + confirmText="Yes, continue" + isVisible={this.state.isDeleteModalOpen} + danger + /> + {isDefaultContactMethod && ( + + This is your current default contact method. You will not be able to delete this contact method until you set an alternative default by selecting another contact method and pressing “Set as default”. + + )} + {!loginData.validatedDate && ( + <> + console.log('hi')} + shouldShowRightIcon + success + /> + + + )} + User.clearContactMethodErrors(contactMethod, 'deletedLogin')} + > console.log('hi')} - shouldShowRightIcon - success + title="Remove" + icon={Expensicons.Trashcan} + onPress={() => User.deleteContactMethod(contactMethod, loginData)} + disabled={isDefaultContactMethod} /> - - - )} - User.clearContactMethodErrors(contactMethod, 'deletedLogin')} - > - User.deleteContactMethod(contactMethod, loginData)} - disabled={isDefaultContactMethod} - /> - - - - ) + + + + ); + } }; ContactMethodDetailsPage.propTypes = propTypes; ContactMethodDetailsPage.defaultProps = defaultProps; -ContactMethodDetailsPage.displayName = 'ContactMethodDetailsPage'; export default compose( withLocalize, From 84cb98b3b6a4140a1a7d46e09e52980611543482 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Tue, 21 Feb 2023 16:02:43 +0200 Subject: [PATCH 12/55] Fix hasPhoneNumberLogin check --- src/pages/settings/Profile/Contacts/ContactMethodsPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js index 694bf6a98615..2a7e0d349c4e 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js @@ -66,7 +66,7 @@ const ContactMethodsPage = props => { // Temporary checks to determine if we need to show specific LoginField // components. This check will be removed soon. - if (Str.isValidPhone(login.partnerUserID)) { + if (Str.isValidPhone(Str.removeSMSDomain(login.partnerUserID))) { hasPhoneNumberLogin = true; } else if (Str.isValidEmail(login.partnerUserID)) { hasEmailLogin = true; From a9698fd9f02ac3a117604ec3c3df9db0c2849917 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 22 Feb 2023 15:45:30 +0200 Subject: [PATCH 13/55] New UI for magic code input form --- .../Contacts/ContactMethodDetailsPage.js | 82 +++++++++++++++---- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index ef3705d1eb7f..f5de6444688d 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -1,7 +1,7 @@ import Str from 'expensify-common/lib/str'; import lodashGet from 'lodash/get'; import React, {Component} from 'react'; -import {View, ScrollView} from 'react-native'; +import {View, ScrollView, TouchableOpacity} from 'react-native'; import PropTypes from 'prop-types'; import Navigation from '../../../../libs/Navigation/Navigation'; import ROUTES from '../../../../ROUTES'; @@ -19,6 +19,10 @@ import Text from '../../../../components/Text'; import OfflineWithFeedback from '../../../../components/OfflineWithFeedback'; import ConfirmModal from '../../../../components/ConfirmModal'; import * as User from '../../../../libs/actions/User'; +import TextInput from '../../../../components/TextInput'; +import CONST from '../../../../CONST'; +import Icon from '../../../../components/Icon'; +import colors from '../../../../styles/colors'; const propTypes = { /* Onyx Props */ @@ -60,9 +64,13 @@ class ContactMethodDetailsPage extends Component { super(props); this.toggleDeleteModal = this.toggleDeleteModal.bind(this); + this.confirmDeleteAndHideModal = this.confirmDeleteAndHideModal.bind(this); + this.resendValidateCode = this.resendValidateCode.bind(this); + this.getContactMethod = this.getContactMethod.bind(this); this.state = { isDeleteModalOpen: false, + validateCode: '', }; } @@ -74,20 +82,33 @@ class ContactMethodDetailsPage extends Component { this.setState({isDeleteModalOpen: shouldOpen}); } + getContactMethod() { + return decodeURIComponent(lodashGet(this.props.route, 'params.contactMethod')); + } + /** * Delete the contact method and hide the modal */ confirmDeleteAndHideModal() { + const contactMethod = this.getContactMethod(); User.deleteContactMethod(contactMethod) this.toggleDeleteModal(false); Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); } + /** + * Request a validate code / magic code be sent to verify this contact method + */ + resendValidateCode() { + User.requestContactMethodValidateCode(this.getContactMethod()); + } + render() { - const contactMethod = decodeURIComponent(lodashGet(this.props.route, 'params.contactMethod')); + const contactMethod = this.getContactMethod(); const loginData = this.props.loginList[contactMethod]; if (!contactMethod || !loginData) { Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); + return null; } const isDefaultContactMethod = (this.props.session.email === loginData.partnerUserID); @@ -102,30 +123,56 @@ class ContactMethodDetailsPage extends Component { /> this.toggleDeleteModal(false)} - prompt="Are you sure you want to remove this contact method? This action cannot be undone." - confirmText="Yes, continue" + prompt={this.props.translate('contacts.removeAreYouSure')} + confirmText={this.props.translate('common.yesContinue')} isVisible={this.state.isDeleteModalOpen} danger /> - {isDefaultContactMethod && ( - - This is your current default contact method. You will not be able to delete this contact method until you set an alternative default by selecting another contact method and pressing “Set as default”. - - )} {!loginData.validatedDate && ( <> - console.log('hi')} shouldShowRightIcon success - /> - + /> */} + {/* */} + + + + + + + + {this.props.translate('contacts.enterMagicCode', {contactMethod})} + + + + this.setState({validateCode: text})} + // onSubmitEditing={this.validateAndSubmitForm} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + errorText="" + blurOnSubmit={false} + /> + + + {this.props.translate('contacts.resendMagicCode')} + + + )} User.clearContactMethodErrors(contactMethod, 'deletedLogin')} > User.deleteContactMethod(contactMethod, loginData)} + onPress={() => this.toggleDeleteModal(true)} disabled={isDefaultContactMethod} /> + {isDefaultContactMethod && ( + + {this.props.translate('contacts.yourDefaultContactMethod')} + + )} ); From dc63b434148a157f75ae40671ebd7c900f15ad84 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 22 Feb 2023 15:45:47 +0200 Subject: [PATCH 14/55] Fix for if we just removed a contact method --- src/pages/settings/Profile/Contacts/ContactMethodsPage.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js index 2a7e0d349c4e..71777fd6ba65 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodsPage.js @@ -51,6 +51,8 @@ const ContactMethodsPage = props => { let hasEmailLogin = false; const loginMenuItems = _.map(props.loginList, (login) => { + if (!login.partnerUserID) return null; + let description = ''; if (props.session.email === login.partnerUserID) { description = props.translate('contacts.getInTouch'); From a893888ee1dd9f34950b29dd714e7d8e3b91883b Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 22 Feb 2023 15:46:10 +0200 Subject: [PATCH 15/55] A few new contact translations --- src/languages/en.js | 10 +++++++++- src/languages/es.js | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/languages/en.js b/src/languages/en.js index a59454a758af..0bfb2747bfec 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -118,6 +118,7 @@ export default { youAppearToBeOffline: 'You appear to be offline.', thisFeatureRequiresInternet: 'This feature requires an active internet connection to be used.', areYouSure: 'Are you sure?', + yesContinue: 'Yes, continue', zipCodeExample: 'e.g. 12345, 12345-1234, 12345 1234', websiteExample: 'e.g. https://www.expensify.com', }, @@ -345,8 +346,15 @@ export default { helpTextAfterEmail: ' from multiple email addresses.', pleaseVerify: 'Please verify this contact method', getInTouch: "Whenever we need to get in touch with you, we'll use this contact method.", - clickVerificationLink: 'Click the verification link we sent you to verify this contact method, or tap above to resend.', + enterMagicCode: ({contactMethod}) => `Please enter the magic code sent to ${contactMethod}`, tempContactVerifiedText: 'This contact method is verified so there is nothing to do here at the moment. Soon you will be able to delete this contact method if desired.', + yourDefaultContactMethod: 'This is your current default contact method. You will not be able to delete this contact method until you set an alternative default by selecting another contact method and pressing “Set as default”.', + removeContactMethod: 'Remove contact method', + removeAreYouSure: 'Are you sure you want to remove this contact method? This action cannot be undone.', + resendMagicCode: 'Resend magic code', + genericFailureMessages: { + requestContactMethodValidateCode: 'Failed to send a new magic code. Please wait a bit and try again.', + }, }, pronouns: { coCos: 'Co / Cos', diff --git a/src/languages/es.js b/src/languages/es.js index 4fc8c752cb50..967cdd07a8a3 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -118,6 +118,7 @@ export default { youAppearToBeOffline: 'Parece que estás desconectado.', thisFeatureRequiresInternet: 'Esta función requiere una conexión a Internet activa para ser utilizada.', areYouSure: '¿Estás seguro?', + yesContinue: '', zipCodeExample: 'p. ej. 12345, 12345-1234, 12345 1234', websiteExample: 'p. ej. https://www.expensify.com', }, @@ -345,8 +346,15 @@ export default { helpTextAfterEmail: '', pleaseVerify: '', getInTouch: '', - clickVerificationLink: '', + enterMagicCode: ({contactMethod}) => ``, tempContactVerifiedText: '', + yourDefaultContactMethod: '', + removeContactMethod: '', + removeAreYouSure: '', + resendMagicCode: '', + genericFailureMessages: { + requestContactMethodValidateCode: '', + }, }, pronouns: { coCos: 'Co / Cos', From e9601259fa72b9037c7c55330aba81067e825c6f Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 22 Feb 2023 15:46:30 +0200 Subject: [PATCH 16/55] New command for requesting magic code for contact method --- src/libs/actions/User.js | 54 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index c3be431f4c25..43a944a94934 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -100,6 +100,59 @@ function resendValidateCode(login, isPasswordless = false) { DeprecatedAPI.ResendValidateCode({email: login, isPasswordless}); } +/** + * @param {String} contactMethod - the new contact method that the user is trying to verify + */ +function requestContactMethodValidateCode(contactMethod) { + const optimisticData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: { + validateCodeSent: true, + errorFields: { + validateCodeSent: null, + }, + pendingFields: { + validateCodeSent: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + }]; + const successData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: { + pendingFields: { + validateCodeSent: null, + }, + }, + }, + }]; + const failureData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: { + validateCodeSent: false, + errorFields: { + validateCodeSent: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('contacts.genericFailureMessages.requestContactMethodValidateCode'), + }, + }, + pendingFields: { + validateCodeSent: null, + }, + }, + }, + }]; + + API.write('RequestContactMethodValidateCode', { + email: contactMethod, + }, {optimisticData, successData, failureData}); +} + /** * Sets whether or not the user is subscribed to Expensify news * @@ -539,6 +592,7 @@ export { updatePassword, closeAccount, resendValidateCode, + requestContactMethodValidateCode, updateNewsletterSubscription, setSecondaryLoginAndNavigate, deleteContactMethod, From a0ffda243ce8575dc2e6056fecef964fed72d639 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 22 Feb 2023 18:51:43 +0200 Subject: [PATCH 17/55] Move pages so page transitions look correct --- .../AppNavigator/ModalStackNavigators.js | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index a2b481df2d82..d1d11fc0f23c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -217,6 +217,20 @@ const SettingsModalStackNavigator = createModalStackNavigator([ }, name: 'Settings_Profile', }, + { + getComponent: () => { + const SettingsContactMethodDetailsPage = require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default; + return SettingsContactMethodDetailsPage; + }, + name: 'Settings_ContactMethodDetails', + }, + { + getComponent: () => { + const SettingsContactMethodsPage = require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default; + return SettingsContactMethodsPage; + }, + name: 'Settings_ContactMethods', + }, { getComponent: () => { const SettingsPronounsPage = require('../../../pages/settings/Profile/PronounsPage').default; @@ -273,20 +287,6 @@ const SettingsModalStackNavigator = createModalStackNavigator([ }, name: 'Settings_PersonalDetails_Address', }, - { - getComponent: () => { - const SettingsContactMethodsPage = require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default; - return SettingsContactMethodsPage; - }, - name: 'Settings_ContactMethods', - }, - { - getComponent: () => { - const SettingsContactMethodDetailsPage = require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default; - return SettingsContactMethodDetailsPage; - }, - name: 'Settings_ContactMethodDetails', - }, { getComponent: () => { const SettingsAddSecondaryLoginPage = require('../../../pages/settings/Profile/Contacts/AddSecondaryLoginPage').default; From 32f443c7acb266612836d10ff4a44fcb92b3bbf0 Mon Sep 17 00:00:00 2001 From: Alex Beaman Date: Wed, 22 Feb 2023 20:37:07 +0200 Subject: [PATCH 18/55] Add validate contact method button --- .../Contacts/ContactMethodDetailsPage.js | 81 +++++++++---------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js index f5de6444688d..02b957b93bb2 100644 --- a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -23,6 +23,7 @@ import TextInput from '../../../../components/TextInput'; import CONST from '../../../../CONST'; import Icon from '../../../../components/Icon'; import colors from '../../../../styles/colors'; +import Button from '../../../../components/Button'; const propTypes = { /* Onyx Props */ @@ -67,6 +68,7 @@ class ContactMethodDetailsPage extends Component { this.confirmDeleteAndHideModal = this.confirmDeleteAndHideModal.bind(this); this.resendValidateCode = this.resendValidateCode.bind(this); this.getContactMethod = this.getContactMethod.bind(this); + this.validateContactMethod = this.validateContactMethod.bind(this); this.state = { isDeleteModalOpen: false, @@ -103,6 +105,13 @@ class ContactMethodDetailsPage extends Component { User.requestContactMethodValidateCode(this.getContactMethod()); } + /** + * Attempt to validate this contact method + */ + validateContactMethod() { + User.validateSecondaryLogin(this.getContactMethod(), this.state.validateCode); + } + render() { const contactMethod = this.getContactMethod(); const loginData = this.props.loginList[contactMethod]; @@ -132,48 +141,38 @@ class ContactMethodDetailsPage extends Component { danger /> {!loginData.validatedDate && ( - <> - {/* console.log('hi')} - shouldShowRightIcon - success - /> */} - {/* */} - - - - - - - - {this.props.translate('contacts.enterMagicCode', {contactMethod})} - - - - this.setState({validateCode: text})} - // onSubmitEditing={this.validateAndSubmitForm} - keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} - errorText="" - blurOnSubmit={false} - /> - - - {this.props.translate('contacts.resendMagicCode')} + + + + + + {this.props.translate('contacts.enterMagicCode', {contactMethod})} - + - + this.setState({validateCode: text})} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + blurOnSubmit={false} + /> + + + {this.props.translate('contacts.resendMagicCode')} + + +