Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor Settlement Button for reuse in both Details and Confirm screens #6651

Merged
merged 2 commits into from
Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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