diff --git a/src/CONST.js b/src/CONST.js index 83922e707316..78e1ac329947 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -432,6 +432,12 @@ const CONST = { OTHER: 'other', }, + PAYMENT_METHODS: { + PAYPAL: 'payPalMe', + DEBIT_CARD: 'debitCard', + BANK_ACCOUNT: 'bankAccount', + }, + IOU: { // Note: These payment types are used when building IOU reportAction message values in the server and should // not be changed. diff --git a/src/ROUTES.js b/src/ROUTES.js index f3b856a469de..463ecdd1126f 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -29,6 +29,7 @@ export default { SETTINGS_PAYMENTS: 'settings/payments', SETTINGS_ADD_PAYPAL_ME: 'settings/payments/add-paypal-me', SETTINGS_ADD_DEBIT_CARD: 'settings/payments/add-debit-card', + SETTINGS_ADD_BANK_ACCOUNT: 'settings/payments/add-bank-account', SETTINGS_ADD_LOGIN: 'settings/addlogin/:type', getSettingsAddLoginRoute: type => `settings/addlogin/${type}`, NEW_GROUP: 'new/group', diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js new file mode 100644 index 000000000000..5e77e2fd47b8 --- /dev/null +++ b/src/components/AddPaymentMethodMenu.js @@ -0,0 +1,71 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import Popover from './Popover'; +import MenuItem from './MenuItem'; +import * as Expensicons from './Icon/Expensicons'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import styles from '../styles/styles'; +import compose from '../libs/compose'; +import ONYXKEYS from '../ONYXKEYS'; +import CONST from '../CONST'; + +const propTypes = { + isVisible: PropTypes.bool.isRequired, + onClose: PropTypes.func.isRequired, + anchorPosition: PropTypes.shape({ + top: PropTypes.number, + left: PropTypes.number, + }), + + /** Username for PayPal.Me */ + payPalMeUsername: PropTypes.string, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + anchorPosition: {}, + payPalMeUsername: '', +}; + +const AddPaymentMethodMenu = props => ( + + props.onItemSelected(CONST.PAYMENT_METHODS.BANK_ACCOUNT)} + wrapperStyle={styles.pr15} + /> + props.onItemSelected(CONST.PAYMENT_METHODS.DEBIT_CARD)} + wrapperStyle={styles.pr15} + /> + {!props.payPalMeUsername && ( + props.onItemSelected(CONST.PAYMENT_METHODS.PAYPAL)} + wrapperStyle={styles.pr15} + /> + )} + +); + +AddPaymentMethodMenu.propTypes = propTypes; +AddPaymentMethodMenu.defaultProps = defaultProps; + +export default compose( + withLocalize, + withOnyx({ + payPalMeUsername: { + key: ONYXKEYS.NVP_PAYPAL_ME_ADDRESS, + }, + }), +)(AddPaymentMethodMenu); diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index abd2f6b5b8cb..2950755bd77e 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -12,6 +12,7 @@ import PlaidLink from './PlaidLink'; import * as BankAccounts from '../libs/actions/BankAccounts'; import ONYXKEYS from '../ONYXKEYS'; import styles from '../styles/styles'; +import canFocusInputOnScreenFocus from '../libs/canFocusInputOnScreenFocus'; import themeColors from '../styles/themes/default'; import compose from '../libs/compose'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; @@ -21,10 +22,9 @@ import * as ReimbursementAccountUtils from '../libs/ReimbursementAccountUtils'; import ReimbursementAccountForm from '../pages/ReimbursementAccount/ReimbursementAccountForm'; import getBankIcon from './Icon/BankIcons'; import Icon from './Icon'; +import ExpensiTextInput from './ExpensiTextInput'; const propTypes = { - ...withLocalizePropTypes, - /** Plaid SDK token to use to initialize the widget */ plaidLinkToken: PropTypes.string, @@ -78,6 +78,11 @@ const propTypes = { /** During the OAuth flow we need to use the plaidLink token that we initially connected with */ plaidLinkOAuthToken: PropTypes.string, + + /** Should we require a password to create a bank account? */ + isPasswordRequired: PropTypes.bool, + + ...withLocalizePropTypes, }; const defaultProps = { @@ -90,6 +95,7 @@ const defaultProps = { text: '', receivedRedirectURI: null, plaidLinkOAuthToken: '', + isPasswordRequired: false, }; class AddPlaidBankAccount extends React.Component { @@ -102,10 +108,14 @@ class AddPlaidBankAccount extends React.Component { this.state = { selectedIndex: undefined, institution: {}, + password: '', }; this.getErrors = () => ReimbursementAccountUtils.getErrors(this.props); this.clearError = inputKey => ReimbursementAccountUtils.clearError(this.props, inputKey); + this.getErrorText = inputKey => ReimbursementAccountUtils.getErrorText(this.props, { + password: 'passwordForm.error.incorrectLoginOrPassword', + }, inputKey); } componentDidMount() { @@ -119,6 +129,10 @@ class AddPlaidBankAccount extends React.Component { BankAccounts.fetchPlaidLinkToken(); } + componentWillUnmount() { + BankAccounts.setBankAccountFormValidationErrors({}); + } + /** * Get list of bank accounts * @@ -149,6 +163,11 @@ class AddPlaidBankAccount extends React.Component { if (_.isUndefined(this.state.selectedIndex)) { errors.selectedBank = true; } + + if (this.props.isPasswordRequired && _.isEmpty(this.state.password)) { + errors.password = true; + } + BankAccounts.setBankAccountFormValidationErrors(errors); return _.size(errors) === 0; } @@ -165,6 +184,7 @@ class AddPlaidBankAccount extends React.Component { bankName, account, plaidLinkToken: this.getPlaidLinkToken(), + password: this.state.password, }); } @@ -233,6 +253,22 @@ class AddPlaidBankAccount extends React.Component { hasError={this.getErrors().selectedBank} /> + {!_.isUndefined(this.state.selectedIndex) && this.props.isPasswordRequired && ( + + this.setState({password: text})} + errorText={this.getErrorText('password')} + hasError={this.getErrors().password} + /> + + )} )} diff --git a/src/languages/en.js b/src/languages/en.js index 85f944a7380d..94a88cfc9599 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -89,6 +89,7 @@ export default { more: 'More', debitCard: 'Debit card', payPalMe: 'PayPal.me', + bankAccount: 'Bank account', }, attachmentPicker: { cameraPermissionRequired: 'Camera permission required', @@ -475,6 +476,7 @@ export default { }, }, addPersonalBankAccountPage: { + enterPassword: 'Enter Expensify password', alreadyAdded: 'This account has already been added.', chooseAccountLabel: 'Account', }, diff --git a/src/languages/es.js b/src/languages/es.js index db90ccfbec0c..b319106a03bd 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -89,6 +89,7 @@ export default { more: 'Más', debitCard: 'Tarjeta de débito', payPalMe: 'PayPal.me', + bankAccount: 'Cuenta bancaria', }, attachmentPicker: { cameraPermissionRequired: 'Se necesita permiso para usar la cámara', @@ -475,6 +476,7 @@ export default { }, }, addPersonalBankAccountPage: { + enterPassword: 'Escribe tu contraseña de Expensify', alreadyAdded: 'Esta cuenta ya ha sido agregada.', chooseAccountLabel: 'Cuenta', }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 4b998a632ee4..c8d5677ea301 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -182,6 +182,10 @@ const SettingsModalStackNavigator = createModalStackNavigator([ Component: SettingsAddDebitCardPage, name: 'Settings_Add_Debit_Card', }, + { + Component: AddPersonalBankAccountPage, + name: 'Settings_Add_Bank_Account', + }, { Component: WorkspaceInitialPage, name: 'Workspace_Initial', diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 4e89c80ec813..be838f7391c3 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -56,6 +56,10 @@ export default { path: ROUTES.SETTINGS_ADD_DEBIT_CARD, exact: true, }, + Settings_Add_Bank_Account: { + path: ROUTES.SETTINGS_ADD_BANK_ACCOUNT, + exact: true, + }, Settings_Profile: { path: ROUTES.SETTINGS_PROFILE, exact: true, diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js index 587b33c94ae2..245c4fb02a83 100644 --- a/src/libs/actions/BankAccounts.js +++ b/src/libs/actions/BankAccounts.js @@ -1,7 +1,12 @@ import _ from 'underscore'; +import Onyx from 'react-native-onyx'; import CONST from '../../CONST'; import * as API from '../API'; import * as Plaid from './Plaid'; +import * as ReimbursementAccount from './ReimbursementAccount'; +import Navigation from '../Navigation/Navigation'; +import ONYXKEYS from '../../ONYXKEYS'; +import * as PaymentMethods from './PaymentMethods'; export { setupWithdrawalAccount, @@ -41,6 +46,7 @@ export { * @param {String} plaidLinkToken */ function addPersonalBankAccount(account, password, plaidLinkToken) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: true}); const unmaskedAccount = _.find(Plaid.getPlaidBankAccounts(), bankAccount => ( bankAccount.plaidAccountID === account.plaidAccountID )); @@ -71,12 +77,22 @@ function addPersonalBankAccount(account, password, plaidLinkToken) { }), }) .then((response) => { - if (response.jsonCode !== 200) { - alert('There was a problem adding this bank account.'); + if (response.jsonCode === 200) { + PaymentMethods.getPaymentMethods() + .then(() => { + Navigation.goBack(); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); + }); return; } - alert('Bank account added successfully.'); + if (response.message === 'Incorrect Expensify password entered') { + ReimbursementAccount.setBankAccountFormValidationErrors({password: true}); + } + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); + }) + .catch(() => { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {loading: false}); }); } diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js index 0736f19bca85..187b65ee696f 100644 --- a/src/pages/AddPersonalBankAccountPage.js +++ b/src/pages/AddPersonalBankAccountPage.js @@ -27,6 +27,8 @@ const AddPersonalBankAccountPage = props => ( Navigation.goBack()} /> { @@ -35,6 +37,7 @@ const AddPersonalBankAccountPage = props => ( onExitPlaid={Navigation.dismissModal} receivedRedirectURI={getPlaidOAuthReceivedRedirectURI()} plaidLinkOAuthToken={props.plaidLinkToken} + isPasswordRequired /> ); diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js index e31669167471..ea4d2ef7c519 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountForm.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountForm.js @@ -61,6 +61,7 @@ class ReimbursementAccountForm extends React.Component { }} message={this.props.reimbursementAccount.errorModalMessage} isMessageHtml={this.props.reimbursementAccount.isErrorModalMessageHtml} + isLoading={this.props.reimbursementAccount.loading} /> ); diff --git a/src/pages/settings/Payments/PaymentsPage.js b/src/pages/settings/Payments/PaymentsPage.js index d686c3163662..a49b2ada357f 100644 --- a/src/pages/settings/Payments/PaymentsPage.js +++ b/src/pages/settings/Payments/PaymentsPage.js @@ -13,16 +13,12 @@ import compose from '../../../libs/compose'; import KeyboardAvoidingView from '../../../components/KeyboardAvoidingView/index'; import ExpensifyText from '../../../components/ExpensifyText'; import * as PaymentMethods from '../../../libs/actions/PaymentMethods'; -import Popover from '../../../components/Popover'; -import * as Expensicons from '../../../components/Icon/Expensicons'; -import MenuItem from '../../../components/MenuItem'; import getClickedElementLocation from '../../../libs/getClickedElementLocation'; import CurrentWalletBalance from '../../../components/CurrentWalletBalance'; import ONYXKEYS from '../../../ONYXKEYS'; import Permissions from '../../../libs/Permissions'; - -const PAYPAL = 'payPalMe'; -const DEBIT_CARD = 'debitCard'; +import AddPaymentMethodMenu from '../../../components/AddPaymentMethodMenu'; +import CONST from '../../../CONST'; const propTypes = { ...withLocalizePropTypes, @@ -32,15 +28,11 @@ const propTypes = { /** Are we loading payment methods? */ isLoadingPaymentMethods: PropTypes.bool, - - /** Username for PayPal.Me */ - payPalMeUsername: PropTypes.string, }; const defaultProps = { betas: [], isLoadingPaymentMethods: true, - payPalMeUsername: '', }; class PaymentsPage extends React.Component { @@ -70,7 +62,7 @@ class PaymentsPage extends React.Component { */ paymentMethodPressed(nativeEvent, account) { if (account) { - if (account === PAYPAL) { + if (account === CONST.PAYMENT_METHODS.PAYPAL) { Navigation.navigate(ROUTES.SETTINGS_ADD_PAYPAL_ME); } } else { @@ -93,13 +85,22 @@ class PaymentsPage extends React.Component { addPaymentMethodTypePressed(paymentType) { this.hideAddPaymentMenu(); - if (paymentType === PAYPAL) { + if (paymentType === CONST.PAYMENT_METHODS.PAYPAL) { Navigation.navigate(ROUTES.SETTINGS_ADD_PAYPAL_ME); + return; } - if (paymentType === DEBIT_CARD) { + if (paymentType === CONST.PAYMENT_METHODS.DEBIT_CARD) { Navigation.navigate(ROUTES.SETTINGS_ADD_DEBIT_CARD); + return; + } + + if (paymentType === CONST.PAYMENT_METHODS.BANK_ACCOUNT) { + Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT); + return; } + + throw new Error('Invalid payment method type selected'); } /** @@ -135,29 +136,15 @@ class PaymentsPage extends React.Component { isAddPaymentMenuActive={this.state.shouldShowAddPaymentMenu} /> - - {!this.props.payPalMeUsername && ( - this.addPaymentMethodTypePressed(PAYPAL)} - wrapperStyle={styles.pr15} - /> - )} - this.addPaymentMethodTypePressed(DEBIT_CARD)} - wrapperStyle={styles.pr15} - /> - + onItemSelected={method => this.addPaymentMethodTypePressed(method)} + /> ); @@ -177,8 +164,5 @@ export default compose( key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS, initWithStoredValues: false, }, - payPalMeUsername: { - key: ONYXKEYS.NVP_PAYPAL_ME_ADDRESS, - }, }), )(PaymentsPage);