diff --git a/assets/images/bolt.svg b/assets/images/bolt.svg
new file mode 100644
index 000000000000..15e45f9b9bff
--- /dev/null
+++ b/assets/images/bolt.svg
@@ -0,0 +1,7 @@
+
+
+
diff --git a/assets/images/transfer.svg b/assets/images/transfer.svg
new file mode 100644
index 000000000000..03cb3c5f72b2
--- /dev/null
+++ b/assets/images/transfer.svg
@@ -0,0 +1,11 @@
+
+
+
diff --git a/src/CONST.js b/src/CONST.js
index 78e1ac329947..2a18124938cb 100755
--- a/src/CONST.js
+++ b/src/CONST.js
@@ -371,6 +371,11 @@ const CONST = {
},
WALLET: {
+ TRANSFER_BALANCE_FEE: 0.30,
+ TRANSFER_METHOD_TYPE: {
+ INSTANT: 'instant',
+ ACH: 'ach',
+ },
ERROR: {
IDENTITY_NOT_FOUND: 'Identity not found',
INVALID_SSN: 'Invalid SSN',
diff --git a/src/ONYXKEYS.js b/src/ONYXKEYS.js
index 928d3b3f5eb2..2f6057d2a475 100755
--- a/src/ONYXKEYS.js
+++ b/src/ONYXKEYS.js
@@ -127,6 +127,9 @@ export default {
// Stores information about the active reimbursement account being set up
REIMBURSEMENT_ACCOUNT: 'reimbursementAccount',
+ // Stores information about active wallet transfer amount, selectedAccountID, status, etc
+ WALLET_TRANSFER: 'walletTransfer',
+
// Stores draft information about the active reimbursement account being set up
REIMBURSEMENT_ACCOUNT_DRAFT: 'reimbursementAccountDraft',
diff --git a/src/ROUTES.js b/src/ROUTES.js
index 463ecdd1126f..90a52b80b6c4 100644
--- a/src/ROUTES.js
+++ b/src/ROUTES.js
@@ -28,6 +28,8 @@ export default {
SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links',
SETTINGS_PAYMENTS: 'settings/payments',
SETTINGS_ADD_PAYPAL_ME: 'settings/payments/add-paypal-me',
+ SETTINGS_TRANSFER_BALANCE: 'settings/payments/transfer-balance',
+ SETTINGS_PAYMENTS_CHOOSE_TRANSFER_ACCOUNT: 'settings/payments/choose-transfer-account',
SETTINGS_ADD_DEBIT_CARD: 'settings/payments/add-debit-card',
SETTINGS_ADD_BANK_ACCOUNT: 'settings/payments/add-bank-account',
SETTINGS_ADD_LOGIN: 'settings/addlogin/:type',
diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js
index 5e77e2fd47b8..b1059f1e600a 100644
--- a/src/components/AddPaymentMethodMenu.js
+++ b/src/components/AddPaymentMethodMenu.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import Popover from './Popover';
@@ -9,54 +9,166 @@ import styles from '../styles/styles';
import compose from '../libs/compose';
import ONYXKEYS from '../ONYXKEYS';
import CONST from '../CONST';
+import * as StyleUtils from '../styles/StyleUtils';
+import getClickedElementLocation from '../libs/getClickedElementLocation';
+import ROUTES from '../ROUTES';
+import Navigation from '../libs/Navigation/Navigation';
-const propTypes = {
- isVisible: PropTypes.bool.isRequired,
- onClose: PropTypes.func.isRequired,
- anchorPosition: PropTypes.shape({
- top: PropTypes.number,
- left: PropTypes.number,
- }),
+const propTypes = {
/** Username for PayPal.Me */
payPalMeUsername: PropTypes.string,
+ /** Type to filter the payment Method list */
+ filterType: PropTypes.oneOf([CONST.PAYMENT_METHODS.DEBIT_CARD, CONST.PAYMENT_METHODS.BANK_ACCOUNT, '']),
+
+ /** Are we loading payments from the server? */
+ isLoadingPayments: PropTypes.bool,
+
...withLocalizePropTypes,
};
const defaultProps = {
- anchorPosition: {},
payPalMeUsername: '',
+ filterType: '',
+ isLoadingPayments: false,
};
-const AddPaymentMethodMenu = props => (
-
-
-);
+class AddPaymentMethodMenu extends Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ isAddPaymentMenuActive: false,
+ anchorPosition: {
+ top: 0,
+ left: 0,
+ },
+ };
+ this.addPaymentMethodPressed = this.addPaymentMethodPressed.bind(this);
+ this.addPaymentMethodTypePressed = this.addPaymentMethodTypePressed.bind(this);
+ this.hideAddPaymentMenu = this.hideAddPaymentMenu.bind(this);
+ }
+
+ /**
+ * Get the AddPaymentMethod Button title
+ * @returns {String}
+ */
+ getAddPaymentMethodButtonTitle() {
+ switch (this.props.filterType) {
+ case CONST.PAYMENT_METHODS.BANK_ACCOUNT:
+ return this.props.translate('paymentMethodList.addBankAccount');
+ case CONST.PAYMENT_METHODS.DEBIT_CARD:
+ return this.props.translate('paymentMethodList.addDebitCard');
+ default: break;
+ }
+ return this.props.translate('paymentMethodList.addPaymentMethod');
+ }
+
+ /**
+ * Display the add payment method menu
+ * @param {Object} nativeEvent
+ */
+ addPaymentMethodPressed(nativeEvent) {
+ const position = getClickedElementLocation(nativeEvent);
+ this.setState({
+ isAddPaymentMenuActive: true,
+ anchorPosition: {
+ top: position.bottom,
+
+ // We want the position to be 20px to the right of the left border
+ left: position.left + 20,
+ },
+ });
+ }
+
+ /**
+ * Navigate to the appropriate payment type addition screen
+ * @param {String} paymentType
+ */
+ addPaymentMethodTypePressed(paymentType) {
+ if (!this.props.filterType) {
+ this.hideAddPaymentMenu();
+ }
+
+ if (paymentType === CONST.PAYMENT_METHODS.PAYPAL) {
+ Navigation.navigate(ROUTES.SETTINGS_ADD_PAYPAL_ME);
+ return;
+ }
+
+ if (paymentType === CONST.PAYMENT_METHODS.DEBIT_CARD) {
+ Navigation.navigate(ROUTES.SETTINGS_ADD_DEBIT_CARD);
+ return;
+ }
+
+ if (paymentType === CONST.PAYMENT_METHODS.BANK_ACCOUNT) {
+ Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT);
+ return;
+ }
+
+ throw new Error('Invalid payment method type selected');
+ }
+
+ /**
+ * Hide the add payment modal
+ */
+ hideAddPaymentMenu() {
+ this.setState({isAddPaymentMenuActive: false});
+ }
+
+ render() {
+ const addPaymentMethodButtonTitle = this.getAddPaymentMethodButtonTitle();
+
+ return (
+ <>
+ (this.props.filterType
+ ? this.addPaymentMethodTypePressed(this.props.filterType)
+ : this.addPaymentMethodPressed(e)
+ )}
+ title={addPaymentMethodButtonTitle}
+ icon={Expensicons.Plus}
+ disabled={this.props.isLoadingPayments}
+ iconFill={this.state.isAddPaymentMenuActive
+ ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.PRESSED)
+ : undefined}
+ wrapperStyle={this.state.isAddPaymentMenuActive
+ ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)]
+ : []}
+
+ />
+ {!this.props.filterType && (
+
+ this.addPaymentMethodTypePressed(CONST.PAYMENT_METHODS.BANK_ACCOUNT)}
+ wrapperStyle={styles.pr15}
+ />
+ this.addPaymentMethodTypePressed(CONST.PAYMENT_METHODS.DEBIT_CARD)}
+ wrapperStyle={styles.pr15}
+ />
+ {!this.props.payPalMeUsername && (
+ this.addPaymentMethodTypePressed(CONST.PAYMENT_METHODS.PAYPAL)}
+ wrapperStyle={styles.pr15}
+ />
+ )}
+
+ )}
+ >
+ );
+ }
+}
AddPaymentMethodMenu.propTypes = propTypes;
AddPaymentMethodMenu.defaultProps = defaultProps;
diff --git a/src/components/CurrentWalletBalance.js b/src/components/CurrentWalletBalance.js
index 9d4e18dfe4a9..a1c98581a8aa 100644
--- a/src/components/CurrentWalletBalance.js
+++ b/src/components/CurrentWalletBalance.js
@@ -17,11 +17,15 @@ const propTypes = {
currentBalance: PropTypes.number,
}),
+ /** Styles of the amount */
+ balanceStyles: PropTypes.arrayOf(PropTypes.object),
+
...withLocalizePropTypes,
};
const defaultProps = {
userWallet: {},
+ balanceStyles: [],
};
const CurrentWalletBalance = (props) => {
@@ -41,7 +45,7 @@ const CurrentWalletBalance = (props) => {
);
return (
{`${formattedBalance}`}
diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js
index d3f1f9be19bc..59cbf1db78b1 100644
--- a/src/components/Icon/Expensicons.js
+++ b/src/components/Icon/Expensicons.js
@@ -4,6 +4,7 @@ import ArrowRight from '../../../assets/images/arrow-right.svg';
import BackArrow from '../../../assets/images/back-left.svg';
import Bank from '../../../assets/images/bank.svg';
import Bill from '../../../assets/images/bill.svg';
+import Bolt from '../../../assets/images/bolt.svg';
import Briefcase from '../../../assets/images/briefcase.svg';
import Bug from '../../../assets/images/bug.svg';
import Building from '../../../assets/images/building.svg';
@@ -55,6 +56,7 @@ import RotateLeft from '../../../assets/images/rotate-left.svg';
import Send from '../../../assets/images/send.svg';
import SignOut from '../../../assets/images/sign-out.svg';
import Sync from '../../../assets/images/sync.svg';
+import Transfer from '../../../assets/images/transfer.svg';
import Trashcan from '../../../assets/images/trashcan.svg';
import UpArrow from '../../../assets/images/arrow-up.svg';
import Upload from '../../../assets/images/upload.svg';
@@ -71,6 +73,7 @@ export {
BackArrow,
Bank,
Bill,
+ Bolt,
Briefcase,
Building,
Bug,
@@ -122,6 +125,7 @@ export {
Send,
SignOut,
Sync,
+ Transfer,
Trashcan,
UpArrow,
Upload,
diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js
index 63d30771cbe5..fd76c91d1ca0 100644
--- a/src/components/MenuItem.js
+++ b/src/components/MenuItem.js
@@ -13,14 +13,12 @@ import Avatar from './Avatar';
import Badge from './Badge';
import CONST from '../CONST';
import menuItemPropTypes from './menuItemPropTypes';
-
-const propTypes = {
- ...menuItemPropTypes,
-};
+import SelectCircle from './SelectCircle';
const defaultProps = {
badgeText: undefined,
shouldShowRightIcon: false,
+ shouldShowSelectedState: false,
wrapperStyle: [],
success: false,
icon: undefined,
@@ -32,6 +30,7 @@ const defaultProps = {
iconFill: undefined,
focused: false,
disabled: false,
+ isSelected: false,
subtitle: undefined,
iconType: 'icon',
onPress: () => {},
@@ -124,13 +123,14 @@ const MenuItem = props => (
/>
)}
+ {props.shouldShowSelectedState && }
>
)}
);
-MenuItem.propTypes = propTypes;
+MenuItem.propTypes = menuItemPropTypes;
MenuItem.defaultProps = defaultProps;
MenuItem.displayName = 'MenuItem';
diff --git a/src/components/SelectCircle.js b/src/components/SelectCircle.js
new file mode 100644
index 000000000000..0d0a23dc8681
--- /dev/null
+++ b/src/components/SelectCircle.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import {View} from 'react-native';
+import PropTypes from 'prop-types';
+import styles from '../styles/styles';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
+import themeColors from '../styles/themes/default';
+
+const propTypes = {
+ /** Whether SelectCircle is checked */
+ isChecked: PropTypes.bool,
+};
+
+const defaultProps = {
+ isChecked: false,
+};
+
+const SelectCircle = props => (
+
+ {props.isChecked && (
+
+ )}
+
+);
+
+SelectCircle.propTypes = propTypes;
+SelectCircle.defaultProps = defaultProps;
+SelectCircle.displayName = 'SelectCircle';
+
+export default SelectCircle;
diff --git a/src/components/menuItemPropTypes.js b/src/components/menuItemPropTypes.js
index 8a00a360d910..9bf35a3ff1ef 100644
--- a/src/components/menuItemPropTypes.js
+++ b/src/components/menuItemPropTypes.js
@@ -57,6 +57,12 @@ const propTypes = {
/** Whether the menu item should be interactive at all */
interactive: PropTypes.bool,
+
+ /** Whether the menu item is selectable */
+ shouldShowSelectedState: PropTypes.bool,
+
+ /** Whether this item is selected */
+ isSelected: PropTypes.bool,
};
export default propTypes;
diff --git a/src/languages/en.js b/src/languages/en.js
index 587b77348748..833c2fed91ad 100755
--- a/src/languages/en.js
+++ b/src/languages/en.js
@@ -75,6 +75,7 @@ export default {
yesterdayAt: 'Yesterday at',
conjunctionAt: 'at',
genericErrorMessage: 'Oops... something went wrong and your request could not be completed. Please try again later.',
+ transferBalance: 'Transfer Balance',
error: {
invalidAmount: 'Invalid amount',
},
@@ -329,10 +330,28 @@ export default {
},
},
paymentsPage: {
- paymentMethodsTitle: 'Payment methods',
+ paymentMethodsTitle: 'Payment Methods',
+ allSet: 'All Set!',
+ transferConfirmText: ({amount}) => `${amount} will hit your account shortly!`,
+ gotIt: 'Got it, Thanks!',
+ },
+ transferAmountPage: {
+ transfer: ({amount}) => `Transfer${amount ? ` ${amount}` : ''}`,
+ instant: 'Instant (Debit Card)',
+ instantSummary: ({amount}) => `1.5% fee (${amount} minimum)`,
+ ach: '1-3 Business Days (Bank Account)',
+ achSummary: 'No fee',
+ whichAccount: 'Which Account?',
+ fee: 'Fee',
+ failedTransfer: 'Failed to transfer balance',
+ },
+ chooseTransferAccountPage: {
+ chooseAccount: 'Choose Account',
},
paymentMethodList: {
addPaymentMethod: 'Add payment method',
+ addDebitCard: 'Add debit card',
+ addBankAccount: 'Add bank account',
accountLastFour: 'Account ending in',
cardLastFour: 'Card ending in',
addFirstPaymentMethod: 'Add a payment method to send and receive payments directly in the app.',
diff --git a/src/languages/es.js b/src/languages/es.js
index 8e0e31931bf3..a21d5aea1286 100644
--- a/src/languages/es.js
+++ b/src/languages/es.js
@@ -75,6 +75,7 @@ export default {
yesterdayAt: 'Ayer a las',
conjunctionAt: 'a',
genericErrorMessage: 'Ups... algo no ha ido bien y la acción no se ha podido completar. Por favor inténtalo más tarde.',
+ transferBalance: 'Transferencia de saldo',
error: {
invalidAmount: 'Monto no válido',
},
@@ -330,9 +331,27 @@ export default {
},
paymentsPage: {
paymentMethodsTitle: 'Métodos de pago',
+ allSet: 'Todo listo!',
+ transferConfirmText: ({amount}) => `${amount} llegará a tu cuenta en breve!`,
+ gotIt: 'Gracias!',
+ },
+ transferAmountPage: {
+ transfer: ({amount}) => `Transferir${amount ? ` ${amount}` : ''}`,
+ instant: 'Instante',
+ instantSummary: ({amount}) => `Tarifa del 1.5% (${amount} mínimo)`,
+ ach: '1-3 días laborales',
+ achSummary: 'Sin cargo',
+ whichAccount: '¿Que cuenta?',
+ fee: 'Tarifa',
+ failedTransfer: 'No se pudo transferir el saldo',
+ },
+ chooseTransferAccountPage: {
+ chooseAccount: 'Elegir cuenta',
},
paymentMethodList: {
addPaymentMethod: 'Agrega método de pago',
+ addDebitCard: 'Agregar tarjeta de débito',
+ addBankAccount: 'Agregar cuenta de banco',
accountLastFour: 'Cuenta con terminación',
cardLastFour: 'Tarjeta con terminacíon',
addFirstPaymentMethod: 'Añade un método de pago para enviar y recibir pagos directamente desde la aplicación.',
diff --git a/src/libs/API.js b/src/libs/API.js
index 60ebf7334203..878cc8088ce4 100644
--- a/src/libs/API.js
+++ b/src/libs/API.js
@@ -1110,6 +1110,21 @@ function UpdatePolicy(parameters) {
return Network.post(commandName, parameters);
}
+/**
+ * Transfer Wallet balance and takes either the bankAccoundID or fundID
+ * @param {Object} parameters
+ * @param {String} [parameters.bankAccountID]
+ * @param {String} [parameters.fundID]
+ * @returns {Promise}
+ */
+function TransferWalletBalance(parameters) {
+ const commandName = 'TransferWalletBalance';
+ if (!parameters.bankAccountID && !parameters.fundID) {
+ throw new Error('Must pass either bankAccountID or fundID to TransferWalletBalance');
+ }
+ return Network.post(commandName, parameters);
+}
+
/**
* @param {Object} parameters
* @param {String} parameters.policyID
@@ -1185,5 +1200,6 @@ export {
Policy_Create,
Policy_Employees_Remove,
PreferredLocale_Update,
+ TransferWalletBalance,
Policy_Delete,
};
diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
index c8d5677ea301..cb0d81a3aced 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js
@@ -28,6 +28,8 @@ import WorkspaceInvitePage from '../../../pages/workspace/WorkspaceInvitePage';
import ReimbursementAccountPage from '../../../pages/ReimbursementAccount/ReimbursementAccountPage';
import RequestCallPage from '../../../pages/RequestCallPage';
import ReportDetailsPage from '../../../pages/ReportDetailsPage';
+import TransferBalancePage from '../../../pages/settings/Payments/TransferBalancePage';
+import ChooseTransferAccountPage from '../../../pages/settings/Payments/ChooseTransferAccountPage';
import WorkspaceSettingsPage from '../../../pages/workspace/WorkspaceSettingsPage';
import WorkspaceInitialPage from '../../../pages/workspace/WorkspaceInitialPage';
import WorkspaceCardPage from '../../../pages/workspace/card/WorkspaceCardPage';
@@ -174,6 +176,14 @@ const SettingsModalStackNavigator = createModalStackNavigator([
Component: SettingsPaymentsPage,
name: 'Settings_Payments',
},
+ {
+ Component: TransferBalancePage,
+ name: 'Settings_Transfer_Balance',
+ },
+ {
+ Component: ChooseTransferAccountPage,
+ name: 'Settings_Choose_Transfer_Account',
+ },
{
Component: SettingsAddPayPalMePage,
name: 'Settings_Add_Paypal_Me',
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index be838f7391c3..a903837e2ed8 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -48,6 +48,14 @@ export default {
path: ROUTES.SETTINGS_PAYMENTS,
exact: true,
},
+ Settings_Transfer_Balance: {
+ path: ROUTES.SETTINGS_TRANSFER_BALANCE,
+ exact: true,
+ },
+ Settings_Choose_Transfer_Account: {
+ path: ROUTES.SETTINGS_PAYMENTS_CHOOSE_TRANSFER_ACCOUNT,
+ exact: true,
+ },
Settings_Add_Paypal_Me: {
path: ROUTES.SETTINGS_ADD_PAYPAL_ME,
exact: true,
diff --git a/src/libs/PaymentUtils.js b/src/libs/PaymentUtils.js
new file mode 100644
index 000000000000..1188fe290ace
--- /dev/null
+++ b/src/libs/PaymentUtils.js
@@ -0,0 +1,102 @@
+import _ from 'underscore';
+import * as Expensicons from '../components/Icon/Expensicons';
+import getBankIcon from '../components/Icon/BankIcons';
+import CONST from '../CONST';
+import * as Localize from './Localize';
+
+/**
+ * PaymentMethod Type
+ * @typedef {Object} PaymentMethod
+ * @property {String} title
+ * @property {String} description
+ * @property {String} key
+ * @property {String} id
+ * @property {String} type
+ * @property {Number} [number] Bank or Card number
+ * @property {String} [bankName] Bank Name
+*/
+
+/**
+ * Get the PaymentMethods list
+ * @param {Array} bankAccountList
+ * @param {Array} cardList
+ * @param {String} [payPalMeUsername='']
+ * @returns {Array}
+ */
+function getPaymentMethods(bankAccountList, cardList, payPalMeUsername = '') {
+ const combinedPaymentMethods = [];
+
+ _.each(bankAccountList, (bankAccount) => {
+ // Add all bank accounts besides the wallet
+ if (bankAccount.type === CONST.BANK_ACCOUNT_TYPES.WALLET) {
+ return;
+ }
+
+ const formattedBankAccountNumber = bankAccount.accountNumber
+ ? `${Localize.translateLocal('paymentMethodList.accountLastFour')} ${bankAccount.accountNumber.slice(-4)
+ }`
+ : null;
+ combinedPaymentMethods.push({
+ title: bankAccount.addressName,
+ bankName: bankAccount.additionalData.bankName,
+ number: bankAccount.accountNumber,
+ id: bankAccount.bankAccountID,
+ description: formattedBankAccountNumber,
+ key: `bankAccount-${bankAccount.bankAccountID}`,
+ type: CONST.PAYMENT_METHODS.BANK_ACCOUNT,
+ });
+ });
+
+ _.each(cardList, (card) => {
+ // Add all cards besides the "cash" card
+ const formattedCardNumber = card.cardNumber
+ ? `${Localize.translateLocal('paymentMethodList.cardLastFour')} ${card.cardNumber.slice(-4)}`
+ : null;
+ combinedPaymentMethods.push({
+ title: card.addressName,
+ bankName: card.bank,
+ number: card.cardNumber,
+ id: card.fundID,
+ description: formattedCardNumber,
+ key: `card-${card.fundID}`,
+ type: CONST.PAYMENT_METHODS.DEBIT_CARD,
+ });
+ });
+
+ if (!_.isEmpty(payPalMeUsername)) {
+ combinedPaymentMethods.push({
+ title: 'PayPal.me',
+ description: payPalMeUsername,
+ key: 'payPalMePaymentMethod',
+ id: 'payPalMe',
+ type: CONST.PAYMENT_METHODS.PAYPAL,
+ });
+ }
+
+ return combinedPaymentMethods;
+}
+
+/**
+ * Get the Icon for PaymentMethod and its properties
+ * @param {PaymentMethod} paymentMethod
+ * @typedef {Object} IconProperties
+ * @property {?} IconProperties.icon
+ * @property {Number} [IconProperties.iconSize]
+ * @returns {IconProperties}
+ */
+function getPaymentMethodIconProperties(paymentMethod) {
+ switch (paymentMethod.type) {
+ case CONST.PAYMENT_METHODS.BANK_ACCOUNT:
+ return getBankIcon(paymentMethod.bankName);
+ case CONST.PAYMENT_METHODS.DEBIT_CARD:
+ return getBankIcon(paymentMethod.bankName, true);
+ case CONST.PAYMENT_METHODS.PAYPAL:
+ return {icon: Expensicons.PayPal};
+ default: break;
+ }
+}
+
+export default {
+ getPaymentMethods,
+ getPaymentMethodIconProperties,
+};
diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js
index 1d982f178700..8181bd7b5f00 100644
--- a/src/libs/actions/PaymentMethods.js
+++ b/src/libs/actions/PaymentMethods.js
@@ -3,8 +3,8 @@ import Onyx from 'react-native-onyx';
import ONYXKEYS from '../../ONYXKEYS';
import * as API from '../API';
import CONST from '../../CONST';
-import ROUTES from '../../ROUTES';
import Growl from '../Growl';
+import ROUTES from '../../ROUTES';
import * as Localize from '../Localize';
import Navigation from '../Navigation/Navigation';
import * as CardUtils from '../CardUtils';
@@ -35,6 +35,34 @@ function getPaymentMethods() {
});
}
+/**
+ * Call the API to transfer wallet balance.
+ * @param {Object} paymentMethod
+ * @param {String} paymentMethod.id
+ * @param {'bankAccount'|'debitCard'} paymentMethod.type
+ * @returns {Promise}
+ */
+function transferWalletBalance(paymentMethod) {
+ const parameters = {
+ [paymentMethod.type === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? 'bankAccountID' : 'fundID']: paymentMethod.id,
+ };
+ Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {loading: true});
+
+ return API.TransferWalletBalance(parameters)
+ .then((response) => {
+ if (response.jsonCode !== 200) {
+ throw new Error(response.message);
+ }
+ Onyx.merge(ONYXKEYS.USER_WALLET, {balance: 0});
+ Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {completed: true, loading: false});
+ Navigation.navigate(ROUTES.SETTINGS_PAYMENTS);
+ }).catch((error) => {
+ console.debug(`[Payments] Failed to transfer wallet balance: ${error.message}`);
+ Growl.error(Localize.translateLocal('transferAmountPage.failedTransfer'));
+ Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {loading: false});
+ });
+}
+
/**
* Calls the API to add a new card.
*
@@ -95,8 +123,42 @@ function clearDebitCardFormErrorAndSubmit() {
});
}
+/**
+ * Set the necessary data for wallet transfer
+ * @param {Number} currentBalance
+ * @param {Number} selectedAccountID
+ */
+function saveWalletTransferAmountAndAccount(currentBalance, selectedAccountID) {
+ Onyx.set(ONYXKEYS.WALLET_TRANSFER, {
+ transferAmount: currentBalance - CONST.WALLET.TRANSFER_BALANCE_FEE,
+ selectedAccountID,
+ filterPaymentMethodType: null,
+ loading: false,
+ completed: false,
+ });
+}
+
+/**
+ * Update selected accountID and other data for wallet transfer
+ * @param {Object} data
+ */
+function updateWalletTransferData(data) {
+ Onyx.merge(ONYXKEYS.WALLET_TRANSFER, data);
+}
+
+/**
+ * Cancel the wallet transfer
+ */
+function cancelWalletTransfer() {
+ Onyx.set(ONYXKEYS.WALLET_TRANSFER, null);
+}
+
export {
getPaymentMethods,
addBillingCard,
clearDebitCardFormErrorAndSubmit,
+ transferWalletBalance,
+ saveWalletTransferAmountAndAccount,
+ updateWalletTransferData,
+ cancelWalletTransfer,
};
diff --git a/src/pages/EnablePayments/userWalletPropTypes.js b/src/pages/EnablePayments/userWalletPropTypes.js
index 6c43089fa1de..4dc5ec85c67f 100644
--- a/src/pages/EnablePayments/userWalletPropTypes.js
+++ b/src/pages/EnablePayments/userWalletPropTypes.js
@@ -8,5 +8,12 @@ export default {
/** Status of wallet - e.g. SILVER or GOLD */
tierName: PropTypes.string,
+
+ /** Linked Bank account to the user wallet */
+ // eslint-disable-next-line react/forbid-prop-types
+ walletLinkedAccountID: PropTypes.number,
+
+ /** The user's current wallet balance */
+ availableBalance: PropTypes.number,
}),
};
diff --git a/src/pages/home/sidebar/OptionRow.js b/src/pages/home/sidebar/OptionRow.js
index 26cc11c936fa..32e7b2357e50 100644
--- a/src/pages/home/sidebar/OptionRow.js
+++ b/src/pages/home/sidebar/OptionRow.js
@@ -14,13 +14,13 @@ import {optionPropTypes} from './optionPropTypes';
import Icon from '../../../components/Icon';
import * as Expensicons from '../../../components/Icon/Expensicons';
import MultipleAvatars from '../../../components/MultipleAvatars';
-import themeColors from '../../../styles/themes/default';
import Hoverable from '../../../components/Hoverable';
import DisplayNames from '../../../components/DisplayNames';
import IOUBadge from '../../../components/IOUBadge';
import colors from '../../../styles/colors';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import ExpensifyText from '../../../components/ExpensifyText';
+import SelectCircle from '../../../components/SelectCircle';
const propTypes = {
/** Background Color of the Option Row */
@@ -203,13 +203,7 @@ const OptionRow = (props) => {
) : null}
- {props.showSelectedState && (
-
- {props.isSelected && (
-
- )}
-
- )}
+ {props.showSelectedState && }
{!props.hideAdditionalOptionStates && (
diff --git a/src/pages/settings/Payments/ChooseTransferAccountPage.js b/src/pages/settings/Payments/ChooseTransferAccountPage.js
new file mode 100644
index 000000000000..61c4d39c106b
--- /dev/null
+++ b/src/pages/settings/Payments/ChooseTransferAccountPage.js
@@ -0,0 +1,97 @@
+import React from 'react';
+import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import ROUTES from '../../../ROUTES';
+import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton';
+import ScreenWrapper from '../../../components/ScreenWrapper';
+import Navigation from '../../../libs/Navigation/Navigation';
+import styles from '../../../styles/styles';
+import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
+import KeyboardAvoidingView from '../../../components/KeyboardAvoidingView/index';
+import ONYXKEYS from '../../../ONYXKEYS';
+import compose from '../../../libs/compose';
+import * as paymentPropTypes from './paymentPropTypes';
+import * as PaymentMethods from '../../../libs/actions/PaymentMethods';
+import CONST from '../../../CONST';
+import AddPaymentMethodMenu from '../../../components/AddPaymentMethodMenu';
+import SelectablePaymentMethodList from './SelectablePaymentMethodList';
+
+const propTypes = {
+ /** Wallet transfer propTypes */
+ walletTransfer: paymentPropTypes.walletTransferPropTypes,
+
+ ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+ walletTransfer: {},
+};
+
+class ChooseTransferAccountPage extends React.Component {
+ constructor(props) {
+ super(props);
+ this.paymentMethodSelected = this.selectAccountAndNavigateBack.bind(this);
+ this.navigateToAddPaymentMethod = this.navigateToAddPaymentMethod.bind(this);
+ }
+
+ /**
+ * Navigate to the appropriate payment method type addition screen
+ */
+ navigateToAddPaymentMethod() {
+ if (this.props.walletTransfer.filterPaymentMethodType === CONST.PAYMENT_METHODS.PAYPAL) {
+ Navigation.navigate(ROUTES.SETTINGS_ADD_PAYPAL_ME);
+ }
+
+ if (this.props.walletTransfer.filterPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD) {
+ Navigation.navigate(ROUTES.SETTINGS_ADD_DEBIT_CARD);
+ }
+
+ if (this.props.walletTransfer.filterPaymentMethodType === CONST.PAYMENT_METHODS.BANK_ACCOUNT) {
+ Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT);
+ }
+ }
+
+ /**
+ * Go back to TransferPage with the selected bank account
+ * @param {String} accountID of the selected account.
+ */
+ selectAccountAndNavigateBack(accountID) {
+ PaymentMethods.updateWalletTransferData({selectedAccountID: accountID});
+ Navigation.navigate(ROUTES.SETTINGS_TRANSFER_BALANCE);
+ }
+
+ render() {
+ return (
+
+
+ Navigation.goBack()}
+ onCloseButtonPress={() => Navigation.dismissModal()}
+ />
+
+
+
+
+
+
+ );
+ }
+}
+
+ChooseTransferAccountPage.propTypes = propTypes;
+ChooseTransferAccountPage.defaultProps = defaultProps;
+
+export default compose(
+ withLocalize,
+ withOnyx({
+ walletTransfer: {
+ key: ONYXKEYS.WALLET_TRANSFER,
+ },
+ }),
+)(ChooseTransferAccountPage);
diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js
index db559f52d2a6..6d4251d3b38a 100644
--- a/src/pages/settings/Payments/PaymentMethodList.js
+++ b/src/pages/settings/Payments/PaymentMethodList.js
@@ -4,16 +4,12 @@ import PropTypes from 'prop-types';
// eslint-disable-next-line no-restricted-imports
import {FlatList, Text} from 'react-native';
import {withOnyx} from 'react-native-onyx';
-import lodashGet from 'lodash/get';
import styles from '../../../styles/styles';
-import * as StyleUtils from '../../../styles/StyleUtils';
import MenuItem from '../../../components/MenuItem';
import compose from '../../../libs/compose';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import ONYXKEYS from '../../../ONYXKEYS';
-import CONST from '../../../CONST';
-import * as Expensicons from '../../../components/Icon/Expensicons';
-import getBankIcon from '../../../components/Icon/BankIcons';
+import PaymentUtils from '../../../libs/PaymentUtils';
import bankAccountPropTypes from '../../../components/bankAccountPropTypes';
const MENU_ITEM = 'menuItem';
@@ -22,12 +18,6 @@ const propTypes = {
/** What to do when a menu item is pressed */
onPress: PropTypes.func.isRequired,
- /** Are we loading payments from the server? */
- isLoadingPayments: PropTypes.bool,
-
- /** Is the payment options menu open / active? */
- isAddPaymentMenuActive: PropTypes.bool,
-
/** User's paypal.me username if they have one */
payPalMeUsername: PropTypes.string,
@@ -53,97 +43,37 @@ const defaultProps = {
payPalMeUsername: '',
bankAccountList: [],
cardList: [],
- isLoadingPayments: false,
- isAddPaymentMenuActive: false,
};
class PaymentMethodList extends Component {
constructor(props) {
super(props);
-
this.renderItem = this.renderItem.bind(this);
+ this.createPaymentMethodList = this.createPaymentMethodList.bind(this);
}
/**
* Take all of the different payment methods and create a list that can be easily digested by renderItem
- *
* @returns {Array}
*/
createPaymentMethodList() {
- const combinedPaymentMethods = [];
-
- _.each(this.props.bankAccountList, (bankAccount) => {
- // Add all bank accounts besides the wallet
- if (bankAccount.type === CONST.BANK_ACCOUNT_TYPES.WALLET) {
- return;
- }
-
- const formattedBankAccountNumber = bankAccount.accountNumber
- ? `${this.props.translate('paymentMethodList.accountLastFour')} ${
- bankAccount.accountNumber.slice(-4)
- }`
- : null;
- const {icon, iconSize} = getBankIcon(lodashGet(bankAccount, 'additionalData.bankName', ''));
- combinedPaymentMethods.push({
- type: MENU_ITEM,
- title: bankAccount.addressName,
-
- // eslint-disable-next-line
- description: formattedBankAccountNumber,
- icon,
- iconSize,
- onPress: e => this.props.onPress(e, bankAccount.bankAccountID),
- key: `bankAccount-${bankAccount.bankAccountID}`,
- });
- });
-
- _.each(this.props.cardList, (card) => {
- const formattedCardNumber = card.cardNumber
- ? `${this.props.translate('paymentMethodList.cardLastFour')} ${card.cardNumber.slice(-4)}`
- : null;
- const {icon, iconSize} = getBankIcon(card.bank, true);
- combinedPaymentMethods.push({
- type: MENU_ITEM,
- title: card.addressName,
- // eslint-disable-next-line
- description: formattedCardNumber,
- icon,
- iconSize,
- onPress: e => this.props.onPress(e, card.cardNumber),
- key: `card-${card.cardNumber}`,
- });
- });
-
- if (this.props.payPalMeUsername) {
- combinedPaymentMethods.push({
- type: MENU_ITEM,
- title: 'PayPal.me',
- description: this.props.payPalMeUsername,
- icon: Expensicons.PayPal,
- onPress: e => this.props.onPress(e, 'payPalMe'),
- key: 'payPalMePaymentMethod',
- });
- }
+ let paymentMethods = PaymentUtils.getPaymentMethods(this.props.bankAccountList, this.props.cardList, this.props.payPalMeUsername);
+
+ paymentMethods = _.map(paymentMethods, method => ({
+ ...method,
+ ...PaymentUtils.getPaymentMethodIconProperties(method),
+ type: MENU_ITEM,
+ onPress: () => this.props.onPress(method.id),
+ }));
// If we have not added any payment methods, show a default empty state
- if (_.isEmpty(combinedPaymentMethods)) {
- combinedPaymentMethods.push({
+ if (_.isEmpty(paymentMethods)) {
+ paymentMethods.push({
text: this.props.translate('paymentMethodList.addFirstPaymentMethod'),
});
}
- combinedPaymentMethods.push({
- type: MENU_ITEM,
- title: this.props.translate('paymentMethodList.addPaymentMethod'),
- icon: Expensicons.Plus,
- onPress: e => this.props.onPress(e),
- key: 'addPaymentMethodButton',
- disabled: this.props.isLoadingPayments,
- iconFill: this.props.isAddPaymentMenuActive ? StyleUtils.getIconFillColor(CONST.BUTTON_STATES.PRESSED) : null,
- wrapperStyle: this.props.isAddPaymentMenuActive ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : [],
- });
-
- return combinedPaymentMethods;
+ return paymentMethods;
}
/**
@@ -152,7 +82,7 @@ class PaymentMethodList extends Component {
* @param {Object} params
* @param {Object} params.item
*
- * @return {React.Component}
+ * @returns {React.Component}
*/
renderItem({item}) {
if (item.type === MENU_ITEM) {
diff --git a/src/pages/settings/Payments/PaymentsPage.js b/src/pages/settings/Payments/PaymentsPage.js
index a49b2ada357f..dbb82c27cb56 100644
--- a/src/pages/settings/Payments/PaymentsPage.js
+++ b/src/pages/settings/Payments/PaymentsPage.js
@@ -1,6 +1,6 @@
import React from 'react';
-import {ScrollView} from 'react-native';
import {withOnyx} from 'react-native-onyx';
+import {ScrollView, View} from 'react-native';
import PropTypes from 'prop-types';
import PaymentMethodList from './PaymentMethodList';
import ROUTES from '../../../ROUTES';
@@ -13,24 +13,33 @@ import compose from '../../../libs/compose';
import KeyboardAvoidingView from '../../../components/KeyboardAvoidingView/index';
import ExpensifyText from '../../../components/ExpensifyText';
import * as PaymentMethods from '../../../libs/actions/PaymentMethods';
-import getClickedElementLocation from '../../../libs/getClickedElementLocation';
import CurrentWalletBalance from '../../../components/CurrentWalletBalance';
import ONYXKEYS from '../../../ONYXKEYS';
+import * as paymentPropTypes from './paymentPropTypes';
import Permissions from '../../../libs/Permissions';
import AddPaymentMethodMenu from '../../../components/AddPaymentMethodMenu';
import CONST from '../../../CONST';
+import * as Expensicons from '../../../components/Icon/Expensicons';
+import MenuItem from '../../../components/MenuItem';
+import ConfirmModal from '../../../components/ConfirmModal';
const propTypes = {
- ...withLocalizePropTypes,
+ /** Wallet balance transfer props */
+ walletTransfer: paymentPropTypes.walletTransferPropTypes,
/** List of betas available to current user */
betas: PropTypes.arrayOf(PropTypes.string),
/** Are we loading payment methods? */
isLoadingPaymentMethods: PropTypes.bool,
+
+ ...withLocalizePropTypes,
};
const defaultProps = {
+ walletTransfer: {
+ completed: false,
+ },
betas: [],
isLoadingPaymentMethods: true,
};
@@ -39,15 +48,8 @@ class PaymentsPage extends React.Component {
constructor(props) {
super(props);
- this.state = {
- shouldShowAddPaymentMenu: false,
- anchorPositionTop: 0,
- anchorPositionLeft: 0,
- };
-
this.paymentMethodPressed = this.paymentMethodPressed.bind(this);
- this.addPaymentMethodTypePressed = this.addPaymentMethodTypePressed.bind(this);
- this.hideAddPaymentMenu = this.hideAddPaymentMenu.bind(this);
+ this.transferBalance = this.transferBalance.bind(this);
}
componentDidMount() {
@@ -55,59 +57,22 @@ class PaymentsPage extends React.Component {
}
/**
- * Display the delete/default menu, or the add payment method menu
+ * Navigate to respective payment page when existing payment method is pressed
*
- * @param {Object} nativeEvent
* @param {String} account
*/
- paymentMethodPressed(nativeEvent, account) {
- if (account) {
- if (account === CONST.PAYMENT_METHODS.PAYPAL) {
- Navigation.navigate(ROUTES.SETTINGS_ADD_PAYPAL_ME);
- }
- } else {
- const position = getClickedElementLocation(nativeEvent);
- this.setState({
- shouldShowAddPaymentMenu: true,
- anchorPositionTop: position.bottom,
-
- // We want the position to be 20px to the right of the left border
- anchorPositionLeft: position.left + 20,
- });
- }
- }
-
- /**
- * Navigate to the appropriate payment type addition screen
- *
- * @param {String} paymentType
- */
- addPaymentMethodTypePressed(paymentType) {
- this.hideAddPaymentMenu();
-
- if (paymentType === CONST.PAYMENT_METHODS.PAYPAL) {
- Navigation.navigate(ROUTES.SETTINGS_ADD_PAYPAL_ME);
- return;
- }
-
- if (paymentType === CONST.PAYMENT_METHODS.DEBIT_CARD) {
- Navigation.navigate(ROUTES.SETTINGS_ADD_DEBIT_CARD);
+ paymentMethodPressed(account) {
+ if (account !== CONST.PAYMENT_METHODS.PAYPAL) {
return;
}
-
- if (paymentType === CONST.PAYMENT_METHODS.BANK_ACCOUNT) {
- Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT);
- return;
- }
-
- throw new Error('Invalid payment method type selected');
+ Navigation.navigate(ROUTES.SETTINGS_ADD_PAYPAL_ME);
}
/**
- * Hide the add payment modal
+ * Transfer wallet balance
*/
- hideAddPaymentMenu() {
- this.setState({shouldShowAddPaymentMenu: false});
+ transferBalance() {
+ Navigation.navigate(ROUTES.SETTINGS_TRANSFER_BALANCE);
}
render() {
@@ -121,29 +86,42 @@ class PaymentsPage extends React.Component {
onCloseButtonPress={() => Navigation.dismissModal(true)}
/>
- {
- Permissions.canUseWallet(this.props.betas) &&
- }
+ {Permissions.canUseWallet(this.props.betas) && (
+ <>
+
+
+
+
+ >
+ )}
{this.props.translate('paymentsPage.paymentMethodsTitle')}
+
- this.addPaymentMethodTypePressed(method)}
+
@@ -157,6 +135,9 @@ PaymentsPage.defaultProps = defaultProps;
export default compose(
withLocalize,
withOnyx({
+ walletTransfer: {
+ key: ONYXKEYS.WALLET_TRANSFER,
+ },
betas: {
key: ONYXKEYS.BETAS,
},
diff --git a/src/pages/settings/Payments/SelectablePaymentMethodList.js b/src/pages/settings/Payments/SelectablePaymentMethodList.js
new file mode 100644
index 000000000000..6d035a625b34
--- /dev/null
+++ b/src/pages/settings/Payments/SelectablePaymentMethodList.js
@@ -0,0 +1,155 @@
+import _ from 'underscore';
+import React, {Component} from 'react';
+import PropTypes from 'prop-types';
+// eslint-disable-next-line no-restricted-imports
+import {FlatList, Text} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import styles from '../../../styles/styles';
+import MenuItem from '../../../components/MenuItem';
+import compose from '../../../libs/compose';
+import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
+import ONYXKEYS from '../../../ONYXKEYS';
+import CONST from '../../../CONST';
+import PaymentUtils from '../../../libs/PaymentUtils';
+import bankAccountPropTypes from '../../../components/bankAccountPropTypes';
+
+const MENU_ITEM = 'menuItem';
+
+const propTypes = {
+ /** What to do when a menu item is pressed */
+ onPress: PropTypes.func.isRequired,
+
+ /** User's paypal.me username if they have one */
+ payPalMeUsername: PropTypes.string,
+
+ /** Type to filter the payment Method list */
+ filterType: PropTypes.oneOf([CONST.PAYMENT_METHODS.DEBIT_CARD, CONST.PAYMENT_METHODS.BANK_ACCOUNT, '']),
+
+ /** Selected Account ID if selection is active */
+ selectedAccountID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+
+ /** Array of bank account objects */
+ bankAccountList: PropTypes.arrayOf(bankAccountPropTypes),
+
+ /** Array of card objects */
+ cardList: PropTypes.arrayOf(PropTypes.shape({
+ /** The name of the institution (bank of america, etc */
+ cardName: PropTypes.string,
+
+ /** The masked credit card number */
+ cardNumber: PropTypes.string,
+
+ /** The ID of the card in the cards DB */
+ cardID: PropTypes.number,
+ })),
+
+ ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+ payPalMeUsername: '',
+ bankAccountList: [],
+ cardList: [],
+ selectedAccountID: '',
+ filterType: '',
+};
+
+class SelectablePaymentMethodList extends Component {
+ constructor(props) {
+ super(props);
+ this.renderItem = this.renderItem.bind(this);
+ this.createPaymentMethodList = this.createPaymentMethodList.bind(this);
+ }
+
+ /**
+ * Take all of the different payment methods and create a list that can be easily digested by renderItem
+ * @returns {Array}
+ */
+ createPaymentMethodList() {
+ let paymentMethods = PaymentUtils.getPaymentMethods(this.props.bankAccountList, this.props.cardList, this.props.payPalMeUsername);
+
+ if (!_.isEmpty(this.props.filterType)) {
+ paymentMethods = _.filter(paymentMethods, paymentMethod => paymentMethod.type === this.props.filterType);
+ }
+
+ paymentMethods = _.map(paymentMethods, method => ({
+ ...method,
+ ...PaymentUtils.getPaymentMethodIconProperties(method),
+ type: MENU_ITEM,
+ onPress: () => this.props.onPress(method.id),
+ }));
+
+ // If we have not added any payment methods, show a default empty state
+ if (_.isEmpty(paymentMethods)) {
+ paymentMethods.push({
+ text: this.props.translate('paymentMethodList.addFirstPaymentMethod'),
+ });
+ }
+
+ return paymentMethods;
+ }
+
+ /**
+ * Create a menuItem for each passed paymentMethod
+ *
+ * @param {Object} params
+ * @param {Object} params.item
+ *
+ * @returns {React.Component}
+ */
+ renderItem({item}) {
+ if (item.type === MENU_ITEM) {
+ return (
+
+ );
+ }
+
+ return (
+
+ {item.text}
+
+ );
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+SelectablePaymentMethodList.propTypes = propTypes;
+SelectablePaymentMethodList.defaultProps = defaultProps;
+
+export default compose(
+ withLocalize,
+ withOnyx({
+ bankAccountList: {
+ key: ONYXKEYS.BANK_ACCOUNT_LIST,
+ },
+ cardList: {
+ key: ONYXKEYS.CARD_LIST,
+ },
+ payPalMeUsername: {
+ key: ONYXKEYS.NVP_PAYPAL_ME_ADDRESS,
+ },
+ }),
+)(SelectablePaymentMethodList);
diff --git a/src/pages/settings/Payments/TransferBalancePage.js b/src/pages/settings/Payments/TransferBalancePage.js
new file mode 100644
index 000000000000..d66f0a138d06
--- /dev/null
+++ b/src/pages/settings/Payments/TransferBalancePage.js
@@ -0,0 +1,264 @@
+import _ from 'underscore';
+import React from 'react';
+import {View} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import {ScrollView} from 'react-native-gesture-handler';
+import lodashGet from 'lodash/get';
+import ONYXKEYS from '../../../ONYXKEYS';
+import ROUTES from '../../../ROUTES';
+import HeaderWithCloseButton from '../../../components/HeaderWithCloseButton';
+import ScreenWrapper from '../../../components/ScreenWrapper';
+import Navigation from '../../../libs/Navigation/Navigation';
+import styles from '../../../styles/styles';
+import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
+import compose from '../../../libs/compose';
+import KeyboardAvoidingView from '../../../components/KeyboardAvoidingView/index';
+import * as Expensicons from '../../../components/Icon/Expensicons';
+import MenuItem from '../../../components/MenuItem';
+import CONST from '../../../CONST';
+import variables from '../../../styles/variables';
+import ExpensifyText from '../../../components/ExpensifyText';
+import ExpensifyButton from '../../../components/ExpensifyButton';
+import FixedFooter from '../../../components/FixedFooter';
+import CurrentWalletBalance from '../../../components/CurrentWalletBalance';
+import * as paymentPropTypes from './paymentPropTypes';
+import * as PaymentMethods from '../../../libs/actions/PaymentMethods';
+import PaymentUtils from '../../../libs/PaymentUtils';
+import userWalletPropTypes from '../../EnablePayments/userWalletPropTypes';
+
+const propTypes = {
+ /** User's wallet information */
+ userWallet: userWalletPropTypes.userWallet,
+
+ /** Array of bank account objects */
+ bankAccountList: paymentPropTypes.bankAccountListPropTypes,
+
+ /** Array of card objects */
+ cardList: paymentPropTypes.cardListPropTypes,
+
+ /** Wallet balance transfer props */
+ walletTransfer: paymentPropTypes.walletTransferPropTypes,
+
+ ...withLocalizePropTypes,
+};
+
+const defaultProps = {
+ userWallet: {},
+ bankAccountList: [],
+ cardList: [],
+ walletTransfer: {},
+};
+
+class TransferBalancePage extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.paymentTypes = [
+ {
+ key: CONST.WALLET.TRANSFER_METHOD_TYPE.INSTANT,
+ title: this.props.translate('transferAmountPage.instant'),
+ description: this.props.translate('transferAmountPage.instantSummary', {
+ amount: this.props.numberFormat(
+ 0.25,
+ {style: 'currency', currency: 'USD'},
+ ),
+ }),
+ icon: Expensicons.Bolt,
+ type: CONST.PAYMENT_METHODS.DEBIT_CARD,
+ },
+ {
+ key: CONST.WALLET.TRANSFER_METHOD_TYPE.ACH,
+ title: this.props.translate('transferAmountPage.ach'),
+ description: this.props.translate('transferAmountPage.achSummary'),
+ icon: Expensicons.Bank,
+ type: CONST.PAYMENT_METHODS.BANK_ACCOUNT,
+ },
+ ];
+
+ this.transferBalance = this.transferBalance.bind(this);
+ this.navigateToSelectAccount = this.navigateToChooseTransferAccount.bind(this);
+
+ const selectedAccount = this.getSelectedAccount();
+ PaymentMethods.saveWalletTransferAmountAndAccount(
+ this.props.userWallet.currentBalance,
+ selectedAccount ? selectedAccount.id : '',
+ );
+ }
+
+ /**
+ * Get the selected/default Account for wallet tsransfer
+ * @returns {Object|undefined}
+ */
+ getSelectedAccount() {
+ const paymentMethods = PaymentUtils.getPaymentMethods(
+ this.props.bankAccountList,
+ this.props.cardList,
+ );
+
+ const defaultAccount = _.find(
+ paymentMethods,
+ method => method.id === lodashGet(this.props, 'userWallet.walletLinkedAccountID', ''),
+ );
+ const selectedAccount = this.props.walletTransfer.selectedAccountID
+ ? _.find(
+ paymentMethods,
+ method => method.id === this.props.walletTransfer.selectedAccountID,
+ )
+ : defaultAccount;
+
+ if (selectedAccount) {
+ const iconProperties = PaymentUtils.getPaymentMethodIconProperties(selectedAccount);
+ selectedAccount.icon = iconProperties.icon;
+ selectedAccount.iconSize = iconProperties.iconSize;
+ }
+
+ return selectedAccount;
+ }
+
+ /**
+ * Navigate to SETTINGS_PAYMENTS_CHOOSE_TRANSFER_ACCOUNT screen for selecting an account.
+ * @param {String} filterPaymentMethodType
+ */
+ navigateToChooseTransferAccount(filterPaymentMethodType) {
+ PaymentMethods.updateWalletTransferData({filterPaymentMethodType});
+ Navigation.navigate(ROUTES.SETTINGS_PAYMENTS_CHOOSE_TRANSFER_ACCOUNT);
+ }
+
+ /**
+ * Transfer Wallet balance
+ * @param {PaymentMethod} selectedAccount
+ */
+ transferBalance(selectedAccount) {
+ if (!selectedAccount) {
+ return;
+ }
+ PaymentMethods.transferWalletBalance(selectedAccount);
+ }
+
+ render() {
+ const selectedAccount = this.getSelectedAccount();
+ const selectedPaymentType = selectedAccount && selectedAccount.type === CONST.PAYMENT_METHODS.BANK_ACCOUNT
+ ? CONST.WALLET.TRANSFER_METHOD_TYPE.ACH
+ : CONST.WALLET.TRANSFER_METHOD_TYPE.INSTANT;
+ const transferAmount = this.props.walletTransfer.transferAmount.toFixed(2);
+ const canTransfer = transferAmount > CONST.WALLET.TRANSFER_BALANCE_FEE;
+ const isButtonDisabled = !canTransfer || !selectedAccount;
+
+ return (
+
+
+ Navigation.goBack()}
+ onCloseButtonPress={() => Navigation.dismissModal(true)}
+ />
+
+
+
+
+ {_.map(this.paymentTypes, paymentType => (
+ this.navigateToChooseTransferAccount(paymentType.type)}
+ />
+ ))}
+
+ {this.props.translate('transferAmountPage.whichAccount')}
+
+ {Boolean(selectedAccount)
+ && (
+ this.navigateToChooseTransferAccount(selectedAccount.type)}
+ />
+ )}
+
+ {this.props.translate('transferAmountPage.fee')}
+
+
+ {this.props.numberFormat(
+ CONST.WALLET.TRANSFER_BALANCE_FEE,
+ {style: 'currency', currency: 'USD'},
+ )}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+TransferBalancePage.propTypes = propTypes;
+TransferBalancePage.defaultProps = defaultProps;
+
+export default compose(
+ withLocalize,
+ withOnyx({
+ userWallet: {
+ key: ONYXKEYS.USER_WALLET,
+ },
+ walletTransfer: {
+ key: ONYXKEYS.WALLET_TRANSFER,
+ },
+ bankAccountList: {
+ key: ONYXKEYS.BANK_ACCOUNT_LIST,
+ },
+ cardList: {
+ key: ONYXKEYS.CARD_LIST,
+ },
+ }),
+)(TransferBalancePage);
diff --git a/src/pages/settings/Payments/paymentPropTypes.js b/src/pages/settings/Payments/paymentPropTypes.js
new file mode 100644
index 000000000000..917045d80bd8
--- /dev/null
+++ b/src/pages/settings/Payments/paymentPropTypes.js
@@ -0,0 +1,50 @@
+import PropTypes from 'prop-types';
+import CONST from '../../../CONST';
+
+/** Array of bank account objects */
+const bankAccountListPropTypes = PropTypes.arrayOf(PropTypes.shape({
+ /** The name of the institution (bank of america, etc) */
+ addressName: PropTypes.string,
+
+ /** The masked bank account number */
+ accountNumber: PropTypes.string,
+
+ /** The bankAccountID in the bankAccounts db */
+ bankAccountID: PropTypes.number,
+
+ /** The bank account type */
+ type: PropTypes.string,
+}));
+
+/** Array of card objects */
+const cardListPropTypes = PropTypes.arrayOf(PropTypes.shape({
+ /** The name of the institution (bank of america, etc) */
+ cardName: PropTypes.string,
+
+ /** The masked credit card number */
+ cardNumber: PropTypes.string,
+
+ /** The ID of the card in the cards DB */
+ cardID: PropTypes.number,
+}));
+
+/** Wallet balance transfer props */
+const walletTransferPropTypes = PropTypes.shape({
+ /** Amount being transferred */
+ transferAmount: PropTypes.number,
+
+ /** Selected accountID for transfer */
+ selectedAccountID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
+
+ /** Type to filter the payment Method list */
+ filterPaymentMethodType: PropTypes.oneOf([CONST.PAYMENT_METHODS.DEBIT_CARD, CONST.PAYMENT_METHODS.BANK_ACCOUNT, '']),
+
+ /** Whether the user has intiatied the tranfer and transfer request is submitted to backend. */
+ completed: PropTypes.bool,
+});
+
+export {
+ bankAccountListPropTypes,
+ cardListPropTypes,
+ walletTransferPropTypes,
+};
diff --git a/src/styles/styles.js b/src/styles/styles.js
index c0ec2df5f495..27c3193c22f1 100644
--- a/src/styles/styles.js
+++ b/src/styles/styles.js
@@ -2205,6 +2205,16 @@ const styles = {
height: 20,
},
+ transferBalancePayment: {
+ borderWidth: 2,
+ borderRadius: variables.componentBorderRadiusNormal,
+ borderColor: themeColors.border,
+ },
+
+ transferBalanceSelectedPayment: {
+ borderColor: themeColors.iconSuccessFill,
+ },
+
reportMarkerBadgeWrapper: {
position: 'absolute',
left: '50%',
@@ -2316,6 +2326,10 @@ const styles = {
backgroundColor: colors.black,
flex: 1,
},
+
+ transferBalanceBalance: {
+ fontSize: 48,
+ },
};
export default styles;
diff --git a/src/styles/utilities/spacing.js b/src/styles/utilities/spacing.js
index 6c585dc6c78e..d01246fdcfec 100644
--- a/src/styles/utilities/spacing.js
+++ b/src/styles/utilities/spacing.js
@@ -165,6 +165,10 @@ export default {
marginBottom: -4,
},
+ mrn3: {
+ marginRight: -12,
+ },
+
p0: {
padding: 0,
},
diff --git a/src/styles/variables.js b/src/styles/variables.js
index e57db6ae87bf..4182f8ba0517 100644
--- a/src/styles/variables.js
+++ b/src/styles/variables.js
@@ -28,6 +28,7 @@ export default {
iconSizeSmall: 16,
iconSizeNormal: 20,
iconSizeLarge: 24,
+ iconSizeXLarge: 28,
iconSizeExtraLarge: 40,
emojiSize: 20,
iouAmountTextSize: 40,