Skip to content

Commit 8c5ab14

Browse files
authored
Merge pull request Expensify#65296 from getusha/re-feat-standardize-pay-button
feat: standardize pay button
2 parents 6905fe4 + 7f526d2 commit 8c5ab14

56 files changed

Lines changed: 1539 additions & 316 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/CONST/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6848,9 +6848,9 @@ const CONST = {
68486848
},
68496849
LAST_PAYMENT_METHOD: {
68506850
LAST_USED: 'lastUsed',
6851-
IOU: 'Iou',
6852-
EXPENSE: 'Expense',
6853-
INVOICE: 'Invoice',
6851+
IOU: 'iou',
6852+
EXPENSE: 'expense',
6853+
INVOICE: 'invoice',
68546854
},
68556855
SKIPPABLE_COLLECTION_MEMBER_IDS: [String(DEFAULT_NUMBER_ID), '-1', 'undefined', 'null', 'NaN'] as string[],
68566856
SETUP_SPECIALIST_LOGIN: 'Setup Specialist',

src/components/Button/index.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,19 @@ function Button(
310310
const textComponent = secondLineText ? (
311311
<View style={[styles.alignItemsCenter, styles.flexColumn, styles.flexShrink1]}>
312312
{primaryText}
313-
<Text style={[isLoading && styles.opacity0, styles.pointerEventsNone, styles.fontWeightNormal, styles.textDoubleDecker]}>{secondLineText}</Text>
313+
<Text
314+
style={[
315+
isLoading && styles.opacity0,
316+
styles.pointerEventsNone,
317+
styles.fontWeightNormal,
318+
styles.textDoubleDecker,
319+
!!secondLineText && styles.textExtraSmallSupporting,
320+
styles.textWhite,
321+
styles.textBold,
322+
]}
323+
>
324+
{secondLineText}
325+
</Text>
314326
</View>
315327
) : (
316328
primaryText

src/components/ButtonWithDropdownMenu/index.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import useTheme from '@hooks/useTheme';
1313
import useThemeStyles from '@hooks/useThemeStyles';
1414
import useWindowDimensions from '@hooks/useWindowDimensions';
1515
import mergeRefs from '@libs/mergeRefs';
16+
import variables from '@styles/variables';
1617
import CONST from '@src/CONST';
1718
import type {AnchorPosition} from '@src/styles';
1819
import type {ButtonWithDropdownMenuProps} from './types';
@@ -50,7 +51,10 @@ function ButtonWithDropdownMenu<IValueType>({
5051
testID,
5152
secondLineText = '',
5253
icon,
54+
shouldPopoverUseScrollView = false,
55+
containerStyles,
5356
shouldUseModalPaddingStyle = true,
57+
shouldUseShortForm = false,
5458
shouldUseOptionIcon = false,
5559
}: ButtonWithDropdownMenuProps<IValueType>) {
5660
const theme = useTheme();
@@ -72,9 +76,14 @@ function ButtonWithDropdownMenu<IValueType>({
7276
const areAllOptionsDisabled = options.every((option) => option.disabled);
7377
const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize);
7478
const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE;
79+
const isButtonSizeSmall = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL;
7580
const nullCheckRef = (ref: RefObject<View | null>) => ref ?? null;
7681
const shouldShowButtonRightIcon = !!options.at(0)?.shouldShowButtonRightIcon;
7782

83+
useEffect(() => {
84+
setSelectedItemIndex(defaultSelectedIndex);
85+
}, [defaultSelectedIndex]);
86+
7887
useEffect(() => {
7988
if (!dropdownAnchor.current) {
8089
return;
@@ -128,6 +137,7 @@ function ButtonWithDropdownMenu<IValueType>({
128137
},
129138
);
130139
const splitButtonWrapperStyle = isSplitButton ? [styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter] : {};
140+
const isTextTooLong = customText && customText?.length > 6;
131141

132142
const handlePress = useCallback(
133143
(event?: GestureResponderEvent | KeyboardEvent) => {
@@ -157,12 +167,13 @@ function ButtonWithDropdownMenu<IValueType>({
157167
large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE}
158168
medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
159169
small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL}
160-
innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView]}
170+
innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView, isTextTooLong && shouldUseShortForm && {...styles.pl2, ...styles.pr1}]}
161171
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
162172
iconRight={Expensicons.DownArrow}
163173
shouldShowRightIcon={!isSplitButton}
164174
isSplitButton={isSplitButton}
165175
testID={testID}
176+
textStyles={[isTextTooLong && shouldUseShortForm ? {...styles.textExtraSmall, ...styles.textBold} : {}]}
166177
secondLineText={secondLineText}
167178
icon={icon}
168179
/>
@@ -178,16 +189,25 @@ function ButtonWithDropdownMenu<IValueType>({
178189
large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE}
179190
medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
180191
small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL}
181-
innerStyles={[styles.dropDownButtonCartIconContainerPadding, innerStyleDropButton]}
192+
innerStyles={[styles.dropDownButtonCartIconContainerPadding, innerStyleDropButton, isButtonSizeSmall && styles.dropDownButtonCartIcon]}
182193
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
183194
>
184195
<View style={[styles.dropDownButtonCartIconView, innerStyleDropButton]}>
185196
<View style={[success ? styles.buttonSuccessDivider : styles.buttonDivider]} />
186-
<View style={[isButtonSizeLarge ? styles.dropDownLargeButtonArrowContain : styles.dropDownMediumButtonArrowContain]}>
197+
<View
198+
style={[
199+
isButtonSizeLarge && styles.dropDownLargeButtonArrowContain,
200+
isButtonSizeSmall && shouldUseShortForm ? styles.dropDownSmallButtonArrowContain : styles.dropDownMediumButtonArrowContain,
201+
]}
202+
>
187203
<Icon
188204
medium={isButtonSizeLarge}
189-
small={!isButtonSizeLarge}
205+
small={!isButtonSizeLarge && !shouldUseShortForm}
206+
inline={shouldUseShortForm}
207+
width={shouldUseShortForm ? variables.iconSizeExtraSmall : undefined}
208+
height={shouldUseShortForm ? variables.iconSizeExtraSmall : undefined}
190209
src={Expensicons.DownArrow}
210+
additionalStyles={shouldUseShortForm ? [styles.pRelative, styles.t0] : undefined}
191211
fill={success ? theme.buttonSuccessText : theme.icon}
192212
/>
193213
</View>
@@ -237,18 +257,27 @@ function ButtonWithDropdownMenu<IValueType>({
237257
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
238258
// eslint-disable-next-line react-compiler/react-compiler
239259
anchorRef={nullCheckRef(dropdownAnchor)}
240-
withoutOverlay
241-
shouldUseScrollView
242260
scrollContainerStyle={!shouldUseModalPaddingStyle && isSmallScreenWidth && styles.pv4}
243-
shouldUseModalPaddingStyle={shouldUseModalPaddingStyle}
244261
anchorAlignment={anchorAlignment}
262+
shouldUseModalPaddingStyle={shouldUseModalPaddingStyle}
245263
headerText={menuHeaderText}
264+
shouldUseScrollView={shouldPopoverUseScrollView}
265+
containerStyles={containerStyles}
246266
menuItems={options.map((item, index) => ({
247267
...item,
248268
onSelected: item.onSelected
249-
? () => item.onSelected?.()
269+
? () => {
270+
item.onSelected?.();
271+
if (item.shouldUpdateSelectedIndex) {
272+
setSelectedItemIndex(index);
273+
}
274+
}
250275
: () => {
251276
onOptionSelected?.(item);
277+
if (item.shouldUpdateSelectedIndex === false) {
278+
return;
279+
}
280+
252281
setSelectedItemIndex(index);
253282
},
254283
shouldCallAfterModalHide: true,

src/components/ButtonWithDropdownMenu/types.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ type DropdownOption<TValueType> = {
4141
descriptionTextStyle?: StyleProp<TextStyle>;
4242
wrapperStyle?: StyleProp<ViewStyle>;
4343
displayInDefaultIconColor?: boolean;
44+
/** Whether the selected index should be updated when the option is selected even if we have onSelected callback */
45+
shouldUpdateSelectedIndex?: boolean;
4446
subMenuItems?: PopoverMenuItem[];
4547
backButtonText?: string;
4648
avatarSize?: ValueOf<typeof CONST.AVATAR_SIZE>;
@@ -141,9 +143,18 @@ type ButtonWithDropdownMenuProps<TValueType> = {
141143
/** Icon for main button */
142144
icon?: IconAsset;
143145

146+
/** Whether the popover content should be scrollable */
147+
shouldPopoverUseScrollView?: boolean;
148+
149+
/** Container style to be applied to the popover of the dropdown menu */
150+
containerStyles?: StyleProp<ViewStyle>;
151+
144152
/** Whether to use modal padding style for the popover menu */
145153
shouldUseModalPaddingStyle?: boolean;
146154

155+
/** Whether to use short form for the button */
156+
shouldUseShortForm?: boolean;
157+
147158
/** Whether to display the option icon when only one option is available */
148159
shouldUseOptionIcon?: boolean;
149160
};

src/components/KYCWall/BaseKYCWall.tsx

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,20 @@ import type {EmitterSubscription, GestureResponderEvent, View} from 'react-nativ
55
import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
66
import useOnyx from '@hooks/useOnyx';
77
import {openPersonalBankAccountSetupView} from '@libs/actions/BankAccounts';
8-
import {completePaymentOnboarding} from '@libs/actions/IOU';
8+
import {completePaymentOnboarding, savePreferredPaymentMethod} from '@libs/actions/IOU';
9+
import {moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report';
910
import getClickedTargetLocation from '@libs/getClickedTargetLocation';
1011
import Log from '@libs/Log';
1112
import Navigation from '@libs/Navigation/Navigation';
1213
import {hasExpensifyPaymentMethod} from '@libs/PaymentUtils';
13-
import {isExpenseReport as isExpenseReportReportUtils, isIOUReport} from '@libs/ReportUtils';
14+
import {getPolicyExpenseChat, isExpenseReport as isExpenseReportReportUtils, isIOUReport} from '@libs/ReportUtils';
1415
import {kycWallRef} from '@userActions/PaymentMethods';
1516
import {createWorkspaceFromIOUPayment} from '@userActions/Policy/Policy';
1617
import {setKYCWallSource} from '@userActions/Wallet';
1718
import CONST from '@src/CONST';
1819
import ONYXKEYS from '@src/ONYXKEYS';
1920
import ROUTES from '@src/ROUTES';
20-
import type {BankAccountList} from '@src/types/onyx';
21+
import type {BankAccountList, Policy} from '@src/types/onyx';
2122
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
2223
import {getEmptyObject} from '@src/types/utils/EmptyObject';
2324
import viewRef from '@src/types/utils/viewRef';
@@ -102,24 +103,48 @@ function KYCWall({
102103
}, [getAnchorPosition]);
103104

104105
const selectPaymentMethod = useCallback(
105-
(paymentMethod: PaymentMethod) => {
106-
onSelectPaymentMethod(paymentMethod);
106+
(paymentMethod?: PaymentMethod, policy?: Policy) => {
107+
if (paymentMethod) {
108+
onSelectPaymentMethod(paymentMethod);
109+
}
107110

108111
if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
109112
openPersonalBankAccountSetupView();
110113
} else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) {
111114
Navigation.navigate(addDebitCardRoute ?? ROUTES.HOME);
112-
} else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) {
115+
} else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT || policy) {
113116
if (iouReport && isIOUReport(iouReport)) {
117+
if (policy) {
118+
const policyExpenseChatReportID = getPolicyExpenseChat(iouReport.ownerAccountID, policy.id)?.reportID;
119+
if (!policyExpenseChatReportID) {
120+
const {policyExpenseChatReportID: newPolicyExpenseChatReportID} = moveIOUReportToPolicyAndInviteSubmitter(iouReport.reportID, policy.id) ?? {};
121+
savePreferredPaymentMethod(iouReport.policyID, policy.id, CONST.LAST_PAYMENT_METHOD.IOU);
122+
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(newPolicyExpenseChatReportID));
123+
} else {
124+
moveIOUReportToPolicy(iouReport.reportID, policy.id, true);
125+
savePreferredPaymentMethod(iouReport.policyID, policy.id, CONST.LAST_PAYMENT_METHOD.IOU);
126+
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(policyExpenseChatReportID));
127+
}
128+
129+
if (policy?.achAccount) {
130+
return;
131+
}
132+
// Navigate to the bank account set up flow for this specific policy
133+
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policy.id));
134+
return;
135+
}
136+
114137
const {policyID, workspaceChatReportID, reportPreviewReportActionID, adminsChatReportID} = createWorkspaceFromIOUPayment(iouReport) ?? {};
138+
if (policyID) {
139+
savePreferredPaymentMethod(iouReport.policyID, policyID, CONST.LAST_PAYMENT_METHOD.IOU);
140+
}
115141
completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, adminsChatReportID, policyID);
116142
if (workspaceChatReportID) {
117143
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(workspaceChatReportID, reportPreviewReportActionID));
118144
}
119145

120146
// Navigate to the bank account set up flow for this specific policy
121147
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID));
122-
123148
return;
124149
}
125150
Navigation.navigate(addBankAccountRoute);
@@ -135,7 +160,7 @@ function KYCWall({
135160
*
136161
*/
137162
const continueAction = useCallback(
138-
(event?: GestureResponderEvent | KeyboardEvent, iouPaymentType?: PaymentMethodType) => {
163+
(event?: GestureResponderEvent | KeyboardEvent, iouPaymentType?: PaymentMethodType, paymentMethod?: PaymentMethod, policy?: Policy) => {
139164
const currentSource = walletTerms?.source ?? source;
140165

141166
/**
@@ -169,6 +194,19 @@ function KYCWall({
169194
return;
170195
}
171196

197+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
198+
if (paymentMethod || policy) {
199+
setShouldShowAddPaymentMenu(false);
200+
selectPaymentMethod(paymentMethod, policy);
201+
return;
202+
}
203+
204+
if (iouPaymentType && isExpenseReport) {
205+
setShouldShowAddPaymentMenu(false);
206+
selectPaymentMethod(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT);
207+
return;
208+
}
209+
172210
const clickedElementLocation = getClickedTargetLocation(targetElement as HTMLDivElement);
173211
const position = getAnchorPosition(clickedElementLocation);
174212

@@ -181,13 +219,20 @@ function KYCWall({
181219
// Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks
182220
const hasActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName);
183221

184-
if (!hasActivatedWallet) {
222+
if (!hasActivatedWallet && !policy) {
185223
Log.info('[KYC Wallet] User does not have active wallet');
186224

187225
Navigation.navigate(enablePaymentsRoute);
188226

189227
return;
190228
}
229+
230+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
231+
if (paymentMethod || policy) {
232+
setShouldShowAddPaymentMenu(false);
233+
selectPaymentMethod(paymentMethod, policy);
234+
return;
235+
}
191236
}
192237

193238
Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them');

src/components/KYCWall/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx';
44
import type {ValueOf} from 'type-fest';
55
import type CONST from '@src/CONST';
66
import type {Route} from '@src/ROUTES';
7-
import type {Report} from '@src/types/onyx';
7+
import type {Policy, Report} from '@src/types/onyx';
88
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
99
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
1010

@@ -63,6 +63,9 @@ type KYCWallProps = {
6363

6464
/** Children to build the KYC */
6565
children: (continueAction: (event: GestureResponderEvent | KeyboardEvent | undefined, method?: PaymentMethodType) => void, anchorRef: RefObject<View | null>) => void;
66+
67+
/** The policy used for payment */
68+
policy?: Policy;
6669
};
6770

6871
export type {AnchorPosition, KYCWallProps, PaymentMethod, DomRect, PaymentMethodType, Source};

src/components/MoneyReportHeader.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ function MoneyReportHeader({
332332
payInvoice(type, chatReport, moneyRequestReport, payAsBusiness, methodID, paymentMethod);
333333
} else {
334334
startAnimation();
335-
payMoneyRequest(type, chatReport, moneyRequestReport, true);
335+
payMoneyRequest(type, chatReport, moneyRequestReport, undefined, true);
336336
}
337337
},
338338
[chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, showDelegateNoAccessModal, isInvoiceReport, moneyRequestReport, startAnimation],
@@ -543,6 +543,7 @@ function MoneyReportHeader({
543543
isPaidAnimationRunning={isPaidAnimationRunning}
544544
isApprovedAnimationRunning={isApprovedAnimationRunning}
545545
onAnimationFinish={stopAnimation}
546+
formattedAmount={totalAmount}
546547
canIOUBePaid
547548
onlyShowPayElsewhere={onlyShowPayElsewhere}
548549
currency={moneyRequestReport?.currency}

src/components/PopoverMenu.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -312,13 +312,10 @@ function PopoverMenu({
312312
}
313313
setFocusedIndex(menuIndex);
314314
}}
315-
wrapperStyle={StyleUtils.getItemBackgroundColorStyle(
316-
!!item.isSelected,
317-
focusedIndex === menuIndex,
318-
item.disabled ?? false,
319-
theme.activeComponentBG,
320-
theme.hoverComponentBG,
321-
)}
315+
wrapperStyle={[
316+
StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, focusedIndex === menuIndex, item.disabled ?? false, theme.activeComponentBG, theme.hoverComponentBG),
317+
shouldUseScrollView && StyleUtils.getOptionMargin(menuIndex, currentMenuItems.length - 1),
318+
]}
322319
shouldRemoveHoverBackground={item.isSelected}
323320
titleStyle={StyleSheet.flatten([styles.flex1, item.titleStyle])}
324321
// Spread other props dynamically

src/components/ProcessMoneyReportHoldMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ function ProcessMoneyReportHoldMenu({
7777
if (startAnimation) {
7878
startAnimation();
7979
}
80-
payMoneyRequest(paymentType, chatReport, moneyRequestReport, full);
80+
payMoneyRequest(paymentType, chatReport, moneyRequestReport, undefined, full);
8181
}
8282
onClose();
8383
};

0 commit comments

Comments
 (0)