diff --git a/assets/images/bank.svg b/assets/images/bank.svg new file mode 100644 index 000000000000..4e17ec42c591 --- /dev/null +++ b/assets/images/bank.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/images/example-check-image.png b/assets/images/example-check-image.png new file mode 100644 index 000000000000..c6ef809ed983 Binary files /dev/null and b/assets/images/example-check-image.png differ diff --git a/assets/images/paycheck.svg b/assets/images/paycheck.svg new file mode 100644 index 000000000000..231545aec85d --- /dev/null +++ b/assets/images/paycheck.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/CONST.js b/src/CONST.js index 71a80cf4f0b7..52c6ec618333 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -6,6 +6,16 @@ const CONST = { IOS: 'https://apps.apple.com/us/app/expensify-cash/id1530278510', DESKTOP: 'https://expensify.cash/Expensify.cash.dmg', }, + BANK_ACCOUNT: { + ADD_METHOD: { + MANUAL: 'manual', + PLAID: 'plaid', + }, + REGEX: { + IBAN: /^[A-Za-z0-9]{2,30}$/, + SWIFT_BIC: /^[A-Za-z0-9]{8,11}$/, + }, + }, BETAS: { ALL: 'all', CHRONOS_IN_CASH: 'chronosInCash', diff --git a/src/ROUTES.js b/src/ROUTES.js index 0a84cb15ed98..8fbb80f1853c 100644 --- a/src/ROUTES.js +++ b/src/ROUTES.js @@ -9,6 +9,7 @@ const REPORT = 'r'; export default { ADD_PERSONAL_BANK_ACCOUNT: 'add-personal-bank-account', + BANK_ACCOUNT_NEW: 'bank-account/new', HOME: '', SETTINGS: 'settings', SETTINGS_PROFILE: 'settings/profile', diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js new file mode 100644 index 000000000000..1743eaeb3d82 --- /dev/null +++ b/src/components/AddPlaidBankAccount.js @@ -0,0 +1,202 @@ +import _ from 'underscore'; +import React from 'react'; +import { + ActivityIndicator, + View, + TextInput, +} from 'react-native'; +import PropTypes from 'prop-types'; +import lodashGet from 'lodash/get'; +import {withOnyx} from 'react-native-onyx'; +import PlaidLink from './PlaidLink'; +import { + clearPlaidBankAccountsAndToken, + fetchPlaidLinkToken, + getPlaidBankAccounts, +} from '../libs/actions/BankAccounts'; +import ONYXKEYS from '../ONYXKEYS'; +import styles from '../styles/styles'; +import canFocusInputOnScreenFocus from '../libs/canFocusInputOnScreenFocus'; +import compose from '../libs/compose'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; +import Button from './Button'; +import Picker from './Picker'; +import Icon from './Icon'; +import {DownArrow} from './Icon/Expensicons'; +import Text from './Text'; + +const propTypes = { + ...withLocalizePropTypes, + + /** Plaid SDK token to use to initialize the widget */ + plaidLinkToken: PropTypes.string, + + /** Contains list of accounts and loading state while fetching them */ + plaidBankAccounts: PropTypes.shape({ + /** Whether we are fetching the bank accounts from the API */ + loading: PropTypes.bool, + + /** List of accounts */ + accounts: PropTypes.arrayOf(PropTypes.object), + }), + + /** Fired when the user exits the Plaid flow */ + onExitPlaid: PropTypes.func, + + /** Fired when the user selects an account and submits the form */ + onSubmit: PropTypes.func, + + /** Additional text to display */ + text: PropTypes.string, +}; + +const defaultProps = { + plaidLinkToken: '', + plaidBankAccounts: { + loading: false, + }, + onExitPlaid: () => {}, + onSubmit: () => {}, + text: '', +}; + +class AddPlaidBankAccount extends React.Component { + constructor(props) { + super(props); + + this.selectAccount = this.selectAccount.bind(this); + + this.state = { + selectedIndex: undefined, + password: '', + isCreatingAccount: false, + institution: {}, + }; + } + + componentDidMount() { + clearPlaidBankAccountsAndToken(); + fetchPlaidLinkToken(); + } + + /** + * Get list of bank accounts + * + * @returns {Object[]} + */ + getAccounts() { + return lodashGet(this.props.plaidBankAccounts, 'accounts', []); + } + + selectAccount() { + const account = this.getAccounts()[this.state.selectedIndex]; + this.props.onSubmit({ + account, password: this.state.password, plaidLinkToken: this.props.plaidLinkToken, + }); + this.setState({isCreatingAccount: true}); + } + + render() { + const accounts = this.getAccounts(); + const options = _.chain(accounts) + .filter(account => !account.alreadyExists) + .map((account, index) => ({ + value: index, label: `${account.addressName} ${account.accountNumber}`, + })) + .value(); + + return ( + <> + {(!this.props.plaidLinkToken || this.props.plaidBankAccounts.loading) + && ( + + + + )} + {!_.isEmpty(this.props.plaidLinkToken) && ( + { + getPlaidBankAccounts(publicToken, metadata.institution.name); + this.setState({institution: metadata.institution}); + }} + onError={(error) => { + console.debug(`Plaid Error: ${error.message}`); + }} + + // User prematurely exited the Plaid flow + // eslint-disable-next-line react/jsx-props-no-multi-spaces + onExit={this.props.onExitPlaid} + /> + )} + {accounts.length > 0 && ( + <> + + {!_.isEmpty(this.props.text) && ( + {this.props.text} + )} + {/* @TODO there are a bunch of logos to incorporate here to replace this name + https://d2k5nsl2zxldvw.cloudfront.net/images/plaid/bg_plaidLogos_12@2x.png */} + {this.state.institution.name} + + { + this.setState({selectedIndex: Number(index)}); + }} + items={options} + placeholder={_.isUndefined(this.state.selectedIndex) ? { + value: '', + label: this.props.translate('bankAccount.chooseAnAccount'), + } : {}} + value={this.state.selectedIndex} + icon={() => } + /> + + {!_.isUndefined(this.state.selectedIndex) && ( + + + {this.props.translate('addPersonalBankAccountPage.enterPassword')} + + this.setState({password: text})} + /> + + )} + + +