Skip to content

Commit 6734c9b

Browse files
authored
Merge pull request #6651 from Expensify/marcaaron-payWithExpensifyButton
Refactor Settlement Button for reuse in both Details and Confirm screens
2 parents 387a9f0 + d654c88 commit 6734c9b

File tree

4 files changed

+199
-242
lines changed

4 files changed

+199
-242
lines changed

src/components/ButtonWithMenu.js

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@ const propTypes = {
1414
/** Callback to execute when the main button is pressed */
1515
onPress: PropTypes.func.isRequired,
1616

17-
/** Callback to execute when a menu item is selected */
18-
onChange: PropTypes.func,
19-
2017
/** Whether we should show a loading state for the main button */
2118
isLoading: PropTypes.bool,
2219

@@ -26,6 +23,7 @@ const propTypes = {
2623
/** Menu options to display */
2724
/** e.g. [{text: 'Pay with Expensify', icon: Wallet}, {text: 'PayPal', icon: PayPal}, {text: 'Venmo', icon: Venmo}] */
2825
options: PropTypes.arrayOf(PropTypes.shape({
26+
value: PropTypes.string.isRequired,
2927
text: PropTypes.string.isRequired,
3028
icon: PropTypes.elementType,
3129
iconWidth: PropTypes.number,
@@ -35,7 +33,6 @@ const propTypes = {
3533
};
3634

3735
const defaultProps = {
38-
onChange: () => {},
3936
isLoading: false,
4037
isDisabled: false,
4138
menuHeaderText: '',
@@ -63,7 +60,7 @@ class ButtonWithMenu extends PureComponent {
6360
<ButtonWithDropdown
6461
buttonText={selectedItemText}
6562
isLoading={this.props.isLoading}
66-
onButtonPress={this.props.onPress}
63+
onButtonPress={() => this.props.onPress(this.state.selectedItem.value)}
6764
onDropdownPress={() => {
6865
this.setMenuVisibility(true);
6966
}}
@@ -75,7 +72,7 @@ class ButtonWithMenu extends PureComponent {
7572
style={[styles.w100]}
7673
isLoading={this.props.isLoading}
7774
text={selectedItemText}
78-
onPress={this.props.onPress}
75+
onPress={() => this.props.onPress(this.props.options[0].value)}
7976
pressOnEnter
8077
/>
8178
)}
@@ -92,7 +89,6 @@ class ButtonWithMenu extends PureComponent {
9289
...item,
9390
onSelected: () => {
9491
this.setState({selectedItem: item});
95-
this.props.onChange(item);
9692
},
9793
}))}
9894
/>

src/components/IOUConfirmationList.js

Lines changed: 35 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ import FixedFooter from './FixedFooter';
1717
import ExpensiTextInput from './ExpensiTextInput';
1818
import CONST from '../CONST';
1919
import ButtonWithMenu from './ButtonWithMenu';
20-
import * as Expensicons from './Icon/Expensicons';
21-
import Permissions from '../libs/Permissions';
22-
import isAppInstalled from '../libs/isAppInstalled';
23-
import * as ValidationUtils from '../libs/ValidationUtils';
24-
import makeCancellablePromise from '../libs/MakeCancellablePromise';
20+
import SettlementButton from './SettlementButton';
21+
import Log from '../libs/Log';
2522

2623
const propTypes = {
2724
/** Callback to inform parent modal of success */
@@ -118,38 +115,21 @@ class IOUConfirmationList extends Component {
118115
constructor(props) {
119116
super(props);
120117

121-
const formattedParticipants = _.map(this.getParticipantsWithAmount(this.props.participants), participant => ({
118+
const formattedParticipants = _.map(this.getParticipantsWithAmount(props.participants), participant => ({
122119
...participant, selected: true,
123120
}));
124121

125-
// Add the button options to payment menu
126-
const confirmationButtonOptions = [];
127-
let defaultButtonOption = {
128-
text: this.props.translate(this.props.hasMultipleParticipants ? 'iou.split' : 'iou.request', {
129-
amount: this.props.numberFormat(
130-
this.props.iouAmount,
131-
{style: 'currency', currency: this.props.iou.selectedCurrencyCode},
122+
this.splitOrRequestOptions = [{
123+
text: props.translate(props.hasMultipleParticipants ? 'iou.split' : 'iou.request', {
124+
amount: props.numberFormat(
125+
props.iouAmount,
126+
{style: 'currency', currency: props.iou.selectedCurrencyCode},
132127
),
133128
}),
134-
};
135-
if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND && this.props.participants.length === 1 && Permissions.canUseIOUSend(this.props.betas)) {
136-
// Add the Expensify Wallet option if available and make it the first option
137-
if (this.props.localCurrencyCode === CONST.CURRENCY.USD && Permissions.canUsePayWithExpensify(this.props.betas) && Permissions.canUseWallet(this.props.betas)) {
138-
confirmationButtonOptions.push({text: this.props.translate('iou.settleExpensify'), icon: Expensicons.Wallet});
139-
}
140-
141-
// Add PayPal option
142-
if (this.props.participants[0].payPalMeAddress) {
143-
confirmationButtonOptions.push({text: this.props.translate('iou.settlePaypalMe'), icon: Expensicons.PayPal});
144-
}
145-
defaultButtonOption = {text: this.props.translate('iou.settleElsewhere'), icon: Expensicons.Cash};
146-
}
147-
confirmationButtonOptions.push(defaultButtonOption);
148-
149-
this.checkVenmoAvailabilityPromise = null;
129+
value: props.hasMultipleParticipants ? CONST.IOU.IOU_TYPE.SPLIT : CONST.IOU.IOU_TYPE.REQUEST,
130+
}];
150131

151132
this.state = {
152-
confirmationButtonOptions,
153133
participants: formattedParticipants,
154134
};
155135

@@ -161,31 +141,17 @@ class IOUConfirmationList extends Component {
161141
// We need to wait for the transition animation to end before focusing the TextInput,
162142
// otherwise the TextInput isn't animated correctly
163143
setTimeout(() => this.textInput.focus(), CONST.ANIMATED_TRANSITION);
164-
165-
// Only add the Venmo option if we're sending a payment
166-
if (this.props.iouType !== CONST.IOU.IOU_TYPE.SEND) {
167-
return;
168-
}
169-
170-
this.addVenmoPaymentOptionToMenu();
171-
}
172-
173-
componentWillUnmount() {
174-
if (!this.checkVenmoAvailabilityPromise) {
175-
return;
176-
}
177-
178-
this.checkVenmoAvailabilityPromise.cancel();
179-
this.checkVenmoAvailabilityPromise = null;
180144
}
181145

182146
/**
183-
* When confirmation button is clicked
147+
* @param {String} value
184148
*/
185-
onPress() {
149+
onPress(value) {
186150
if (this.props.iouType === CONST.IOU.IOU_TYPE.SEND) {
151+
Log.info(`[IOU] Sending money via: ${value}`);
187152
this.props.onConfirm();
188153
} else {
154+
Log.info(`[IOU] Requesting money via: ${value}`);
189155
this.props.onConfirm(this.getSplits());
190156
}
191157
}
@@ -329,31 +295,6 @@ class IOUConfirmationList extends Component {
329295
];
330296
}
331297

332-
/**
333-
* Adds Venmo, if available, as the second option in the menu of payment options
334-
*/
335-
addVenmoPaymentOptionToMenu() {
336-
if (this.props.localCurrencyCode !== CONST.CURRENCY.USD || !this.state.participants[0].phoneNumber || !ValidationUtils.isValidUSPhone(this.state.participants[0].phoneNumber)) {
337-
return;
338-
}
339-
340-
this.checkVenmoAvailabilityPromise = makeCancellablePromise(isAppInstalled('venmo'));
341-
this.checkVenmoAvailabilityPromise
342-
.promise
343-
.then((isVenmoInstalled) => {
344-
if (!isVenmoInstalled) {
345-
return;
346-
}
347-
348-
this.setState(prevState => ({
349-
confirmationButtonOptions: [...prevState.confirmationButtonOptions.slice(0, 1),
350-
{text: this.props.translate('iou.settleVenmo'), icon: Expensicons.Venmo},
351-
...prevState.confirmationButtonOptions.slice(1),
352-
],
353-
}));
354-
});
355-
}
356-
357298
/**
358299
* Calculates the amount per user given a list of participants
359300
* @param {Array} participants
@@ -403,6 +344,10 @@ class IOUConfirmationList extends Component {
403344
const hoverStyle = this.props.hasMultipleParticipants ? styles.hoveredComponentBG : {};
404345
const toggleOption = this.props.hasMultipleParticipants ? this.toggleOption : undefined;
405346
const selectedParticipants = this.getSelectedParticipants();
347+
const shouldShowSettlementButton = this.props.iouType === CONST.IOU.IOU_TYPE.SEND;
348+
const shouldDisableButton = selectedParticipants.length === 0 || this.props.network.isOffline;
349+
const isLoading = this.props.iou.loading && !this.props.network.isOffline;
350+
const recipient = this.state.participants[0];
406351
return (
407352
<>
408353
<ScrollView style={[styles.flexGrow0, styles.flexShrink1, styles.flexBasisAuto, styles.w100]}>
@@ -435,12 +380,23 @@ class IOUConfirmationList extends Component {
435380
{this.props.translate('session.offlineMessage')}
436381
</ExpensifyText>
437382
)}
438-
<ButtonWithMenu
439-
options={this.state.confirmationButtonOptions}
440-
isDisabled={selectedParticipants.length === 0 || this.props.network.isOffline}
441-
isLoading={this.props.iou.loading && !this.props.network.isOffline}
442-
onPress={this.onPress}
443-
/>
383+
{shouldShowSettlementButton ? (
384+
<SettlementButton
385+
isDisabled={shouldDisableButton}
386+
isLoading={this.props.iou.loading && !this.props.network.isOffline}
387+
onPress={this.onPress}
388+
shouldShowPaypal={Boolean(recipient.payPalMeAddress)}
389+
recipientPhoneNumber={recipient.phoneNumber}
390+
currency={this.props.localCurrencyCode}
391+
/>
392+
) : (
393+
<ButtonWithMenu
394+
isDisabled={shouldDisableButton}
395+
isLoading={isLoading}
396+
onPress={this.onPress}
397+
options={this.splitOrRequestOptions}
398+
/>
399+
)}
444400
</FixedFooter>
445401
</>
446402
);

src/components/SettlementButton.js

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import {withOnyx} from 'react-native-onyx';
4+
import ButtonWithMenu from './ButtonWithMenu';
5+
import * as Expensicons from './Icon/Expensicons';
6+
import Permissions from '../libs/Permissions';
7+
import isAppInstalled from '../libs/isAppInstalled';
8+
import * as ValidationUtils from '../libs/ValidationUtils';
9+
import makeCancellablePromise from '../libs/MakeCancellablePromise';
10+
import ONYXKEYS from '../ONYXKEYS';
11+
import CONST from '../CONST';
12+
import compose from '../libs/compose';
13+
import withLocalize, {withLocalizePropTypes} from './withLocalize';
14+
15+
const propTypes = {
16+
/** Settlement currency type */
17+
currency: PropTypes.string,
18+
19+
/** Should we show paypal option */
20+
shouldShowPaypal: PropTypes.bool,
21+
22+
/** Associated phone login for the person we are sending money to */
23+
recipientPhoneNumber: PropTypes.string,
24+
25+
...withLocalizePropTypes,
26+
};
27+
28+
const defaultProps = {
29+
currency: CONST.CURRENCY.USD,
30+
recipientPhoneNumber: '',
31+
shouldShowPaypal: false,
32+
};
33+
34+
class SettlementButton extends React.Component {
35+
constructor(props) {
36+
super(props);
37+
38+
const buttonOptions = [];
39+
40+
if (props.currency === CONST.CURRENCY.USD && Permissions.canUsePayWithExpensify(props.betas) && Permissions.canUseWallet(props.betas)) {
41+
buttonOptions.push({
42+
text: props.translate('iou.settleExpensify'),
43+
icon: Expensicons.Wallet,
44+
value: CONST.IOU.PAYMENT_TYPE.EXPENSIFY,
45+
});
46+
}
47+
48+
if (props.shouldShowPaypal) {
49+
buttonOptions.push({
50+
text: props.translate('iou.settlePaypalMe'),
51+
icon: Expensicons.PayPal,
52+
value: CONST.IOU.PAYMENT_TYPE.PAYPAL_ME,
53+
});
54+
}
55+
56+
buttonOptions.push({
57+
text: props.translate('iou.settleElsewhere'),
58+
icon: Expensicons.Cash,
59+
value: CONST.IOU.PAYMENT_TYPE.ELSEWHERE,
60+
});
61+
62+
// Venmo requires an async call to the native layer to determine availability and will be added as an option if available.
63+
this.checkVenmoAvailabilityPromise = null;
64+
65+
this.state = {
66+
buttonOptions,
67+
};
68+
}
69+
70+
componentDidMount() {
71+
this.addVenmoPaymentOptionToMenu();
72+
}
73+
74+
componentWillUnmount() {
75+
if (!this.checkVenmoAvailabilityPromise) {
76+
return;
77+
}
78+
79+
this.checkVenmoAvailabilityPromise.cancel();
80+
this.checkVenmoAvailabilityPromise = null;
81+
}
82+
83+
/**
84+
* @returns {Boolean}
85+
*/
86+
doesRecipientHaveValidPhoneLogin() {
87+
return this.props.recipientPhoneNumber && ValidationUtils.isValidUSPhone(this.props.recipientPhoneNumber);
88+
}
89+
90+
/**
91+
* Adds Venmo, if available, as the second option in the menu of payment options
92+
*/
93+
addVenmoPaymentOptionToMenu() {
94+
if (this.props.currency !== CONST.CURRENCY.USD || !this.doesRecipientHaveValidPhoneLogin()) {
95+
return;
96+
}
97+
98+
this.checkVenmoAvailabilityPromise = makeCancellablePromise(isAppInstalled('venmo'));
99+
this.checkVenmoAvailabilityPromise
100+
.promise
101+
.then((isVenmoInstalled) => {
102+
if (!isVenmoInstalled) {
103+
return;
104+
}
105+
106+
this.setState(prevState => ({
107+
buttonOptions: [...prevState.buttonOptions.slice(0, 1),
108+
{
109+
text: this.props.translate('iou.settleVenmo'),
110+
icon: Expensicons.Venmo,
111+
value: CONST.IOU.PAYMENT_TYPE.VENMO,
112+
},
113+
...prevState.buttonOptions.slice(1),
114+
],
115+
}));
116+
});
117+
}
118+
119+
render() {
120+
return (
121+
<ButtonWithMenu
122+
isDisabled={this.props.isDisabled}
123+
isLoading={this.props.isLoading}
124+
onPress={this.props.onPress}
125+
options={this.state.buttonOptions}
126+
/>
127+
);
128+
}
129+
}
130+
131+
SettlementButton.propTypes = propTypes;
132+
SettlementButton.defaultProps = defaultProps;
133+
134+
export default compose(
135+
withLocalize,
136+
withOnyx({
137+
betas: {
138+
key: ONYXKEYS.BETAS,
139+
},
140+
}),
141+
)(SettlementButton);

0 commit comments

Comments
 (0)