diff --git a/assets/images/document-check.svg b/assets/images/document-check.svg
new file mode 100644
index 000000000000..1364765e7e70
--- /dev/null
+++ b/assets/images/document-check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx
index 56e8a81e0033..b05e44daaf39 100644
--- a/src/components/ButtonWithDropdownMenu/index.tsx
+++ b/src/components/ButtonWithDropdownMenu/index.tsx
@@ -59,8 +59,10 @@ function ButtonWithDropdownMenu({ref, ...props}: ButtonWithDropdownM
shouldUseModalPaddingStyle = true,
shouldUseShortForm = false,
shouldUseOptionIcon = false,
+ headerTextStyles,
shouldStayNormalOnDisable = false,
sentryLabel,
+ shouldPutHeaderTextAfterBackButton = false,
} = props;
const icons = useMemoizedLazyExpensifyIcons(['DownArrow']);
@@ -273,6 +275,7 @@ function ButtonWithDropdownMenu({ref, ...props}: ButtonWithDropdownM
setIsMenuVisible(false);
}
}}
+ headerStyles={headerTextStyles}
anchorPosition={popoverAnchorPosition}
shouldShowSelectedItemCheck={shouldShowSelectedItemCheck}
anchorRef={nullCheckRef(dropdownAnchor)}
@@ -302,6 +305,7 @@ function ButtonWithDropdownMenu({ref, ...props}: ButtonWithDropdownM
shouldCallAfterModalHide: true,
subMenuItems: item.subMenuItems?.map((subItem) => ({...subItem, shouldCallAfterModalHide: true})),
}))}
+ shouldPutHeaderTextAfterBackButton={shouldPutHeaderTextAfterBackButton}
/>
)}
diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts
index 9120a183d6c7..216b6a5896f2 100644
--- a/src/components/ButtonWithDropdownMenu/types.ts
+++ b/src/components/ButtonWithDropdownMenu/types.ts
@@ -158,8 +158,14 @@ type ButtonWithDropdownMenuProps = WithSentryLabel & {
/** Whether to display the option icon when only one option is available */
shouldUseOptionIcon?: boolean;
+ /** Used to apply styles specifically to the header text */
+ headerTextStyles?: StyleProp;
+
/** Reference to the outer element */
ref?: React.Ref;
+
+ /** Whether to put the header text after the back button */
+ shouldPutHeaderTextAfterBackButton?: boolean;
};
type ButtonWithDropdownMenuRef = {
diff --git a/src/components/ExpenseHeaderApprovalButton.tsx b/src/components/ExpenseHeaderApprovalButton.tsx
new file mode 100644
index 000000000000..8a7536b8e6e0
--- /dev/null
+++ b/src/components/ExpenseHeaderApprovalButton.tsx
@@ -0,0 +1,151 @@
+import React from 'react';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import type IconAsset from '@src/types/utils/IconAsset';
+import Button from './Button';
+import ButtonWithDropdownMenu from './ButtonWithDropdownMenu';
+import type {LocaleContextProps} from './LocaleContextProvider';
+
+type ExpenseHeaderApprovalButtonProps = {
+ /** Whether any transaction is on hold */
+ isAnyTransactionOnHold: boolean;
+
+ /** Whether delegate access is restricted */
+ isDelegateAccessRestricted: boolean;
+
+ /** Whether the report has only held expenses */
+ hasOnlyHeldExpenses: boolean;
+
+ /** Whether there is a valid non-held amount */
+ hasValidNonHeldAmount: boolean;
+
+ /** The non-held amount string */
+ nonHeldAmount: string | undefined;
+
+ /** The full amount string */
+ fullAmount: string;
+
+ /** Callback when approval is confirmed */
+ onApprove: (isFullApproval: boolean) => void;
+
+ /** Whether the button is disabled */
+ isDisabled?: boolean;
+};
+
+type ApprovalOption = {
+ value: string;
+ text: string;
+ icon: IconAsset;
+ onSelected: () => void;
+ keyForList: string;
+};
+
+type ApprovalDropdownOptionProps = {
+ nonHeldAmount: string | undefined;
+ fullAmount: string;
+ hasValidNonHeldAmount: boolean;
+ hasOnlyHeldExpenses: boolean;
+ onPartialApprove: () => void;
+ onFullApprove: () => void;
+ translate: LocaleContextProps['translate'];
+ illustrations: Record<'ThumbsUp' | 'DocumentCheck', IconAsset>;
+};
+
+/**
+ * Generates dropdown options for approve button when there are held expenses
+ */
+function getApprovalDropdownOptions({
+ nonHeldAmount,
+ fullAmount,
+ hasValidNonHeldAmount,
+ hasOnlyHeldExpenses,
+ onPartialApprove,
+ onFullApprove,
+ translate,
+ illustrations,
+}: ApprovalDropdownOptionProps): ApprovalOption[] {
+ const APPROVE_PARTIAL = 'approve_partial';
+ const APPROVE_FULL = 'approve_full';
+ const options: ApprovalOption[] = [];
+
+ if (nonHeldAmount && hasValidNonHeldAmount && !hasOnlyHeldExpenses) {
+ options.push({
+ value: APPROVE_PARTIAL,
+ text: `${translate('iou.approveOnly')} ${nonHeldAmount}`,
+ icon: illustrations.ThumbsUp,
+ onSelected: onPartialApprove,
+ keyForList: APPROVE_PARTIAL,
+ });
+ }
+
+ options.push({
+ value: APPROVE_FULL,
+ text: `${translate('iou.approve')} ${fullAmount}`,
+ icon: illustrations.DocumentCheck,
+ onSelected: onFullApprove,
+ keyForList: APPROVE_FULL,
+ });
+
+ return options;
+}
+
+function ExpenseHeaderApprovalButton({
+ isAnyTransactionOnHold,
+ isDelegateAccessRestricted,
+ hasOnlyHeldExpenses,
+ hasValidNonHeldAmount,
+ nonHeldAmount,
+ fullAmount,
+ onApprove,
+ isDisabled = false,
+}: ExpenseHeaderApprovalButtonProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const illustrations = useMemoizedLazyExpensifyIcons(['ThumbsUp', 'DocumentCheck']);
+
+ const shouldShowDropdown = isAnyTransactionOnHold && !isDelegateAccessRestricted;
+
+ if (shouldShowDropdown) {
+ const approvalOptions = getApprovalDropdownOptions({
+ nonHeldAmount: !hasOnlyHeldExpenses && hasValidNonHeldAmount ? nonHeldAmount : undefined,
+ fullAmount,
+ hasValidNonHeldAmount,
+ hasOnlyHeldExpenses,
+ onPartialApprove: () => onApprove(false),
+ onFullApprove: () => onApprove(true),
+ translate,
+ illustrations,
+ });
+
+ if (approvalOptions.length > 1) {
+ return (
+ {}}
+ customText={translate('iou.approve')}
+ headerTextStyles={styles.lineHeightNormal}
+ shouldAlwaysShowDropdownMenu
+ isSplitButton={false}
+ isDisabled={isDisabled}
+ />
+ );
+ }
+ }
+
+ return (
+