Skip to content

Commit

Permalink
Merge pull request #3459 from Expensify/marcaaron-addBusinessBankAcco…
Browse files Browse the repository at this point in the history
…untPage

Add VBA flow Part 1
  • Loading branch information
marcaaron authored Jun 11, 2021
2 parents 5d75abb + 6b3d5f5 commit 157db96
Show file tree
Hide file tree
Showing 24 changed files with 605 additions and 235 deletions.
3 changes: 3 additions & 0 deletions assets/images/bank.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/example-check-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/images/paycheck.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions src/ROUTES.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
202 changes: 202 additions & 0 deletions src/components/AddPlaidBankAccount.js
Original file line number Diff line number Diff line change
@@ -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)
&& (
<View style={[styles.flex1, styles.alignItemsCenter, styles.justifyContentCenter]}>
<ActivityIndicator size="large" />
</View>
)}
{!_.isEmpty(this.props.plaidLinkToken) && (
<PlaidLink
token={this.props.plaidLinkToken}
onSuccess={({publicToken, metadata}) => {
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 && (
<>
<View style={[styles.m5, styles.flex1]}>
{!_.isEmpty(this.props.text) && (
<Text style={[styles.mb5]}>{this.props.text}</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 */}
<Text style={[styles.mb5, styles.h1]}>{this.state.institution.name}</Text>
<View style={[styles.mb5]}>
<Picker
onChange={(index) => {
this.setState({selectedIndex: Number(index)});
}}
items={options}
placeholder={_.isUndefined(this.state.selectedIndex) ? {
value: '',
label: this.props.translate('bankAccount.chooseAnAccount'),
} : {}}
value={this.state.selectedIndex}
icon={() => <Icon src={DownArrow} />}
/>
</View>
{!_.isUndefined(this.state.selectedIndex) && (
<View style={[styles.mb5]}>
<Text style={[styles.formLabel]}>
{this.props.translate('addPersonalBankAccountPage.enterPassword')}
</Text>
<TextInput
secureTextEntry
style={[styles.textInput, styles.mb2]}
value={this.state.password}
autoCompleteType="password"
textContentType="password"
autoCapitalize="none"
autoFocus={canFocusInputOnScreenFocus()}
onChangeText={text => this.setState({password: text})}
/>
</View>
)}
</View>
<View style={[styles.m5]}>
<Button
success
text={this.props.translate('common.saveAndContinue')}
isLoading={this.state.isCreatingAccount}
onPress={this.selectAccount}
isDisabled={_.isUndefined(this.state.selectedIndex) || !this.state.password}
/>
</View>
</>
)}
</>
);
}
}

AddPlaidBankAccount.propTypes = propTypes;
AddPlaidBankAccount.defaultProps = defaultProps;

export default compose(
withLocalize,
withOnyx({
plaidLinkToken: {
key: ONYXKEYS.PLAID_LINK_TOKEN,
},
plaidBankAccounts: {
key: ONYXKEYS.PLAID_BANK_ACCOUNTS,
},
}),
)(AddPlaidBankAccount);
10 changes: 5 additions & 5 deletions src/components/Checkbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ const propTypes = {
/** Whether checkbox is checked */
isChecked: PropTypes.bool.isRequired,

/** A function that is called when the box/label is clicked on */
onClick: PropTypes.func.isRequired,
/** A function that is called when the box/label is pressed */
onPress: PropTypes.func.isRequired,

/** Text that appears next to check box */
label: PropTypes.string,
Expand All @@ -22,17 +22,17 @@ const defaultProps = {

const Checkbox = ({
isChecked,
onClick,
onPress,
label,
}) => (
<View style={styles.flexRow}>
<Pressable onPress={() => onClick(!isChecked)}>
<Pressable onPress={() => onPress(!isChecked)}>
<View style={[styles.checkboxContainer, isChecked && styles.checkedContainer]}>
<Icon src={Checkmark} fill="white" height={14} width={14} />
</View>
</Pressable>
{label && (
<Pressable onPress={() => onClick(!isChecked)}>
<Pressable onPress={() => onPress(!isChecked)}>
<Text style={[styles.ml2, styles.textP]}>
{label}
</Text>
Expand Down
58 changes: 58 additions & 0 deletions src/components/CheckboxWithLabel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from 'react';
import PropTypes from 'prop-types';
import {View, TouchableOpacity} from 'react-native';
import _ from 'underscore';
import styles from '../styles/styles';
import Checkbox from './Checkbox';

const propTypes = {
/** Component to display for label */
LabelComponent: PropTypes.func.isRequired,

/** Whether the checkbox is checked */
isChecked: PropTypes.bool.isRequired,

/** Called when the checkbox or label is pressed */
onPress: PropTypes.func.isRequired,

/** Container styles */
style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
};

const defaultProps = {
style: [],
};

const CheckboxWithLabel = ({
LabelComponent, isChecked, onPress, style,
}) => {
const defaultStyles = [styles.flexRow];
const wrapperStyles = _.isArray(style) ? [...defaultStyles, ...style] : [...defaultStyles, style];
return (
<View style={wrapperStyles}>
<Checkbox
isChecked={isChecked}
onPress={onPress}
/>
<TouchableOpacity
onPress={onPress}
style={[
styles.ml2,
styles.pr2,
styles.w100,
styles.flexRow,
styles.flexWrap,
styles.alignItemsCenter,
]}
>
<LabelComponent />
</TouchableOpacity>
</View>
);
};

CheckboxWithLabel.propTypes = propTypes;
CheckboxWithLabel.defaultProps = defaultProps;
CheckboxWithLabel.displayName = 'CheckboxWithLabel';

export default CheckboxWithLabel;
4 changes: 4 additions & 0 deletions src/components/Icon/Expensicons.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Android from '../../../assets/images/android.svg';
import Apple from '../../../assets/images/apple.svg';
import ArrowRight from '../../../assets/images/arrow-right.svg';
import BackArrow from '../../../assets/images/back-left.svg';
import Bank from '../../../assets/images/bank.svg';
import Bug from '../../../assets/images/bug.svg';
import Camera from '../../../assets/images/camera.svg';
import ChatBubble from '../../../assets/images/chatbubble.svg';
Expand All @@ -27,6 +28,7 @@ import Monitor from '../../../assets/images/monitor.svg';
import NewWindow from '../../../assets/images/new-window.svg';
import Offline from '../../../assets/images/offline.svg';
import Paperclip from '../../../assets/images/paperclip.svg';
import Paycheck from '../../../assets/images/paycheck.svg';
import Pencil from '../../../assets/images/pencil.svg';
import Phone from '../../../assets/images/phone.svg';
import Pin from '../../../assets/images/pin.svg';
Expand All @@ -47,6 +49,7 @@ export {
Apple,
ArrowRight,
BackArrow,
Bank,
Bug,
Camera,
ChatBubble,
Expand All @@ -72,6 +75,7 @@ export {
NewWindow,
Offline,
Paperclip,
Paycheck,
Pencil,
Phone,
Pin,
Expand Down
2 changes: 1 addition & 1 deletion src/components/Picker/PickerPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const propTypes = {
/** The items to display in the list of selections */
items: PropTypes.arrayOf(PropTypes.shape({
/** The value of the item that is being selected */
value: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,

/** The text to display for the item */
label: PropTypes.string.isRequired,
Expand Down
5 changes: 3 additions & 2 deletions src/components/TextInputWithLabel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ import styles from '../styles/styles';

const propTypes = {
/** Label text */
label: PropTypes.string.isRequired,
label: PropTypes.string,

/** Text to show if there is an error */
errorText: PropTypes.string,
};

const defaultProps = {
label: '',
errorText: '',
};

const TextInputWithLabel = props => (
<>
<Text style={[styles.formLabel]}>{props.label}</Text>
{!_.isEmpty(props.label) && <Text style={[styles.formLabel]}>{props.label}</Text>}
<TextInput
style={[styles.textInput, styles.mb1]}
// eslint-disable-next-line react/jsx-props-no-spreading
Expand Down
Loading

0 comments on commit 157db96

Please sign in to comment.