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) {