Skip to content

Commit

Permalink
Merge pull request #6651 from Expensify/marcaaron-payWithExpensifyButton
Browse files Browse the repository at this point in the history
Refactor Settlement Button for reuse in both Details and Confirm screens
  • Loading branch information
marcaaron authored Dec 9, 2021
2 parents 387a9f0 + d654c88 commit 6734c9b
Show file tree
Hide file tree
Showing 4 changed files with 199 additions and 242 deletions.
10 changes: 3 additions & 7 deletions src/components/ButtonWithMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand All @@ -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,
Expand All @@ -35,7 +33,6 @@ const propTypes = {
};

const defaultProps = {
onChange: () => {},
isLoading: false,
isDisabled: false,
menuHeaderText: '',
Expand Down Expand Up @@ -63,7 +60,7 @@ class ButtonWithMenu extends PureComponent {
<ButtonWithDropdown
buttonText={selectedItemText}
isLoading={this.props.isLoading}
onButtonPress={this.props.onPress}
onButtonPress={() => this.props.onPress(this.state.selectedItem.value)}
onDropdownPress={() => {
this.setMenuVisibility(true);
}}
Expand All @@ -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
/>
)}
Expand All @@ -92,7 +89,6 @@ class ButtonWithMenu extends PureComponent {
...item,
onSelected: () => {
this.setState({selectedItem: item});
this.props.onChange(item);
},
}))}
/>
Expand Down
114 changes: 35 additions & 79 deletions src/components/IOUConfirmationList.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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,
};

Expand All @@ -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());
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
<>
<ScrollView style={[styles.flexGrow0, styles.flexShrink1, styles.flexBasisAuto, styles.w100]}>
Expand Down Expand Up @@ -435,12 +380,23 @@ class IOUConfirmationList extends Component {
{this.props.translate('session.offlineMessage')}
</ExpensifyText>
)}
<ButtonWithMenu
options={this.state.confirmationButtonOptions}
isDisabled={selectedParticipants.length === 0 || this.props.network.isOffline}
isLoading={this.props.iou.loading && !this.props.network.isOffline}
onPress={this.onPress}
/>
{shouldShowSettlementButton ? (
<SettlementButton
isDisabled={shouldDisableButton}
isLoading={this.props.iou.loading && !this.props.network.isOffline}
onPress={this.onPress}
shouldShowPaypal={Boolean(recipient.payPalMeAddress)}
recipientPhoneNumber={recipient.phoneNumber}
currency={this.props.localCurrencyCode}
/>
) : (
<ButtonWithMenu
isDisabled={shouldDisableButton}
isLoading={isLoading}
onPress={this.onPress}
options={this.splitOrRequestOptions}
/>
)}
</FixedFooter>
</>
);
Expand Down
141 changes: 141 additions & 0 deletions src/components/SettlementButton.js
Original file line number Diff line number Diff line change
@@ -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 (
<ButtonWithMenu
isDisabled={this.props.isDisabled}
isLoading={this.props.isLoading}
onPress={this.props.onPress}
options={this.state.buttonOptions}
/>
);
}
}

SettlementButton.propTypes = propTypes;
SettlementButton.defaultProps = defaultProps;

export default compose(
withLocalize,
withOnyx({
betas: {
key: ONYXKEYS.BETAS,
},
}),
)(SettlementButton);
Loading

0 comments on commit 6734c9b

Please sign in to comment.