diff --git a/src/ROUTES.js b/src/ROUTES.js index 74f3ba233bdf..65add35fbc58 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/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 6430d463a712..f2a92f9df98c 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -15,6 +15,8 @@ import {policyPropTypes} from '../pages/workspace/withPolicy'; import walletTermsPropTypes from '../pages/EnablePayments/walletTermsPropTypes'; import * as PolicyUtils from '../libs/PolicyUtils'; import * as PaymentMethods from '../libs/actions/PaymentMethods'; +import * as UserUtils from '../libs/UserUtils'; +import themeColors from '../styles/themes/default'; const propTypes = { /** URL for the avatar */ @@ -26,6 +28,8 @@ const propTypes = { /** To show a tooltip on hover */ tooltipText: PropTypes.string, + /* Onyx Props */ + /** The employee list of all policies (coming from Onyx) */ policiesMemberList: PropTypes.objectOf(policyMemberPropType), @@ -43,6 +47,15 @@ const propTypes = { /** Information about the user accepting the terms for payments */ walletTerms: walletTermsPropTypes, + + /** Login list for the user that is signed in */ + loginList: PropTypes.shape({ + /** Date login was validated, used to show info indicator status */ + validatedDate: PropTypes.string, + + /** Field-specific server side errors keyed by microtime */ + errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + }), }; const defaultProps = { @@ -54,45 +67,50 @@ const defaultProps = { cardList: {}, userWallet: {}, walletTerms: {}, + loginList: {}, }; const AvatarWithIndicator = (props) => { - const isLarge = props.size === 'large'; - const indicatorStyles = [ - styles.alignItemsCenter, - styles.justifyContentCenter, - isLarge ? styles.statusIndicatorLarge : styles.statusIndicator, - ]; - // If a policy was just deleted from Onyx, then Onyx will pass a null value to the props, and // those should be cleaned out before doing any error checking const cleanPolicies = _.pick(props.policies, policy => policy); const cleanPolicyMembers = _.pick(props.policiesMemberList, member => member); - // All of the error-checking methods are put into an array. This is so that using _.some() will return - // early as soon as the first error is returned. This makes the error checking very efficient since - // we only care if a single error exists anywhere. + // All of the error & info-checking methods are put into an array. This is so that using _.some() will return + // early as soon as the first error / info condition is returned. This makes the checks very efficient since + // we only care if a single error / info condition exists anywhere. const errorCheckingMethods = [ () => !_.isEmpty(props.userWallet.errors), () => PaymentMethods.hasPaymentMethodError(props.bankAccountList, props.cardList), () => _.some(cleanPolicies, PolicyUtils.hasPolicyError), () => _.some(cleanPolicies, PolicyUtils.hasCustomUnitsError), () => _.some(cleanPolicyMembers, PolicyUtils.hasPolicyMemberError), + () => UserUtils.hasLoginListError(props.loginList), // Wallet term errors that are not caused by an IOU (we show the red brick indicator for those in the LHN instead) () => !_.isEmpty(props.walletTerms.errors) && !props.walletTerms.chatReportID, ]; - const shouldShowIndicator = _.some(errorCheckingMethods, errorCheckingMethod => errorCheckingMethod()); + const infoCheckingMethods = [ + () => UserUtils.hasLoginListInfo(props.loginList), + ]; + const shouldShowErrorIndicator = _.some(errorCheckingMethods, errorCheckingMethod => errorCheckingMethod()); + const shouldShowInfoIndicator = !shouldShowErrorIndicator && _.some(infoCheckingMethods, infoCheckingMethod => infoCheckingMethod()); + + const indicatorColor = shouldShowErrorIndicator ? themeColors.danger : themeColors.success; + const indicatorStyles = [ + styles.alignItemsCenter, + styles.justifyContentCenter, + styles.statusIndicator(indicatorColor), + ]; return ( - + - {shouldShowIndicator && ( + {(shouldShowErrorIndicator || shouldShowInfoIndicator) && ( )} @@ -123,4 +141,7 @@ export default withOnyx({ walletTerms: { key: ONYXKEYS.WALLET_TERMS, }, + loginList: { + key: ONYXKEYS.LOGIN_LIST, + }, })(AvatarWithIndicator); diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 33563bb029be..b23cf1ba6f8c 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -62,7 +62,7 @@ const MenuItem = (props) => { const descriptionTextStyle = StyleUtils.combineStyles([ styles.textLabelSupporting, (props.icon ? styles.ml3 : undefined), - styles.breakAll, + styles.breakWord, styles.lineHeightNormal, ], props.style); diff --git a/src/languages/en.js b/src/languages/en.js index c98c2a5e9a72..7d76503bd399 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -118,6 +118,8 @@ export default { youAppearToBeOffline: 'You appear to be offline.', thisFeatureRequiresInternet: 'This feature requires an active internet connection to be used.', areYouSure: 'Are you sure?', + verify: 'Verify', + yesContinue: 'Yes, continue', zipCodeExample: 'e.g. 12345, 12345-1234, 12345 1234', websiteExample: 'e.g. https://www.expensify.com', }, @@ -341,6 +343,20 @@ 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.", + enterMagicCode: ({contactMethod}) => `Please enter the magic code sent to ${contactMethod}`, + 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.', + validateSecondaryLogin: 'Failed to validate contact method with given magic code. Please request a new code and try again.', + deleteContactMethod: 'Failed to delete contact method. Please reach out to Concierge for help.', + }, }, pronouns: { coCos: 'Co / Cos', diff --git a/src/languages/es.js b/src/languages/es.js index 9e2775d26757..a1f08793056a 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -118,6 +118,8 @@ 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?', + verify: 'Verifique', + yesContinue: 'Sí, Continuar', zipCodeExample: 'p. ej. 12345, 12345-1234, 12345 1234', websiteExample: 'p. ej. https://www.expensify.com', }, @@ -341,6 +343,20 @@ export default { contacts: { contactMethod: 'Método de contacto', contactMethods: 'Métodos de contacto', + helpTextBeforeEmail: 'Añade más formas de que la gente te encuentre y reenvía los recibos a ', + helpTextAfterEmail: ' desde varias direcciones de correo electrónico.', + pleaseVerify: 'Por favor verifica este método de contacto', + getInTouch: 'Utilizaremos este método de contacto cuando necesitemos contactarte.', + enterMagicCode: ({contactMethod}) => `Por favor, introduce el código mágico enviado a ${contactMethod}`, + yourDefaultContactMethod: 'Este es tu método de contacto predeterminado. No podrás eliminarlo hasta que añadas otro método de contacto y lo marques como predeterminado pulsando "Establecer como predeterminado".', + removeContactMethod: 'Eliminar método de contacto', + removeAreYouSure: '¿Estás seguro de que quieres eliminar este método de contacto? Esta acción no se puede deshacer.', + resendMagicCode: 'Reenviar código mágico', + genericFailureMessages: { + requestContactMethodValidateCode: 'No se ha podido enviar un nuevo código mágico. Espera un rato y vuelve a intentarlo.', + validateSecondaryLogin: 'No se ha podido validar el método de contacto con el código mágico provisto. Solicita un nuevo código y vuelve a intentarlo.', + deleteContactMethod: 'No se ha podido eliminar el método de contacto. Por favor contacta con Concierge para obtener ayuda.', + }, }, pronouns: { coCos: 'Co / Cos', diff --git a/src/libs/ErrorUtils.js b/src/libs/ErrorUtils.js index e1ca7233ba8a..8173c57ad8a6 100644 --- a/src/libs/ErrorUtils.js +++ b/src/libs/ErrorUtils.js @@ -1,4 +1,5 @@ import _ from 'underscore'; +import lodashGet from 'lodash/get'; import CONST from '../CONST'; /** @@ -54,8 +55,30 @@ function getLatestErrorMessage(onyxData) { .value(); } +/** + * @param {Object} onyxData + * @param {Object} onyxData.errorFields + * @param {String} fieldName + * @returns {Object} + */ +function getLatestErrorField(onyxData, fieldName) { + const errorsForField = lodashGet(onyxData, ['errorFields', fieldName], {}); + + if (_.isEmpty(errorsForField)) { + return {}; + } + return _.chain(errorsForField) + .keys() + .sortBy() + .reverse() + .map(key => ({[key]: errorsForField[key]})) + .first() + .value(); +} + export { // eslint-disable-next-line import/prefer-default-export getAuthenticateErrorMessage, getLatestErrorMessage, + getLatestErrorField, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 07c67bec110e..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,13 +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 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/libs/UserUtils.js b/src/libs/UserUtils.js new file mode 100644 index 000000000000..748ad18dac69 --- /dev/null +++ b/src/libs/UserUtils.js @@ -0,0 +1,68 @@ +import _ from 'underscore'; +import lodashGet from 'lodash/get'; +import CONST from '../CONST'; + +/** + * Searches through given loginList for any contact method / login with an error. + * + * Example that should return false: + * {{ + * test@test.com: { + * errorFields: { + * validateCodeSent: null + * } + * } + * }} + * + * Example that should return true: + * {{ + * test@test.com: { + * errorFields: { + * validateCodeSent: { 18092081290: 'An error' } + * } + * } + * }} + * + * @param {Object} loginList + * @param {Object} loginList.errorFields + * @returns {Boolean} + */ +function hasLoginListError(loginList) { + return _.some(loginList, login => _.some(lodashGet(login, 'errorFields', {}), field => !_.isEmpty(field))); +} + +/** + * Searches through given loginList for any contact method / login that requires + * an Info brick road status indicator. Currently this only applies if the user + * has an unvalidated contact method. + * + * @param {Object} loginList + * @param {String} loginList.validatedDate + * @returns {Boolean} + */ +function hasLoginListInfo(loginList) { + return _.some(loginList, login => _.isEmpty(login.validatedDate)); +} + +/** + * Gets the appropriate brick road indicator status for a given loginList. + * Error status is higher priority, so we check for that first. + * + * @param {Object} loginList + * @returns {String} + */ +function getLoginListBrickRoadIndicator(loginList) { + if (hasLoginListError(loginList)) { + return CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR; + } + if (hasLoginListInfo(loginList)) { + return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; + } + return ''; +} + +export { + hasLoginListError, + hasLoginListInfo, + getLoginListBrickRoadIndicator, +}; diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 7e7b51e93d3d..01daca6f7a28 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'; import * as Session from './Session'; let currentUserAccountID = ''; @@ -100,6 +101,61 @@ function resendValidateCode(login) { Session.resendValidateCode(login); } +/** + * Requests a new validate code be sent for the passed contact method + * + * @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 * @@ -162,6 +218,77 @@ 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: '', + 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()]: Localize.translateLocal('contacts.genericFailureMessages.deleteContactMethod'), + }, + }, + pendingFields: { + deletedLogin: null, + }, + }, + }, + }]; + + API.write('DeleteContactMethod', { + partnerUserID: contactMethod, + }, {optimisticData, successData, failureData}); +} + +/** + * Clears any possible stored errors for a specific field on a contact method + * + * @param {String} contactMethod + * @param {String} fieldName + */ +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 * @@ -187,6 +314,62 @@ function validateLogin(accountID, validateCode) { Navigation.navigate(ROUTES.HOME); } +/** + * Validates a secondary login / contact method + * + * @param {String} contactMethod - The contact method the user is trying to verify + * @param {String} validateCode + */ +function validateSecondaryLogin(contactMethod, validateCode) { + const optimisticData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: { + errorFields: { + validateLogin: null, + }, + pendingFields: { + validateLogin: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + }]; + const successData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: { + pendingFields: { + validateLogin: null, + }, + }, + }, + }]; + const failureData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.LOGIN_LIST, + value: { + [contactMethod]: { + errorFields: { + validateLogin: { + [DateUtils.getMicroseconds()]: Localize.translateLocal('contacts.genericFailureMessages.validateSecondaryLogin'), + }, + }, + pendingFields: { + validateLogin: null, + }, + }, + }, + }]; + + API.write('ValidateSecondaryLogin', { + partnerUserID: contactMethod, + validateCode, + }, {optimisticData, successData, failureData}); + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); +} + /** * Checks the blockedFromConcierge object to see if it has an expiresAt key, * and if so whether the expiresAt date of a user's ban is before right now @@ -474,9 +657,13 @@ export { updatePassword, closeAccount, resendValidateCode, + requestContactMethodValidateCode, updateNewsletterSubscription, setSecondaryLoginAndNavigate, + deleteContactMethod, + clearContactMethodErrors, validateLogin, + validateSecondaryLogin, isBlockedFromConcierge, subscribeToUserEvents, updatePreferredSkinTone, diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index a5696d3e02e5..160729b91f5b 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -34,6 +34,7 @@ import ConfirmModal from '../../components/ConfirmModal'; import * as ReportUtils from '../../libs/ReportUtils'; import * as Link from '../../libs/actions/Link'; import OfflineWithFeedback from '../../components/OfflineWithFeedback'; +import * as UserUtils from '../../libs/UserUtils'; const propTypes = { /* Onyx Props */ @@ -80,6 +81,15 @@ const propTypes = { /** Information about the user accepting the terms for payments */ walletTerms: walletTermsPropTypes, + /** Login list for the user that is signed in */ + loginList: PropTypes.shape({ + /** Date login was validated, used to show brickroad info status */ + validatedDate: PropTypes.string, + + /** Field-specific server side errors keyed by microtime */ + errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + }), + ...withLocalizePropTypes, ...withCurrentUserPersonalDetailsPropTypes, }; @@ -145,6 +155,7 @@ class InitialSettingsPage extends React.Component { .filter(policy => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) .find(policy => PolicyUtils.hasPolicyError(policy) || PolicyUtils.getPolicyBrickRoadIndicatorStatus(policy, this.props.policyMembers)) .value() ? 'error' : null; + const profileBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(this.props.loginList); return ([ { @@ -159,6 +170,7 @@ class InitialSettingsPage extends React.Component { translationKey: 'common.profile', icon: Expensicons.Profile, action: () => { App.openProfile(); }, + brickRoadIndicator: profileBrickRoadIndicator, }, { translationKey: 'common.preferences', @@ -209,7 +221,6 @@ class InitialSettingsPage extends React.Component { iconType={item.iconType} onPress={item.action} iconStyles={item.iconStyles} - iconFill={item.iconFill} shouldShowRightIcon iconRight={item.iconRight} badgeText={this.getWalletBalance(isPaymentItem)} @@ -340,6 +351,9 @@ export default compose( walletTerms: { key: ONYXKEYS.WALLET_TERMS, }, + loginList: { + key: ONYXKEYS.LOGIN_LIST, + }, }), withNetwork(), )(InitialSettingsPage); diff --git a/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js new file mode 100644 index 000000000000..2ae32c9a4e3a --- /dev/null +++ b/src/pages/settings/Profile/Contacts/ContactMethodDetailsPage.js @@ -0,0 +1,257 @@ +import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; +import React, {Component} from 'react'; +import {View, ScrollView, TouchableOpacity} from 'react-native'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +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 ONYXKEYS from '../../../../ONYXKEYS'; +import withLocalize, {withLocalizePropTypes} from '../../../../components/withLocalize'; +import MenuItem from '../../../../components/MenuItem'; +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'; +import TextInput from '../../../../components/TextInput'; +import CONST from '../../../../CONST'; +import Icon from '../../../../components/Icon'; +import colors from '../../../../styles/colors'; +import Button from '../../../../components/Button'; +import * as ErrorUtils from '../../../../libs/ErrorUtils'; +import themeColors from '../../../../styles/themes/default'; + +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 when login was validated */ + validatedDate: PropTypes.string, + + /** Field-specific server side errors keyed by microtime */ + errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + + /** Field-specific pending states for offline UI status */ + pendingFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), + }), + + /** Current user session */ + session: PropTypes.shape({ + email: PropTypes.string.isRequired, + }).isRequired, + + /** Route params */ + route: PropTypes.shape({ + params: PropTypes.shape({ + /** passed via route /settings/profile/contact-methods/:contactMethod/details */ + contactMethod: PropTypes.string, + }), + }), + + ...withLocalizePropTypes, +}; + +const defaultProps = { + loginList: {}, + route: { + params: { + contactMethod: '', + }, + }, +}; + +class ContactMethodDetailsPage extends Component { + constructor(props) { + 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.validateContactMethod = this.validateContactMethod.bind(this); + + this.state = { + isDeleteModalOpen: false, + validateCode: '', + }; + } + + componentDidMount() { + if (this.getContactMethod()) { + return; + } + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); + } + + /** + * Gets the current contact method from the route params + * + * @returns {string} + */ + getContactMethod() { + return decodeURIComponent(lodashGet(this.props.route, 'params.contactMethod')); + } + + /** + * Toggle delete confirm modal visibility + * @param {Boolean} shouldOpen + */ + toggleDeleteModal(shouldOpen) { + this.setState({isDeleteModalOpen: shouldOpen}); + } + + /** + * 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()); + } + + /** + * 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]; + if (!contactMethod || !loginData) { + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS); + return null; + } + + const isDefaultContactMethod = (this.props.session.email === loginData.partnerUserID); + const hasMagicCodeBeenSent = lodashGet(this.props.loginList, [contactMethod, 'validateCodeSent'], false); + + return ( + + Navigation.navigate(ROUTES.SETTINGS_CONTACT_METHODS)} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + + this.toggleDeleteModal(false)} + prompt={this.props.translate('contacts.removeAreYouSure')} + confirmText={this.props.translate('common.yesContinue')} + isVisible={this.state.isDeleteModalOpen} + danger + /> + {!loginData.validatedDate && ( + + + + + + {this.props.translate('contacts.enterMagicCode', {contactMethod})} + + + + this.setState({validateCode: text})} + keyboardType={CONST.KEYBOARD_TYPE.NUMBER_PAD} + blurOnSubmit={false} + /> + User.clearContactMethodErrors(contactMethod, 'validateCodeSent')} + > + + + {this.props.translate('contacts.resendMagicCode')} + + {hasMagicCodeBeenSent && ( + + )} + + + User.clearContactMethodErrors(contactMethod, 'validateLogin')} + > +