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 => ( - - props.onItemSelected(CONST.PAYMENT_METHODS.BANK_ACCOUNT)} - wrapperStyle={styles.pr15} - /> - props.onItemSelected(CONST.PAYMENT_METHODS.DEBIT_CARD)} - wrapperStyle={styles.pr15} - /> - {!props.payPalMeUsername && ( - props.onItemSelected(CONST.PAYMENT_METHODS.PAYPAL)} - wrapperStyle={styles.pr15} - /> - )} - -); +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,