diff --git a/src/components/ButtonWithMenu.js b/src/components/ButtonWithMenu.js index 0bc54435e7f9..421136fc41f8 100644 --- a/src/components/ButtonWithMenu.js +++ b/src/components/ButtonWithMenu.js @@ -14,9 +14,6 @@ const propTypes = { /** Callback to execute when the main button is pressed */ onPress: PropTypes.func.isRequired, - /** Callback to execute when a menu item is selected */ - onChange: PropTypes.func, - /** Whether we should show a loading state for the main button */ isLoading: PropTypes.bool, @@ -26,6 +23,7 @@ const propTypes = { /** Menu options to display */ /** e.g. [{text: 'Pay with Expensify', icon: Wallet}, {text: 'PayPal', icon: PayPal}, {text: 'Venmo', icon: Venmo}] */ options: PropTypes.arrayOf(PropTypes.shape({ + value: PropTypes.string.isRequired, text: PropTypes.string.isRequired, icon: PropTypes.elementType, iconWidth: PropTypes.number, @@ -35,7 +33,6 @@ const propTypes = { }; const defaultProps = { - onChange: () => {}, isLoading: false, isDisabled: false, menuHeaderText: '', @@ -63,7 +60,7 @@ class ButtonWithMenu extends PureComponent { this.props.onPress(this.state.selectedItem.value)} onDropdownPress={() => { this.setMenuVisibility(true); }} @@ -75,7 +72,7 @@ class ButtonWithMenu extends PureComponent { style={[styles.w100]} isLoading={this.props.isLoading} text={selectedItemText} - onPress={this.props.onPress} + onPress={() => this.props.onPress(this.props.options[0].value)} pressOnEnter /> )} @@ -92,7 +89,6 @@ class ButtonWithMenu extends PureComponent { ...item, onSelected: () => { this.setState({selectedItem: item}); - this.props.onChange(item); }, }))} /> diff --git a/src/components/IOUConfirmationList.js b/src/components/IOUConfirmationList.js index d4afada746ac..224a13be27b2 100755 --- a/src/components/IOUConfirmationList.js +++ b/src/components/IOUConfirmationList.js @@ -17,11 +17,8 @@ import FixedFooter from './FixedFooter'; import ExpensiTextInput from './ExpensiTextInput'; import CONST from '../CONST'; import ButtonWithMenu from './ButtonWithMenu'; -import * as Expensicons from './Icon/Expensicons'; -import Permissions from '../libs/Permissions'; -import isAppInstalled from '../libs/isAppInstalled'; -import * as ValidationUtils from '../libs/ValidationUtils'; -import makeCancellablePromise from '../libs/MakeCancellablePromise'; +import SettlementButton from './SettlementButton'; +import Log from '../libs/Log'; const propTypes = { /** Callback to inform parent modal of success */ @@ -118,38 +115,21 @@ class IOUConfirmationList extends Component { constructor(props) { super(props); - const formattedParticipants = _.map(this.getParticipantsWithAmount(this.props.participants), participant => ({ + const formattedParticipants = _.map(this.getParticipantsWithAmount(props.participants), participant => ({ ...participant, selected: true, })); - // Add the button options to payment menu - const confirmationButtonOptions = []; - let defaultButtonOption = { - text: this.props.translate(this.props.hasMultipleParticipants ? 'iou.split' : 'iou.request', { - amount: this.props.numberFormat( - this.props.iouAmount, - {style: 'currency', currency: this.props.iou.selectedCurrencyCode}, + this.splitOrRequestOptions = [{ + text: props.translate(props.hasMultipleParticipants ? 'iou.split' : 'iou.request', { + amount: props.numberFormat( + props.iouAmount, + {style: 'currency', currency: props.iou.selectedCurrencyCode}, ), }), - }; - if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND && this.props.participants.length === 1 && Permissions.canUseIOUSend(this.props.betas)) { - // Add the Expensify Wallet option if available and make it the first option - if (this.props.localCurrencyCode === CONST.CURRENCY.USD && Permissions.canUsePayWithExpensify(this.props.betas) && Permissions.canUseWallet(this.props.betas)) { - confirmationButtonOptions.push({text: this.props.translate('iou.settleExpensify'), icon: Expensicons.Wallet}); - } - - // Add PayPal option - if (this.props.participants[0].payPalMeAddress) { - confirmationButtonOptions.push({text: this.props.translate('iou.settlePaypalMe'), icon: Expensicons.PayPal}); - } - defaultButtonOption = {text: this.props.translate('iou.settleElsewhere'), icon: Expensicons.Cash}; - } - confirmationButtonOptions.push(defaultButtonOption); - - this.checkVenmoAvailabilityPromise = null; + value: props.hasMultipleParticipants ? CONST.IOU.IOU_TYPE.SPLIT : CONST.IOU.IOU_TYPE.REQUEST, + }]; this.state = { - confirmationButtonOptions, participants: formattedParticipants, }; @@ -161,31 +141,17 @@ class IOUConfirmationList extends Component { // We need to wait for the transition animation to end before focusing the TextInput, // otherwise the TextInput isn't animated correctly setTimeout(() => this.textInput.focus(), CONST.ANIMATED_TRANSITION); - - // Only add the Venmo option if we're sending a payment - if (this.props.iouType !== CONST.IOU.IOU_TYPE.SEND) { - return; - } - - this.addVenmoPaymentOptionToMenu(); - } - - componentWillUnmount() { - if (!this.checkVenmoAvailabilityPromise) { - return; - } - - this.checkVenmoAvailabilityPromise.cancel(); - this.checkVenmoAvailabilityPromise = null; } /** - * When confirmation button is clicked + * @param {String} value */ - onPress() { + onPress(value) { if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND) { + Log.info(`[IOU] Sending money via: ${value}`); this.props.onConfirm(); } else { + Log.info(`[IOU] Requesting money via: ${value}`); this.props.onConfirm(this.getSplits()); } } @@ -329,31 +295,6 @@ class IOUConfirmationList extends Component { ]; } - /** - * Adds Venmo, if available, as the second option in the menu of payment options - */ - addVenmoPaymentOptionToMenu() { - if (this.props.localCurrencyCode !== CONST.CURRENCY.USD || !this.state.participants[0].phoneNumber || !ValidationUtils.isValidUSPhone(this.state.participants[0].phoneNumber)) { - return; - } - - this.checkVenmoAvailabilityPromise = makeCancellablePromise(isAppInstalled('venmo')); - this.checkVenmoAvailabilityPromise - .promise - .then((isVenmoInstalled) => { - if (!isVenmoInstalled) { - return; - } - - this.setState(prevState => ({ - confirmationButtonOptions: [...prevState.confirmationButtonOptions.slice(0, 1), - {text: this.props.translate('iou.settleVenmo'), icon: Expensicons.Venmo}, - ...prevState.confirmationButtonOptions.slice(1), - ], - })); - }); - } - /** * Calculates the amount per user given a list of participants * @param {Array} participants @@ -403,6 +344,10 @@ class IOUConfirmationList extends Component { const hoverStyle = this.props.hasMultipleParticipants ? styles.hoveredComponentBG : {}; const toggleOption = this.props.hasMultipleParticipants ? this.toggleOption : undefined; const selectedParticipants = this.getSelectedParticipants(); + const shouldShowSettlementButton = this.props.iouType === CONST.IOU.IOU_TYPE.SEND; + const shouldDisableButton = selectedParticipants.length === 0 || this.props.network.isOffline; + const isLoading = this.props.iou.loading && !this.props.network.isOffline; + const recipient = this.state.participants[0]; return ( <> @@ -435,12 +380,23 @@ class IOUConfirmationList extends Component { {this.props.translate('session.offlineMessage')} )} - + {shouldShowSettlementButton ? ( + + ) : ( + + )} ); diff --git a/src/components/SettlementButton.js b/src/components/SettlementButton.js new file mode 100644 index 000000000000..dd6bfb13b4b1 --- /dev/null +++ b/src/components/SettlementButton.js @@ -0,0 +1,141 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {withOnyx} from 'react-native-onyx'; +import ButtonWithMenu from './ButtonWithMenu'; +import * as Expensicons from './Icon/Expensicons'; +import Permissions from '../libs/Permissions'; +import isAppInstalled from '../libs/isAppInstalled'; +import * as ValidationUtils from '../libs/ValidationUtils'; +import makeCancellablePromise from '../libs/MakeCancellablePromise'; +import ONYXKEYS from '../ONYXKEYS'; +import CONST from '../CONST'; +import compose from '../libs/compose'; +import withLocalize, {withLocalizePropTypes} from './withLocalize'; + +const propTypes = { + /** Settlement currency type */ + currency: PropTypes.string, + + /** Should we show paypal option */ + shouldShowPaypal: PropTypes.bool, + + /** Associated phone login for the person we are sending money to */ + recipientPhoneNumber: PropTypes.string, + + ...withLocalizePropTypes, +}; + +const defaultProps = { + currency: CONST.CURRENCY.USD, + recipientPhoneNumber: '', + shouldShowPaypal: false, +}; + +class SettlementButton extends React.Component { + constructor(props) { + super(props); + + const buttonOptions = []; + + if (props.currency === CONST.CURRENCY.USD && Permissions.canUsePayWithExpensify(props.betas) && Permissions.canUseWallet(props.betas)) { + buttonOptions.push({ + text: props.translate('iou.settleExpensify'), + icon: Expensicons.Wallet, + value: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + }); + } + + if (props.shouldShowPaypal) { + buttonOptions.push({ + text: props.translate('iou.settlePaypalMe'), + icon: Expensicons.PayPal, + value: CONST.IOU.PAYMENT_TYPE.PAYPAL_ME, + }); + } + + buttonOptions.push({ + text: props.translate('iou.settleElsewhere'), + icon: Expensicons.Cash, + value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, + }); + + // Venmo requires an async call to the native layer to determine availability and will be added as an option if available. + this.checkVenmoAvailabilityPromise = null; + + this.state = { + buttonOptions, + }; + } + + componentDidMount() { + this.addVenmoPaymentOptionToMenu(); + } + + componentWillUnmount() { + if (!this.checkVenmoAvailabilityPromise) { + return; + } + + this.checkVenmoAvailabilityPromise.cancel(); + this.checkVenmoAvailabilityPromise = null; + } + + /** + * @returns {Boolean} + */ + doesRecipientHaveValidPhoneLogin() { + return this.props.recipientPhoneNumber && ValidationUtils.isValidUSPhone(this.props.recipientPhoneNumber); + } + + /** + * Adds Venmo, if available, as the second option in the menu of payment options + */ + addVenmoPaymentOptionToMenu() { + if (this.props.currency !== CONST.CURRENCY.USD || !this.doesRecipientHaveValidPhoneLogin()) { + return; + } + + this.checkVenmoAvailabilityPromise = makeCancellablePromise(isAppInstalled('venmo')); + this.checkVenmoAvailabilityPromise + .promise + .then((isVenmoInstalled) => { + if (!isVenmoInstalled) { + return; + } + + this.setState(prevState => ({ + buttonOptions: [...prevState.buttonOptions.slice(0, 1), + { + text: this.props.translate('iou.settleVenmo'), + icon: Expensicons.Venmo, + value: CONST.IOU.PAYMENT_TYPE.VENMO, + }, + ...prevState.buttonOptions.slice(1), + ], + })); + }); + } + + render() { + return ( + + ); + } +} + +SettlementButton.propTypes = propTypes; +SettlementButton.defaultProps = defaultProps; + +export default compose( + withLocalize, + withOnyx({ + betas: { + key: ONYXKEYS.BETAS, + }, + }), +)(SettlementButton); diff --git a/src/pages/iou/IOUDetailsModal.js b/src/pages/iou/IOUDetailsModal.js index 36e613ee6d57..2cc2c0c407e0 100644 --- a/src/pages/iou/IOUDetailsModal.js +++ b/src/pages/iou/IOUDetailsModal.js @@ -9,7 +9,6 @@ import ONYXKEYS from '../../ONYXKEYS'; import themeColors from '../../styles/themes/default'; import HeaderWithCloseButton from '../../components/HeaderWithCloseButton'; import Navigation from '../../libs/Navigation/Navigation'; -import ButtonWithDropdown from '../../components/ButtonWithDropdown'; import ScreenWrapper from '../../components/ScreenWrapper'; import * as IOU from '../../libs/actions/IOU'; import * as Report from '../../libs/actions/Report'; @@ -18,12 +17,7 @@ import IOUTransactions from './IOUTransactions'; import withLocalize, {withLocalizePropTypes} from '../../components/withLocalize'; import compose from '../../libs/compose'; import CONST from '../../CONST'; -import PopoverMenu from '../../components/PopoverMenu'; -import isAppInstalled from '../../libs/isAppInstalled'; -import ExpensifyButton from '../../components/ExpensifyButton'; -import Permissions from '../../libs/Permissions'; -import * as Expensicons from '../../components/Icon/Expensicons'; -import * as ValidationUtils from '../../libs/ValidationUtils'; +import SettlementButton from '../../components/SettlementButton'; const propTypes = { /** URL Route params */ @@ -69,9 +63,6 @@ const propTypes = { email: PropTypes.string, }).isRequired, - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string).isRequired, - ...withLocalizePropTypes, }; @@ -81,132 +72,35 @@ const defaultProps = { }; class IOUDetailsModal extends Component { - constructor(props) { - super(props); - - // We always have the option to settle manually - const paymentOptions = [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]; - - // Only allow settling via PayPal.me if the submitter has a username set - if (lodashGet(props, 'iouReport.submitterPayPalMeAddress')) { - paymentOptions.push(CONST.IOU.PAYMENT_TYPE.PAYPAL_ME); - } - - this.submitterPhoneNumber = undefined; - this.isComponentMounted = false; - - this.state = { - paymentType: CONST.IOU.PAYMENT_TYPE.ELSEWHERE, - isSettlementMenuVisible: false, - paymentOptions, - }; - - this.performIOUPayment = this.performIOUPayment.bind(this); - } - componentDidMount() { - this.isComponentMounted = true; Report.fetchIOUReportByID(this.props.route.params.iouReportID, this.props.route.params.chatReportID, true); - this.addVenmoPaymentOptionIfAvailable(); - this.addExpensifyPaymentOptionIfAvailable(); } - componentWillUnmount() { - this.isComponentMounted = false; - } - - setMenuVisibility(isSettlementMenuVisible) { - this.setState({isSettlementMenuVisible}); + /** + * @returns {String} + */ + getSubmitterPhoneNumber() { + return _.first(lodashGet(this.props, 'iouReport.submitterPhoneNumbers', [])) || ''; } - performIOUPayment() { + /** + * @param {String} paymentMethodType + */ + performIOUPayment(paymentMethodType) { IOU.payIOUReport({ chatReportID: this.props.route.params.chatReportID, reportID: this.props.route.params.iouReportID, - paymentMethodType: this.state.paymentType, + paymentMethodType, amount: this.props.iouReport.total, currency: this.props.iouReport.currency, submitterPayPalMeAddress: this.props.iouReport.submitterPayPalMeAddress, - submitterPhoneNumber: this.submitterPhoneNumber, + submitterPhoneNumber: this.getSubmitterPhoneNumber(), }); } - /** - * Checks to see if we can use Venmo. The following conditions must be met: - * - * 1. The IOU report currency is USD - * 2. The submitter has as a valid US phone number - * 3. Venmo app is installed - * - */ - addVenmoPaymentOptionIfAvailable() { - if (lodashGet(this.props, 'iouReport.currency') !== CONST.CURRENCY.USD) { - return; - } - - const submitterPhoneNumbers = lodashGet(this.props, 'iouReport.submitterPhoneNumbers', []); - if (_.isEmpty(submitterPhoneNumbers)) { - return; - } - - this.submitterPhoneNumber = _.find(submitterPhoneNumbers, ValidationUtils.isValidUSPhone); - if (!this.submitterPhoneNumber) { - return; - } - - isAppInstalled('venmo') - .then((isVenmoInstalled) => { - // We will return early if the component has unmounted before the async call resolves. This prevents - // setting state on unmounted components which prints noisy warnings in the console. - if (!isVenmoInstalled || !this.isComponentMounted) { - return; - } - - this.setState(prevState => ({ - paymentOptions: [...prevState.paymentOptions, CONST.IOU.PAYMENT_TYPE.VENMO], - })); - }); - } - - /** - * Checks to see if we can use Expensify Wallet to pay for this IOU report. - * The IOU report currency must be USD. - */ - addExpensifyPaymentOptionIfAvailable() { - if (lodashGet(this.props, 'iouReport.currency') !== CONST.CURRENCY.USD - || !Permissions.canUsePayWithExpensify(this.props.betas)) { - return; - } - - // Make it the first payment option and set it as the default. - this.setState(prevState => ({ - paymentOptions: [CONST.IOU.PAYMENT_TYPE.EXPENSIFY, ...prevState.paymentOptions], - paymentType: CONST.IOU.PAYMENT_TYPE.EXPENSIFY, - })); - } - render() { const sessionEmail = lodashGet(this.props.session, 'email', null); const reportIsLoading = _.isUndefined(this.props.iouReport); - const paymentTypeOptions = { - [CONST.IOU.PAYMENT_TYPE.EXPENSIFY]: { - text: this.props.translate('iou.settleExpensify'), - icon: Expensicons.Wallet, - }, - [CONST.IOU.PAYMENT_TYPE.VENMO]: { - text: this.props.translate('iou.settleVenmo'), - icon: Expensicons.Venmo, - }, - [CONST.IOU.PAYMENT_TYPE.PAYPAL_ME]: { - text: this.props.translate('iou.settlePaypalMe'), - icon: Expensicons.PayPal, - }, - [CONST.IOU.PAYMENT_TYPE.ELSEWHERE]: { - text: this.props.translate('iou.settleElsewhere'), - icon: Expensicons.Cash, - }, - }; - const selectedPaymentType = paymentTypeOptions[this.state.paymentType].text; return ( + {(this.props.iouReport.hasOutstandingIOU && this.props.iouReport.managerEmail === sessionEmail && ( - {this.state.paymentOptions.length > 1 ? ( - { - this.setMenuVisibility(true); - }} - /> - ) : ( - - )} - {this.state.paymentOptions.length > 1 && ( - this.setMenuVisibility(false)} - onItemSelected={() => this.setMenuVisibility(false)} - anchorPosition={styles.createMenuPositionRightSidepane} - animationIn="fadeInUp" - animationOut="fadeOutDown" - menuItems={_.map(this.state.paymentOptions, paymentType => ({ - text: paymentTypeOptions[paymentType].text, - icon: paymentTypeOptions[paymentType].icon, - onSelected: () => { - this.setState({paymentType}); - }, - }))} - /> - )} + this.performIOUPayment(paymentMethodType)} + recipientPhoneNumber={this.getSubmitterPhoneNumber()} + shouldShowPaypal={Boolean(lodashGet(this.props, 'iouReport.submitterPayPalMeAddress'))} + currency={lodashGet(this.props, 'iouReport.currency')} + /> ))} @@ -291,8 +158,5 @@ export default compose( session: { key: ONYXKEYS.SESSION, }, - betas: { - key: ONYXKEYS.BETAS, - }, }), )(IOUDetailsModal);