diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.js index 765ca641a3f4..a3405a4ad919 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.js @@ -8,7 +8,10 @@ import styles from '../styles/styles'; import Tooltip from './Tooltip'; import ONYXKEYS from '../ONYXKEYS'; import policyMemberPropType from '../pages/policyMemberPropType'; +import bankAccountPropTypes from './bankAccountPropTypes'; +import cardPropTypes from './cardPropTypes'; import * as Policy from '../libs/actions/Policy'; +import * as PaymentMethods from '../libs/actions/PaymentMethods'; const propTypes = { /** URL for the avatar */ @@ -22,12 +25,20 @@ const propTypes = { /** The employee list of all policies (coming from Onyx) */ policiesMemberList: PropTypes.objectOf(policyMemberPropType), + + /** List of bank accounts */ + bankAccountList: PropTypes.objectOf(bankAccountPropTypes), + + /** List of cards */ + cardList: PropTypes.objectOf(cardPropTypes), }; const defaultProps = { size: 'default', tooltipText: '', policiesMemberList: {}, + bankAccountList: {}, + cardList: {}, }; const AvatarWithIndicator = (props) => { @@ -39,6 +50,7 @@ const AvatarWithIndicator = (props) => { ]; const hasPolicyMemberError = _.some(props.policiesMemberList, policyMembers => Policy.hasPolicyMemberError(policyMembers)); + const hasPaymentMethodError = PaymentMethods.hasPaymentMethodError(props.bankAccountList, props.cardList); return ( @@ -47,7 +59,7 @@ const AvatarWithIndicator = (props) => { source={props.source} size={props.size} /> - {hasPolicyMemberError && ( + {(hasPolicyMemberError || hasPaymentMethodError) && ( )} @@ -63,4 +75,10 @@ export default withOnyx({ policiesMemberList: { key: ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST, }, + bankAccountList: { + key: ONYXKEYS.BANK_ACCOUNT_LIST, + }, + cardList: { + key: ONYXKEYS.CARD_LIST, + }, })(AvatarWithIndicator); diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index 3f95dff73517..a323893a1fc6 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -26,6 +26,7 @@ const defaultProps = { shouldShowRightIcon: false, shouldShowSelectedState: false, wrapperStyle: [], + style: {}, success: false, icon: undefined, iconWidth: undefined, @@ -45,117 +46,122 @@ const defaultProps = { brickRoadIndicator: '', }; -const MenuItem = props => ( - { - if (props.disabled) { - return; - } +const MenuItem = (props) => { + const titleTextStyle = StyleUtils.combineStyles([ + styles.popoverMenuText, + styles.ml3, + (props.interactive && props.disabled ? styles.disabledText : undefined), + ], props.style); + const descriptionTextStyle = StyleUtils.combineStyles([styles.textLabelSupporting, styles.ml3, styles.mt1, styles.breakAll], props.style); - if (e && e.type === 'click') { - e.currentTarget.blur(); - } + return ( + { + if (props.disabled) { + return; + } - props.onPress(e); - }} - style={({hovered, pressed}) => ([ - styles.popoverMenuItem, - StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || hovered, pressed, props.success, props.disabled, props.interactive)), - ..._.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle], - ])} - disabled={props.disabled} - > - {({hovered, pressed}) => ( - <> - - {(props.icon && props.iconType === CONST.ICON_TYPE_ICON) && ( - - - - )} - {(props.icon && props.iconType === CONST.ICON_TYPE_AVATAR) && ( - - - - )} - - - {props.title} - - {Boolean(props.description) && ( - ([ + styles.popoverMenuItem, + StyleUtils.getButtonBackgroundColorStyle(getButtonState(props.focused || hovered, pressed, props.success, props.disabled, props.interactive)), + ..._.isArray(props.wrapperStyle) ? props.wrapperStyle : [props.wrapperStyle], + ])} + disabled={props.disabled} + > + {({hovered, pressed}) => ( + <> + + {(props.icon && props.iconType === CONST.ICON_TYPE_ICON) && ( + - {props.description} - + + )} - - - - {props.badgeText && } - {/* Since subtitle can be of type number, we should allow 0 to be shown */} - {(props.subtitle || props.subtitle === 0) && ( - + {(props.icon && props.iconType === CONST.ICON_TYPE_AVATAR) && ( + + + + )} + - {props.subtitle} + {props.title} + {Boolean(props.description) && ( + + {props.description} + + )} - )} - {Boolean(props.brickRoadIndicator) && ( - - - - )} - {props.shouldShowRightIcon && ( - - - - )} - {props.shouldShowSelectedState && } - - - )} - -); + + + {props.badgeText && } + {/* Since subtitle can be of type number, we should allow 0 to be shown */} + {(props.subtitle || props.subtitle === 0) && ( + + + {props.subtitle} + + + )} + {Boolean(props.brickRoadIndicator) && ( + + + + )} + {Boolean(props.shouldShowRightIcon) && ( + + + + )} + {props.shouldShowSelectedState && } + + + )} + + ); +}; MenuItem.propTypes = propTypes; MenuItem.defaultProps = defaultProps; diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js index 9c2c5db58aa8..27dcefd9a15e 100644 --- a/src/components/OfflineWithFeedback.js +++ b/src/components/OfflineWithFeedback.js @@ -6,6 +6,7 @@ import compose from '../libs/compose'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; import {withNetwork} from './OnyxProvider'; import networkPropTypes from './networkPropTypes'; +import stylePropTypes from '../styles/stylePropTypes'; import Text from './Text'; import styles from '../styles/styles'; import Tooltip from './Tooltip'; @@ -38,14 +39,11 @@ const propTypes = { /** Information about the network */ network: networkPropTypes.isRequired, - /** Additional styles to add after local styles. Applied to Pressable portion of button */ - style: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.object), - PropTypes.object, - ]), + /** Additional styles to add after local styles. Applied to the parent container */ + style: stylePropTypes, /** Additional style object for the error row */ - errorRowStyles: PropTypes.arrayOf(PropTypes.object), + errorRowStyles: stylePropTypes, ...withLocalizePropTypes, }; @@ -102,7 +100,7 @@ const OfflineWithFeedback = (props) => { )} {hasErrors && ( - + diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js index fe343b608779..65f0dbf48d03 100644 --- a/src/components/menuItemPropTypes.js +++ b/src/components/menuItemPropTypes.js @@ -10,6 +10,9 @@ const propTypes = { // eslint-disable-next-line react/forbid-prop-types wrapperStyle: stylePropTypes, + /** Used to apply offline styles to child text components */ + style: stylePropTypes, + /** Function to fire when component is pressed */ onPress: PropTypes.func, diff --git a/src/libs/PaymentUtils.js b/src/libs/PaymentUtils.js index 1177c5b38e95..4089701ecd60 100644 --- a/src/libs/PaymentUtils.js +++ b/src/libs/PaymentUtils.js @@ -58,6 +58,8 @@ function formatPaymentMethods(bankAccountList, cardList, payPalMeUsername = '', accountType: CONST.PAYMENT_METHODS.BANK_ACCOUNT, accountData: _.extend({}, bankAccount, {icon}), isDefault, + errors: bankAccount.errors, + pendingAction: bankAccount.pendingAction, }); }); @@ -77,6 +79,8 @@ function formatPaymentMethods(bankAccountList, cardList, payPalMeUsername = '', accountType: CONST.PAYMENT_METHODS.DEBIT_CARD, accountData: _.extend({}, card, {icon}), isDefault, + errors: card.errors, + pendingAction: card.pendingAction, }); }); diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index c5eb62c61c15..46dc2a38a81f 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -1,6 +1,7 @@ import _ from 'underscore'; import {createRef} from 'react'; import lodashGet from 'lodash/get'; +import lodashMerge from 'lodash/merge'; import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; import * as DeprecatedAPI from '../deprecatedAPI'; @@ -276,6 +277,42 @@ function dismissSuccessfulTransferBalancePage() { Navigation.navigate(ROUTES.SETTINGS_PAYMENTS); } +/** + * Looks through each payment method to see if there is an existing error + * @param {Object} bankList + * @param {Object} cardList + * @returns {Boolean} + */ +function hasPaymentMethodError(bankList, cardList) { + const combinedPaymentMethods = lodashMerge(bankList, cardList); + return _.some(combinedPaymentMethods, item => !_.isEmpty(item.errors)); +} + +/** + * Clears the error for the specified payment item + * @param {String} paymentListKey The onyx key for the provided payment method + * @param {String} paymentMethodID + */ +function clearDeletePaymentMethodError(paymentListKey, paymentMethodID) { + Onyx.merge(paymentListKey, { + [paymentMethodID]: { + pendingAction: null, + errors: null, + }, + }); +} + +/** + * If there was a failure adding a payment method, clearing it removes the payment method from the list entirely + * @param {String} paymentListKey The onyx key for the provided payment method + * @param {String} paymentMethodID + */ +function clearAddPaymentMethodError(paymentListKey, paymentMethodID) { + Onyx.merge(paymentListKey, { + [paymentMethodID]: null, + }); +} + function deletePaymentCard(fundID) { API.write('DeletePaymentCard', { fundID, @@ -306,4 +343,7 @@ export { saveWalletTransferAccountTypeAndID, saveWalletTransferMethodType, cleanLocalReimbursementData, + hasPaymentMethodError, + clearDeletePaymentMethodError, + clearAddPaymentMethodError, }; diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 35b21c8d8894..e2d105f5d2e1 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -26,6 +26,9 @@ import * as App from '../../libs/actions/App'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../components/withCurrentUserPersonalDetails'; import * as Policy from '../../libs/actions/Policy'; import policyMemberPropType from '../policyMemberPropType'; +import * as PaymentMethods from '../../libs/actions/PaymentMethods'; +import bankAccountPropTypes from '../../components/bankAccountPropTypes'; +import cardPropTypes from '../../components/cardPropTypes'; const propTypes = { /* Onyx Props */ @@ -60,6 +63,12 @@ const propTypes = { currentBalance: PropTypes.number, }), + /** List of bank accounts */ + bankAccountList: PropTypes.objectOf(bankAccountPropTypes), + + /** List of cards */ + cardList: PropTypes.objectOf(cardPropTypes), + /** List of betas available to current user */ betas: PropTypes.arrayOf(PropTypes.string), @@ -78,45 +87,7 @@ const defaultProps = { ...withCurrentUserPersonalDetailsDefaultProps, }; -const defaultMenuItems = [ - { - translationKey: 'common.profile', - icon: Expensicons.Profile, - action: () => { App.openProfile(); }, - }, - { - translationKey: 'common.preferences', - icon: Expensicons.Gear, - action: () => { Navigation.navigate(ROUTES.SETTINGS_PREFERENCES); }, - }, - { - translationKey: 'initialSettingsPage.security', - icon: Expensicons.Lock, - action: () => { Navigation.navigate(ROUTES.SETTINGS_SECURITY); }, - }, - { - translationKey: 'common.payments', - icon: Expensicons.Wallet, - action: () => { Navigation.navigate(ROUTES.SETTINGS_PAYMENTS); }, - }, - { - translationKey: 'initialSettingsPage.about', - icon: Expensicons.Info, - action: () => { Navigation.navigate(ROUTES.SETTINGS_ABOUT); }, - }, - { - translationKey: 'initialSettingsPage.signOut', - icon: Expensicons.Exit, - action: Session.signOutAndRedirectToSignIn, - }, -]; - const InitialSettingsPage = (props) => { - const walletBalance = props.numberFormat( - props.userWallet.currentBalance / 100, // Divide by 100 because balance is in cents - {style: 'currency', currency: 'USD'}, - ); - // On the very first sign in or after clearing storage these // details will not be present on the first render so we'll just // return nothing for now. @@ -124,6 +95,45 @@ const InitialSettingsPage = (props) => { return null; } + const walletBalance = props.numberFormat( + props.userWallet.currentBalance / 100, // Divide by 100 because balance is in cents + {style: 'currency', currency: 'USD'}, + ); + + const defaultMenuItems = [ + { + translationKey: 'common.profile', + icon: Expensicons.Profile, + action: () => { App.openProfile(); }, + }, + { + translationKey: 'common.preferences', + icon: Expensicons.Gear, + action: () => { Navigation.navigate(ROUTES.SETTINGS_PREFERENCES); }, + }, + { + translationKey: 'initialSettingsPage.security', + icon: Expensicons.Lock, + action: () => { Navigation.navigate(ROUTES.SETTINGS_SECURITY); }, + }, + { + translationKey: 'common.payments', + icon: Expensicons.Wallet, + action: () => { Navigation.navigate(ROUTES.SETTINGS_PAYMENTS); }, + brickRoadIndicator: PaymentMethods.hasPaymentMethodError(props.bankAccountList, props.cardList) ? 'error' : null, + }, + { + translationKey: 'initialSettingsPage.about', + icon: Expensicons.Info, + action: () => { Navigation.navigate(ROUTES.SETTINGS_ABOUT); }, + }, + { + translationKey: 'initialSettingsPage.signOut', + icon: Expensicons.Exit, + action: Session.signOutAndRedirectToSignIn, + }, + ]; + // Add free policies (workspaces) to the list of menu items const menuItems = _.chain(props.policies) .filter(policy => policy && policy.type === CONST.POLICY.TYPE.FREE && policy.role === CONST.POLICY.ROLE.ADMIN) @@ -225,5 +235,11 @@ export default compose( betas: { key: ONYXKEYS.BETAS, }, + bankAccountList: { + key: ONYXKEYS.BANK_ACCOUNT_LIST, + }, + cardList: { + key: ONYXKEYS.CARD_LIST, + }, }), )(InitialSettingsPage); diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js index e2389bc07085..3e2cce3409c1 100644 --- a/src/pages/settings/Payments/PaymentMethodList.js +++ b/src/pages/settings/Payments/PaymentMethodList.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import React, {Component} from 'react'; import PropTypes from 'prop-types'; import {FlatList} from 'react-native'; +import lodashGet from 'lodash/get'; import {withOnyx} from 'react-native-onyx'; import styles from '../../../styles/styles'; import * as StyleUtils from '../../../styles/StyleUtils'; @@ -17,6 +18,9 @@ import bankAccountPropTypes from '../../../components/bankAccountPropTypes'; import cardPropTypes from '../../../components/cardPropTypes'; import * as PaymentUtils from '../../../libs/PaymentUtils'; import FormAlertWrapper from '../../../components/FormAlertWrapper'; +import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; +import * as PaymentMethods from '../../../libs/actions/PaymentMethods'; +import Log from '../../../libs/Log'; const MENU_ITEM = 'menuItem'; const BUTTON = 'button'; @@ -164,6 +168,26 @@ class PaymentMethodList extends Component { return combinedPaymentMethods; } + /** + * Dismisses the error on the payment method + * @param {Object} item + */ + dismissError(item) { + const paymentList = item.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.CARD_LIST; + const paymentID = item.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? lodashGet(item, ['accountData', 'bankAccountID'], '') : lodashGet(item, ['accountData', 'fundID'], ''); + + if (!paymentID) { + Log.info('Unable to clear payment method error: ', item); + return; + } + + if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + PaymentMethods.clearDeletePaymentMethodError(paymentList, paymentID); + } else { + PaymentMethods.clearAddPaymentMethodError(paymentList, paymentID); + } + } + /** * @param {Object} paymentMethod * @param {String|Number} paymentMethod.methodID @@ -185,20 +209,27 @@ class PaymentMethodList extends Component { renderItem({item}) { if (item.type === MENU_ITEM) { return ( - + this.dismissError(item)} + pendingAction={item.pendingAction} + errors={item.errors} + errorRowStyles={styles.ph6} + > + + ); } if (item.type === BUTTON) {