Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add VBA flow Part 1 #3459

Merged
merged 27 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
ecb9990
add business bank account new page
marcaaron Jun 8, 2021
c128cfe
add create manual form
marcaaron Jun 8, 2021
5db6177
Move logic out of AddBankAccountPage into AddPlaidBankAccount
marcaaron Jun 8, 2021
176b380
refactor plaid link stuff
marcaaron Jun 8, 2021
225ef6f
fix conflicts
marcaaron Jun 9, 2021
35e91bf
fix style
marcaaron Jun 9, 2021
a84e2fd
Refactor Plaid module so it can be easily used in the the two flows
marcaaron Jun 9, 2021
78ebe95
fix conflicts
marcaaron Jun 9, 2021
8ae125e
update import and clear plaid token on init
marcaaron Jun 9, 2021
ff91605
clean up beta logic
marcaaron Jun 9, 2021
84cfe10
use CheckboxWithLabel
marcaaron Jun 9, 2021
d9ed59a
Fix up checkbox usages
marcaaron Jun 9, 2021
6fbbf5f
update spanish
marcaaron Jun 9, 2021
5cdaec2
fix style
marcaaron Jun 9, 2021
d5d1d99
add checks for number
marcaaron Jun 9, 2021
f985e1a
Use same validation that Web-Secure is using for now
marcaaron Jun 9, 2021
5cfa72d
remove null checks
marcaaron Jun 9, 2021
857ac9a
translate
marcaaron Jun 9, 2021
7d4a9c8
use func for propTypes
marcaaron Jun 9, 2021
a098b66
add assets
marcaaron Jun 9, 2021
7e96346
fix checkbox text flow on iOS
marcaaron Jun 9, 2021
ac39938
remove returnKeyType
marcaaron Jun 9, 2021
45b86d0
fix up misaligned lock icon
marcaaron Jun 10, 2021
8a19a67
rename check image. fix up text link and consts
marcaaron Jun 10, 2021
d04ed46
do not pass array to TextLink
marcaaron Jun 10, 2021
108674f
use higher quality image
marcaaron Jun 10, 2021
6b3d5f5
make requested changes
marcaaron Jun 10, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
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),
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
}),

/** 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,
roryabraham marked this conversation as resolved.
Show resolved Hide resolved

/** 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();
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* 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({
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
account, password: this.state.password, plaidLinkToken: this.props.plaidLinkToken,
});
this.setState({isCreatingAccount: true});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not entirely sure about this, but it looks like there's a button down at the bottom of this file that calls this function onPress, and has isLoading={this.state.isCreatingAccount}. I'm not entirely sure how this screen gets hidden, but I assume that it happens as a result of onSubmit. So I guess I'm wondering if we should switch this line and the previous one so that we set the loading state first, then perform the onSubmit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it's mostly just to show the loading spinner. Probably can be removed or improved in the future. This flow is a little half baked at the moment. I think it will need to be adjusted based on where this all goes next so I'm not super worried about it yet.

}

render() {
const accounts = this.getAccounts();
const options = _.chain(accounts)
.filter(account => !account.alreadyExists)
.map((account, index) => ({
value: index, label: `${account.addressName} ${account.accountNumber}`,
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
}))
.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}`);
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
}}

// 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) && (
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
<Text style={[styles.mb5]}>{this.props.text}</Text>
)}
{/* @TODO there are a bunch of logos to incorporate here to replace this name
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created an issue here to follow up on this eventually https://github.com/Expensify/Expensify/issues/166782

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()}
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB, Should be moved to separate lines?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure we enforce this so if eslint doesn't mind I don't mind 😄

}) => {
const defaultStyles = [styles.flexRow];
const wrapperStyles = _.isArray(style) ? [...defaultStyles, ...style] : [...defaultStyles, style];
roryabraham marked this conversation as resolved.
Show resolved Hide resolved
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;
marcaaron marked this conversation as resolved.
Show resolved Hide resolved
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>}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NAB: Why are we making props.label optional? Seems strange to make a label optional on a component called TextInputWithLabel. I also don't see you using the errorText prop anywhere where you're using TextInputWithLabel without the label prop, so I'm wondering if this change is necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorta answered this in the other comment. But this component is likely going to change in the future when we apply some standardization to TextInputs across the app as it's kind of a mess. But we gotta keep rolling.

<TextInput
style={[styles.textInput, styles.mb1]}
// eslint-disable-next-line react/jsx-props-no-spreading
Expand Down
Loading