diff --git a/src/CONST.js b/src/CONST.js index 33a4f3add359..346d57a254d4 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -386,11 +386,20 @@ const CONST = { }, WALLET: { - TRANSFER_BALANCE_FEE: 0.30, TRANSFER_METHOD_TYPE: { INSTANT: 'instant', ACH: 'ach', }, + TRANSFER_METHOD_TYPE_FEE: { + INSTANT: { + RATE: 1.5, + MINIMUM_FEE: 25, + }, + ACH: { + RATE: 0, + MINIMUM_FEE: 0, + }, + }, ERROR: { IDENTITY_NOT_FOUND: 'Identity not found', INVALID_SSN: 'Invalid SSN', @@ -462,6 +471,11 @@ const CONST = { BANK_ACCOUNT: 'bankAccount', }, + PAYMENT_METHOD_ID_KEYS: { + DEBIT_CARD: 'fundID', + BANK_ACCOUNT: 'bankAccountID', + }, + IOU: { // Note: These payment types are used when building IOU reportAction message values in the server and should // not be changed. diff --git a/src/components/CurrentWalletBalance.js b/src/components/CurrentWalletBalance.js index fcc56fb9fb94..580b110a5d9f 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/languages/en.js b/src/languages/en.js index 794cfb809e80..f3f2b5a7749d 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -353,7 +353,7 @@ export default { transferAmountPage: { transfer: ({amount}) => `Transfer${amount ? ` ${amount}` : ''}`, instant: 'Instant (Debit Card)', - instantSummary: ({amount}) => `1.5% fee (${amount} minimum)`, + instantSummary: ({rate, minAmount}) => `${rate}% fee (${minAmount} minimum)`, ach: '1-3 Business Days (Bank Account)', achSummary: 'No fee', whichAccount: 'Which Account?', diff --git a/src/languages/es.js b/src/languages/es.js index b3a5f036272c..a1e92dca30dc 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -353,7 +353,7 @@ export default { transferAmountPage: { transfer: ({amount}) => `Transferir${amount ? ` ${amount}` : ''}`, instant: 'Instante', - instantSummary: ({amount}) => `Tarifa del 1.5% (${amount} mínimo)`, + instantSummary: ({rate, minAmount}) => `Tarifa del ${rate}% (${minAmount} mínimo)`, ach: '1-3 días laborales', achSummary: 'Sin cargo', whichAccount: '¿Que cuenta?', diff --git a/src/libs/PaymentUtils.js b/src/libs/PaymentUtils.js index f33cd8f8e0b7..3950a6a3214b 100644 --- a/src/libs/PaymentUtils.js +++ b/src/libs/PaymentUtils.js @@ -25,9 +25,9 @@ function hasExpensifyPaymentMethod(cardList = [], bankAccountList = []) { * @param {Array} bankAccountList * @param {Array} cardList * @param {String} [payPalMeUsername=''] - * @returns {Array} + * @returns {Array} */ -function getPaymentMethods(bankAccountList, cardList, payPalMeUsername = '') { +function formatPaymentMethods(bankAccountList, cardList, payPalMeUsername = '') { const combinedPaymentMethods = []; _.each(bankAccountList, (bankAccount) => { @@ -79,7 +79,21 @@ function getPaymentMethods(bankAccountList, cardList, payPalMeUsername = '') { return combinedPaymentMethods; } +/** + * @param {Number} currentBalance + * @param {String} methodType + * @returns {Number} + */ +function calculateWalletTransferBalanceFee(currentBalance, methodType) { + const transferMethodTypeFeeStructure = methodType === CONST.WALLET.TRANSFER_METHOD_TYPE.INSTANT + ? CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT + : CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.ACH; + const calculateFee = (currentBalance * transferMethodTypeFeeStructure.RATE) / 100; + return Math.max(calculateFee, transferMethodTypeFeeStructure.MINIMUM_FEE); +} + export { hasExpensifyPaymentMethod, - getPaymentMethods, + formatPaymentMethods, + calculateWalletTransferBalanceFee, }; diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js index 2010456f2b32..38735599f882 100644 --- a/src/libs/actions/PaymentMethods.js +++ b/src/libs/actions/PaymentMethods.js @@ -8,6 +8,7 @@ import Growl from '../Growl'; import * as Localize from '../Localize'; import Navigation from '../Navigation/Navigation'; import * as CardUtils from '../CardUtils'; +import ROUTES from '../../ROUTES'; /** * Sets up a ref to an instance of the KYC Wall component. @@ -114,10 +115,67 @@ function clearDebitCardFormErrorAndSubmit() { }); } +/** + * Call the API to transfer wallet balance. + * @param {Object} paymentMethod + * @param {*} paymentMethod.methodID + * @param {String} paymentMethod.type + */ +function transferWalletBalance(paymentMethod) { + const paymentMethodIDKey = paymentMethod.type === CONST.PAYMENT_METHODS.BANK_ACCOUNT + ? CONST.PAYMENT_METHOD_ID_KEYS.BANK_ACCOUNT + : CONST.PAYMENT_METHOD_ID_KEYS.DEBIT_CARD; + const parameters = { + [paymentMethodIDKey]: paymentMethod.methodID, + }; + Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {loading: true}); + + 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, {shouldShowConfirmModal: true, loading: false}); + Navigation.navigate(ROUTES.SETTINGS_PAYMENTS); + }).catch(() => { + Growl.error(Localize.translateLocal('transferAmountPage.failedTransfer')); + Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {loading: false}); + }); +} + +/** + * Set the transfer account and reset the transfer data for Wallet balance transfer + * @param {String} selectedAccountID + */ +function saveWalletTransferAccountAndResetData(selectedAccountID) { + Onyx.merge(ONYXKEYS.WALLET_TRANSFER, { + selectedAccountID, + filterPaymentMethodType: null, + loading: false, + shouldShowConfirmModal: false, + }); +} + +/** + * @param {Number} transferAmount + */ +function saveWalletTransferAmount(transferAmount) { + Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {transferAmount}); +} + +function dismissWalletConfirmModal() { + Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {shouldShowConfirmModal: false}); +} + export { getPaymentMethods, addBillingCard, kycWallRef, continueSetup, clearDebitCardFormErrorAndSubmit, + transferWalletBalance, + saveWalletTransferAccountAndResetData, + saveWalletTransferAmount, + dismissWalletConfirmModal, }; diff --git a/src/pages/settings/Payments/PaymentMethodList.js b/src/pages/settings/Payments/PaymentMethodList.js index 1e6a0571343a..1e2c3b12424b 100644 --- a/src/pages/settings/Payments/PaymentMethodList.js +++ b/src/pages/settings/Payments/PaymentMethodList.js @@ -69,7 +69,7 @@ class PaymentMethodList extends Component { * @returns {Array} */ createPaymentMethodList() { - let combinedPaymentMethods = PaymentUtils.getPaymentMethods(this.props.bankAccountList, this.props.cardList, this.props.payPalMeUsername); + let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(this.props.bankAccountList, this.props.cardList, this.props.payPalMeUsername); combinedPaymentMethods = _.map(combinedPaymentMethods, paymentMethod => ({ ...paymentMethod, type: MENU_ITEM, diff --git a/src/pages/settings/Payments/PaymentsPage.js b/src/pages/settings/Payments/PaymentsPage.js index 7e4772df117e..3279ce0adb72 100644 --- a/src/pages/settings/Payments/PaymentsPage.js +++ b/src/pages/settings/Payments/PaymentsPage.js @@ -19,18 +19,28 @@ import ONYXKEYS from '../../../ONYXKEYS'; 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 walletTransferPropTypes from './walletTransferPropTypes'; +import ConfirmModal from '../../../components/ConfirmModal'; const propTypes = { - ...withLocalizePropTypes, + /** Wallet balance transfer props */ + walletTransfer: 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: { + shouldShowConfirmModal: false, + }, betas: [], isLoadingPaymentMethods: true, }; @@ -48,6 +58,7 @@ class PaymentsPage extends React.Component { this.paymentMethodPressed = this.paymentMethodPressed.bind(this); this.addPaymentMethodTypePressed = this.addPaymentMethodTypePressed.bind(this); this.hideAddPaymentMenu = this.hideAddPaymentMenu.bind(this); + this.navigateToTransferBalancePage = this.navigateToTransferBalancePage.bind(this); } componentDidMount() { @@ -110,6 +121,10 @@ class PaymentsPage extends React.Component { this.setState({shouldShowAddPaymentMenu: false}); } + navigateToTransferBalancePage() { + Navigation.navigate(ROUTES.SETTINGS_PAYMENTS_TRANSFER_BALANCE); + } + render() { return ( @@ -121,9 +136,19 @@ class PaymentsPage extends React.Component { onCloseButtonPress={() => Navigation.dismissModal(true)} /> - { - Permissions.canUseWallet(this.props.betas) && - } + {Permissions.canUseWallet(this.props.betas) && ( + <> + + + + + + )} @@ -145,6 +170,19 @@ class PaymentsPage extends React.Component { }} onItemSelected={method => this.addPaymentMethodTypePressed(method)} /> + ); @@ -160,6 +198,9 @@ export default compose( betas: { key: ONYXKEYS.BETAS, }, + walletTransfer: { + key: ONYXKEYS.WALLET_TRANSFER, + }, isLoadingPaymentMethods: { key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS, initWithStoredValues: false, diff --git a/src/pages/settings/Payments/TransferBalancePage.js b/src/pages/settings/Payments/TransferBalancePage.js index eb12627c2a3c..25ff4366b292 100644 --- a/src/pages/settings/Payments/TransferBalancePage.js +++ b/src/pages/settings/Payments/TransferBalancePage.js @@ -1,27 +1,253 @@ +import _ from 'underscore'; import React from 'react'; +import {View, ScrollView} from 'react-native'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import lodashGet from 'lodash/get'; +import ONYXKEYS from '../../../ONYXKEYS'; 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'; +import * as Expensicons from '../../../components/Icon/Expensicons'; +import MenuItem from '../../../components/MenuItem'; +import CONST from '../../../CONST'; +import variables from '../../../styles/variables'; +import Text from '../../../components/Text'; +import Button from '../../../components/Button'; +import FixedFooter from '../../../components/FixedFooter'; +import CurrentWalletBalance from '../../../components/CurrentWalletBalance'; +import walletTransferPropTypes from './walletTransferPropTypes'; +import * as PaymentMethods from '../../../libs/actions/PaymentMethods'; +import * as PaymentUtils from '../../../libs/PaymentUtils'; +import userWalletPropTypes from '../../EnablePayments/userWalletPropTypes'; const propTypes = { + /** User's wallet information */ + userWallet: userWalletPropTypes.userWallet, + + /** Array of bank account objects */ + bankAccountList: 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 */ + 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, + })), + + /** Wallet balance transfer props */ + walletTransfer: walletTransferPropTypes, + ...withLocalizePropTypes, }; -const TransferBalancePage = props => ( - - - Navigation.goBack()} - onCloseButtonPress={() => Navigation.dismissModal(true)} - /> - - -); +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', { + rate: this.props.numberFormat(CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.RATE), + minAmount: this.props.numberFormat( + CONST.WALLET.TRANSFER_METHOD_TYPE_FEE.INSTANT.MINIMUM_FEE / 100, + {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.saveTransferAmountAndBalance = this.saveTransferAmountAndBalance.bind(this); + const selectedAccount = this.getSelectedPaymentMethodAccount(); + PaymentMethods.saveWalletTransferAccountAndResetData(selectedAccount ? selectedAccount.id : ''); + } + + /** + * Get the selected/default payment method account for wallet transfer + * @returns {Object|undefined} + */ + getSelectedPaymentMethodAccount() { + const paymentMethods = PaymentUtils.formatPaymentMethods( + this.props.bankAccountList, + this.props.cardList, + ); + + const accountID = this.props.walletTransfer.selectedAccountID || lodashGet(this.props, 'userWallet.walletLinkedAccountID', ''); + return _.find(paymentMethods, method => method.methodID === accountID); + } + + /** + * @param {Number} transferAmount + * @param {Object} selectedAccount + */ + saveTransferAmountAndBalance(transferAmount, selectedAccount) { + PaymentMethods.saveWalletTransferAmount(transferAmount); + PaymentMethods.transferWalletBalance(selectedAccount); + } + + render() { + const selectedAccount = this.getSelectedPaymentMethodAccount(); + const selectedPaymentType = selectedAccount && selectedAccount.type === CONST.PAYMENT_METHODS.BANK_ACCOUNT + ? CONST.WALLET.TRANSFER_METHOD_TYPE.ACH + : CONST.WALLET.TRANSFER_METHOD_TYPE.INSTANT; + + const calculatedFee = PaymentUtils.calculateWalletTransferBalanceFee(this.props.userWallet.currentBalance, selectedPaymentType); + const transferAmount = this.props.userWallet.currentBalance - calculatedFee; + const canTransfer = transferAmount > 0; + const isButtonDisabled = !canTransfer || !selectedAccount; + + return ( + + + Navigation.goBack()} + onCloseButtonPress={() => Navigation.dismissModal(true)} + /> + + + + + {_.map(this.paymentTypes, paymentType => ( + + ))} + + {this.props.translate('transferAmountPage.whichAccount')} + + {Boolean(selectedAccount) + && ( + + )} + + {this.props.translate('transferAmountPage.fee')} + + + {this.props.numberFormat( + calculatedFee / 100, + {style: 'currency', currency: 'USD'}, + )} + + + +