-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Add VBA flow Part 1 #3459
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
Changes from all commits
ecb9990
c128cfe
5db6177
176b380
225ef6f
35e91bf
a84e2fd
78ebe95
8ae125e
ff91605
84cfe10
d9ed59a
6fbbf5f
5cdaec2
d5d1d99
f985e1a
5cfa72d
857ac9a
7d4a9c8
a098b66
7e96346
ac39938
45b86d0
8a19a67
d04ed46
108674f
6b3d5f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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.
|
||
| }), | ||
|
|
||
| /** 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.
|
||
|
|
||
| /** 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.
|
||
| } | ||
|
|
||
| /** | ||
| * 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.
|
||
| account, password: this.state.password, plaidLinkToken: this.props.plaidLinkToken, | ||
| }); | ||
| this.setState({isCreatingAccount: true}); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
|
||
| })) | ||
| .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.
|
||
| }} | ||
|
|
||
| // 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.
|
||
| <Text style={[styles.mb5]}>{this.props.text}</Text> | ||
| )} | ||
| {/* @TODO there are a bunch of logos to incorporate here to replace this name | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
|
||
| 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); | ||
| 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, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB, Should be moved to separate lines?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
|
||
| 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.
|
||
| CheckboxWithLabel.displayName = 'CheckboxWithLabel'; | ||
|
|
||
| export default CheckboxWithLabel; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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>} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. NAB: Why are we making
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.