Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
df28f4b
revert: revert standardize pay button
getusha Jul 1, 2025
4ba0311
fix #65164: use snapshot data if iouReport is undefined
getusha Jul 1, 2025
fd0d2bd
fix #65129: update copies
getusha Jul 1, 2025
23ed42b
fix #65141: fix unresponsive moneyrequestheader pay option
getusha Jul 1, 2025
f93b464
fix #65152: fix inconsistent bank icon
getusha Jul 2, 2025
e220302
fix #65118: fix unresponsive pay action on search
getusha Jul 2, 2025
0eebeb6
fix #65120: disable action on offline reports
getusha Jul 2, 2025
0653418
fix #65125: apply short form for elsewhere single action
getusha Jul 2, 2025
ffd0264
Merge remote-tracking branch 'exfy/main' into re-feat-standardize-pay…
getusha Jul 2, 2025
a8cb91e
fix lint, ts-check and remove new deprecated function usage
getusha Jul 2, 2025
60fde77
fix: remove submodule change
getusha Jul 2, 2025
d45a56f
fix: even out action sizes
getusha Jul 2, 2025
4e454da
Merge remote-tracking branch 'exfy/main' into re-feat-standardize-pay…
getusha Jul 2, 2025
e126b1e
fix: unused type
getusha Jul 2, 2025
e51af69
fix: adjust font sizes for pay action in search
getusha Jul 3, 2025
537d232
fix: conflicts
getusha Jul 3, 2025
f5675ec
fix: lint
getusha Jul 3, 2025
c17dd69
fix: prettier
getusha Jul 3, 2025
1ffbe0b
fix: minor improvements
getusha Jul 3, 2025
cb5cc9a
fix: address issues and adjust design
getusha Jul 7, 2025
1587e42
Merge remote-tracking branch 'exfy/main' into re-feat-standardize-pay…
getusha Jul 7, 2025
f7e6f34
Update src/components/ButtonWithDropdownMenu/index.tsx
getusha Jul 8, 2025
c397198
Merge remote-tracking branch 'exfy/main' into re-feat-standardize-pay…
getusha Jul 8, 2025
56f055d
fix: address bugs
getusha Jul 9, 2025
ba9adc5
Merge remote-tracking branch 'exfy/main' into re-feat-standardize-pay…
getusha Jul 9, 2025
52e7e43
Merge remote-tracking branch 'exfy/main' into re-feat-standardize-pay…
getusha Jul 9, 2025
a185505
fix: lint
getusha Jul 9, 2025
a333977
fix: address minor issues
getusha Jul 9, 2025
812b180
fix: prettier
getusha Jul 9, 2025
e2de6de
fix: apply suggestion
getusha Jul 10, 2025
d93a604
Merge remote-tracking branch 'exfy/main' into re-feat-standardize-pay…
getusha Jul 10, 2025
7f526d2
fix: lint
getusha Jul 10, 2025
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
6 changes: 3 additions & 3 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6845,9 +6845,9 @@ const CONST = {
},
LAST_PAYMENT_METHOD: {
LAST_USED: 'lastUsed',
IOU: 'Iou',
EXPENSE: 'Expense',
INVOICE: 'Invoice',
IOU: 'iou',
EXPENSE: 'expense',
INVOICE: 'invoice',
},
SKIPPABLE_COLLECTION_MEMBER_IDS: [String(DEFAULT_NUMBER_ID), '-1', 'undefined', 'null', 'NaN'] as string[],
SETUP_SPECIALIST_LOGIN: 'Setup Specialist',
Expand Down
14 changes: 13 additions & 1 deletion src/components/Button/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,19 @@ function Button(
const textComponent = secondLineText ? (
<View style={[styles.alignItemsCenter, styles.flexColumn, styles.flexShrink1]}>
{primaryText}
<Text style={[isLoading && styles.opacity0, styles.pointerEventsNone, styles.fontWeightNormal, styles.textDoubleDecker]}>{secondLineText}</Text>
<Text
style={[
isLoading && styles.opacity0,
styles.pointerEventsNone,
styles.fontWeightNormal,
styles.textDoubleDecker,
!!secondLineText && styles.textExtraSmallSupporting,
styles.textWhite,
styles.textBold,
]}
>
{secondLineText}
</Text>
</View>
) : (
primaryText
Expand Down
45 changes: 37 additions & 8 deletions src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import mergeRefs from '@libs/mergeRefs';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {AnchorPosition} from '@src/styles';
import type {ButtonWithDropdownMenuProps} from './types';
Expand Down Expand Up @@ -50,7 +51,10 @@ function ButtonWithDropdownMenu<IValueType>({
testID,
secondLineText = '',
icon,
shouldPopoverUseScrollView = false,
containerStyles,
shouldUseModalPaddingStyle = true,
shouldUseShortForm = false,
shouldUseOptionIcon = false,
}: ButtonWithDropdownMenuProps<IValueType>) {
const theme = useTheme();
Expand All @@ -72,9 +76,14 @@ function ButtonWithDropdownMenu<IValueType>({
const areAllOptionsDisabled = options.every((option) => option.disabled);
const innerStyleDropButton = StyleUtils.getDropDownButtonHeight(buttonSize);
const isButtonSizeLarge = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE;
const isButtonSizeSmall = buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL;
const nullCheckRef = (ref: RefObject<View | null>) => ref ?? null;
const shouldShowButtonRightIcon = !!options.at(0)?.shouldShowButtonRightIcon;

useEffect(() => {
setSelectedItemIndex(defaultSelectedIndex);
}, [defaultSelectedIndex]);

useEffect(() => {
if (!dropdownAnchor.current) {
return;
Expand Down Expand Up @@ -128,6 +137,7 @@ function ButtonWithDropdownMenu<IValueType>({
},
);
const splitButtonWrapperStyle = isSplitButton ? [styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter] : {};
const isTextTooLong = customText && customText?.length > 6;

const handlePress = useCallback(
(event?: GestureResponderEvent | KeyboardEvent) => {
Expand Down Expand Up @@ -157,12 +167,13 @@ function ButtonWithDropdownMenu<IValueType>({
large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE}
medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL}
innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView]}
innerStyles={[innerStyleDropButton, !isSplitButton && styles.dropDownButtonCartIconView, isTextTooLong && shouldUseShortForm && {...styles.pl2, ...styles.pr1}]}
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
iconRight={Expensicons.DownArrow}
shouldShowRightIcon={!isSplitButton}
isSplitButton={isSplitButton}
testID={testID}
textStyles={[isTextTooLong && shouldUseShortForm ? {...styles.textExtraSmall, ...styles.textBold} : {}]}
secondLineText={secondLineText}
icon={icon}
/>
Expand All @@ -178,16 +189,25 @@ function ButtonWithDropdownMenu<IValueType>({
large={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.LARGE}
medium={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.MEDIUM}
small={buttonSize === CONST.DROPDOWN_BUTTON_SIZE.SMALL}
innerStyles={[styles.dropDownButtonCartIconContainerPadding, innerStyleDropButton]}
innerStyles={[styles.dropDownButtonCartIconContainerPadding, innerStyleDropButton, isButtonSizeSmall && styles.dropDownButtonCartIcon]}
enterKeyEventListenerPriority={enterKeyEventListenerPriority}
>
<View style={[styles.dropDownButtonCartIconView, innerStyleDropButton]}>
<View style={[success ? styles.buttonSuccessDivider : styles.buttonDivider]} />
<View style={[isButtonSizeLarge ? styles.dropDownLargeButtonArrowContain : styles.dropDownMediumButtonArrowContain]}>
<View
style={[
isButtonSizeLarge && styles.dropDownLargeButtonArrowContain,
isButtonSizeSmall && shouldUseShortForm ? styles.dropDownSmallButtonArrowContain : styles.dropDownMediumButtonArrowContain,
]}
>
<Icon
medium={isButtonSizeLarge}
small={!isButtonSizeLarge}
small={!isButtonSizeLarge && !shouldUseShortForm}
inline={shouldUseShortForm}
width={shouldUseShortForm ? variables.iconSizeExtraSmall : undefined}
height={shouldUseShortForm ? variables.iconSizeExtraSmall : undefined}
src={Expensicons.DownArrow}
additionalStyles={shouldUseShortForm ? [styles.pRelative, styles.t0] : undefined}
fill={success ? theme.buttonSuccessText : theme.icon}
/>
</View>
Expand Down Expand Up @@ -237,18 +257,27 @@ function ButtonWithDropdownMenu<IValueType>({
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
// eslint-disable-next-line react-compiler/react-compiler
anchorRef={nullCheckRef(dropdownAnchor)}
withoutOverlay
shouldUseScrollView
scrollContainerStyle={!shouldUseModalPaddingStyle && isSmallScreenWidth && styles.pv4}
shouldUseModalPaddingStyle={shouldUseModalPaddingStyle}
anchorAlignment={anchorAlignment}
shouldUseModalPaddingStyle={shouldUseModalPaddingStyle}
headerText={menuHeaderText}
shouldUseScrollView={shouldPopoverUseScrollView}
containerStyles={containerStyles}
menuItems={options.map((item, index) => ({
...item,
onSelected: item.onSelected
? () => item.onSelected?.()
? () => {
item.onSelected?.();
if (item.shouldUpdateSelectedIndex) {
setSelectedItemIndex(index);
}
}
: () => {
onOptionSelected?.(item);
if (item.shouldUpdateSelectedIndex === false) {
return;
}

setSelectedItemIndex(index);
},
shouldCallAfterModalHide: true,
Expand Down
11 changes: 11 additions & 0 deletions src/components/ButtonWithDropdownMenu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ type DropdownOption<TValueType> = {
descriptionTextStyle?: StyleProp<TextStyle>;
wrapperStyle?: StyleProp<ViewStyle>;
displayInDefaultIconColor?: boolean;
/** Whether the selected index should be updated when the option is selected even if we have onSelected callback */
shouldUpdateSelectedIndex?: boolean;
subMenuItems?: PopoverMenuItem[];
backButtonText?: string;
avatarSize?: ValueOf<typeof CONST.AVATAR_SIZE>;
Expand Down Expand Up @@ -141,9 +143,18 @@ type ButtonWithDropdownMenuProps<TValueType> = {
/** Icon for main button */
icon?: IconAsset;

/** Whether the popover content should be scrollable */
shouldPopoverUseScrollView?: boolean;

/** Container style to be applied to the popover of the dropdown menu */
containerStyles?: StyleProp<ViewStyle>;

/** Whether to use modal padding style for the popover menu */
shouldUseModalPaddingStyle?: boolean;

/** Whether to use short form for the button */
shouldUseShortForm?: boolean;

/** Whether to display the option icon when only one option is available */
shouldUseOptionIcon?: boolean;
};
Expand Down
63 changes: 54 additions & 9 deletions src/components/KYCWall/BaseKYCWall.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,20 @@ import type {EmitterSubscription, GestureResponderEvent, View} from 'react-nativ
import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
import useOnyx from '@hooks/useOnyx';
import {openPersonalBankAccountSetupView} from '@libs/actions/BankAccounts';
import {completePaymentOnboarding} from '@libs/actions/IOU';
import {completePaymentOnboarding, savePreferredPaymentMethod} from '@libs/actions/IOU';
import {moveIOUReportToPolicy, moveIOUReportToPolicyAndInviteSubmitter} from '@libs/actions/Report';
import getClickedTargetLocation from '@libs/getClickedTargetLocation';
import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import {hasExpensifyPaymentMethod} from '@libs/PaymentUtils';
import {isExpenseReport as isExpenseReportReportUtils, isIOUReport} from '@libs/ReportUtils';
import {getPolicyExpenseChat, isExpenseReport as isExpenseReportReportUtils, isIOUReport} from '@libs/ReportUtils';
import {kycWallRef} from '@userActions/PaymentMethods';
import {createWorkspaceFromIOUPayment} from '@userActions/Policy/Policy';
import {setKYCWallSource} from '@userActions/Wallet';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {BankAccountList} from '@src/types/onyx';
import type {BankAccountList, Policy} from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import {getEmptyObject} from '@src/types/utils/EmptyObject';
import viewRef from '@src/types/utils/viewRef';
Expand Down Expand Up @@ -102,24 +103,48 @@ function KYCWall({
}, [getAnchorPosition]);

const selectPaymentMethod = useCallback(
(paymentMethod: PaymentMethod) => {
onSelectPaymentMethod(paymentMethod);
(paymentMethod?: PaymentMethod, policy?: Policy) => {
if (paymentMethod) {
onSelectPaymentMethod(paymentMethod);
}

if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
openPersonalBankAccountSetupView();
} else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) {
Navigation.navigate(addDebitCardRoute ?? ROUTES.HOME);
} else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) {
} else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT || policy) {
if (iouReport && isIOUReport(iouReport)) {
if (policy) {
const policyExpenseChatReportID = getPolicyExpenseChat(iouReport.ownerAccountID, policy.id)?.reportID;
if (!policyExpenseChatReportID) {
const {policyExpenseChatReportID: newPolicyExpenseChatReportID} = moveIOUReportToPolicyAndInviteSubmitter(iouReport.reportID, policy.id) ?? {};
savePreferredPaymentMethod(iouReport.policyID, policy.id, CONST.LAST_PAYMENT_METHOD.IOU);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(newPolicyExpenseChatReportID));
} else {
moveIOUReportToPolicy(iouReport.reportID, policy.id, true);
savePreferredPaymentMethod(iouReport.policyID, policy.id, CONST.LAST_PAYMENT_METHOD.IOU);
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(policyExpenseChatReportID));
}

if (policy?.achAccount) {
return;
}
// Navigate to the bank account set up flow for this specific policy
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policy.id));
return;
}

const {policyID, workspaceChatReportID, reportPreviewReportActionID, adminsChatReportID} = createWorkspaceFromIOUPayment(iouReport) ?? {};
if (policyID) {
savePreferredPaymentMethod(iouReport.policyID, policyID, CONST.LAST_PAYMENT_METHOD.IOU);
}
completePaymentOnboarding(CONST.PAYMENT_SELECTED.BBA, adminsChatReportID, policyID);
if (workspaceChatReportID) {
Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(workspaceChatReportID, reportPreviewReportActionID));
}

// Navigate to the bank account set up flow for this specific policy
Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(policyID));

return;
}
Navigation.navigate(addBankAccountRoute);
Expand All @@ -135,7 +160,7 @@ function KYCWall({
*
*/
const continueAction = useCallback(
(event?: GestureResponderEvent | KeyboardEvent, iouPaymentType?: PaymentMethodType) => {
(event?: GestureResponderEvent | KeyboardEvent, iouPaymentType?: PaymentMethodType, paymentMethod?: PaymentMethod, policy?: Policy) => {
const currentSource = walletTerms?.source ?? source;

/**
Expand Down Expand Up @@ -169,6 +194,19 @@ function KYCWall({
return;
}

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (paymentMethod || policy) {
setShouldShowAddPaymentMenu(false);
selectPaymentMethod(paymentMethod, policy);
return;
}

if (iouPaymentType && isExpenseReport) {
setShouldShowAddPaymentMenu(false);
selectPaymentMethod(CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT);
return;
}

const clickedElementLocation = getClickedTargetLocation(targetElement as HTMLDivElement);
const position = getAnchorPosition(clickedElementLocation);

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

if (!hasActivatedWallet) {
if (!hasActivatedWallet && !policy) {
Log.info('[KYC Wallet] User does not have active wallet');

Navigation.navigate(enablePaymentsRoute);

return;
}

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
if (paymentMethod || policy) {
setShouldShowAddPaymentMenu(false);
selectPaymentMethod(paymentMethod, policy);
return;
}
}

Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them');
Expand Down
5 changes: 4 additions & 1 deletion src/components/KYCWall/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import type {Route} from '@src/ROUTES';
import type {Report} from '@src/types/onyx';
import type {Policy, Report} from '@src/types/onyx';
import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';

Expand Down Expand Up @@ -63,6 +63,9 @@ type KYCWallProps = {

/** Children to build the KYC */
children: (continueAction: (event: GestureResponderEvent | KeyboardEvent | undefined, method?: PaymentMethodType) => void, anchorRef: RefObject<View | null>) => void;

/** The policy used for payment */
policy?: Policy;
};

export type {AnchorPosition, KYCWallProps, PaymentMethod, DomRect, PaymentMethodType, Source};
3 changes: 2 additions & 1 deletion src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ function MoneyReportHeader({
payInvoice(type, chatReport, moneyRequestReport, payAsBusiness, methodID, paymentMethod);
} else {
startAnimation();
payMoneyRequest(type, chatReport, moneyRequestReport, true);
payMoneyRequest(type, chatReport, moneyRequestReport, undefined, true);
}
},
[chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, showDelegateNoAccessModal, isInvoiceReport, moneyRequestReport, startAnimation],
Expand Down Expand Up @@ -543,6 +543,7 @@ function MoneyReportHeader({
isPaidAnimationRunning={isPaidAnimationRunning}
isApprovedAnimationRunning={isApprovedAnimationRunning}
onAnimationFinish={stopAnimation}
formattedAmount={totalAmount}
canIOUBePaid
onlyShowPayElsewhere={onlyShowPayElsewhere}
currency={moneyRequestReport?.currency}
Expand Down
11 changes: 4 additions & 7 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,10 @@ function PopoverMenu({
}
setFocusedIndex(menuIndex);
}}
wrapperStyle={StyleUtils.getItemBackgroundColorStyle(
!!item.isSelected,
focusedIndex === menuIndex,
item.disabled ?? false,
theme.activeComponentBG,
theme.hoverComponentBG,
)}
wrapperStyle={[
StyleUtils.getItemBackgroundColorStyle(!!item.isSelected, focusedIndex === menuIndex, item.disabled ?? false, theme.activeComponentBG, theme.hoverComponentBG),
shouldUseScrollView && StyleUtils.getOptionMargin(menuIndex, currentMenuItems.length - 1),
]}
shouldRemoveHoverBackground={item.isSelected}
titleStyle={StyleSheet.flatten([styles.flex1, item.titleStyle])}
// Spread other props dynamically
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProcessMoneyReportHoldMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ function ProcessMoneyReportHoldMenu({
if (startAnimation) {
startAnimation();
}
payMoneyRequest(paymentType, chatReport, moneyRequestReport, full);
payMoneyRequest(paymentType, chatReport, moneyRequestReport, undefined, full);
}
onClose();
};
Expand Down
Loading
Loading