From 43857d6dc4457610a4171cb8eb0c9d6bebcf8186 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Tue, 3 Dec 2024 17:50:32 +0100
Subject: [PATCH 01/37] Animate payment icons in money request preview

---
 .../MoneyRequestPreviewContent.tsx            | 58 ++++++++++++--
 .../PaymentCompleteAnimation.tsx              | 75 +++++++++++++++++++
 2 files changed, 125 insertions(+), 8 deletions(-)
 create mode 100644 src/components/SettlementButton/PaymentCompleteAnimation.tsx

diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index b577f2be5337..03362d1dbdb0 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -1,11 +1,13 @@
-import {useRoute} from '@react-navigation/native';
+import { useRoute } from '@react-navigation/native';
 import lodashSortBy from 'lodash/sortBy';
 import truncate from 'lodash/truncate';
-import React, {useMemo} from 'react';
-import {View} from 'react-native';
+import { default as React, useEffect, useMemo, useRef, useState } from 'react';
+import { View } from 'react-native';
+
 import type {GestureResponderEvent} from 'react-native';
-import {useOnyx} from 'react-native-onyx';
 import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import Animated, {useAnimatedStyle, useSharedValue, withDelay, withSpring} from 'react-native-reanimated';
 import Button from '@components/Button';
 import Icon from '@components/Icon';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -15,6 +17,7 @@ import MultipleAvatars from '@components/MultipleAvatars';
 import OfflineWithFeedback from '@components/OfflineWithFeedback';
 import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
 import ReportActionItemImages from '@components/ReportActionItem/ReportActionItemImages';
+import PaymentCompleteAnimation from '@components/SettlementButton/PaymentCompleteAnimation';
 import {showContextMenuForReport} from '@components/ShowContextMenuContext';
 import Text from '@components/Text';
 import useLocalize from '@hooks/useLocalize';
@@ -34,8 +37,8 @@ import * as OptionsListUtils from '@libs/OptionsListUtils';
 import * as PolicyUtils from '@libs/PolicyUtils';
 import * as ReceiptUtils from '@libs/ReceiptUtils';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportUtils from '@libs/ReportUtils';
 import type {TransactionDetails} from '@libs/ReportUtils';
+import * as ReportUtils from '@libs/ReportUtils';
 import StringUtils from '@libs/StringUtils';
 import * as TransactionUtils from '@libs/TransactionUtils';
 import ViolationsUtils from '@libs/Violations/ViolationsUtils';
@@ -77,6 +80,7 @@ function MoneyRequestPreviewContent({
     const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || '-1'}`);
     const [session] = useOnyx(ONYXKEYS.SESSION);
     const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || '-1'}`);
+    const [isPaymentAnimationVisible, setIsPaymentAnimationVisible] = useState(false);
 
     const policy = PolicyUtils.getPolicy(iouReport?.policyID);
     const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action);
@@ -156,6 +160,40 @@ function MoneyRequestPreviewContent({
     const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`);
     const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? '');
     const reviewingTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1';
+    const shouldShowPaidIcon = ReportUtils.isSettled(iouReport?.reportID) && !isPartialHold && !isBillSplit;
+
+    const checkMarkScale = useSharedValue(shouldShowPaidIcon ? 1 : 0);
+
+    const checkMarkStyle = useAnimatedStyle(() => ({
+        ...styles.defaultCheckmarkWrapper,
+        transform: [{scale: checkMarkScale.value}],
+    }));
+
+    const handleAnimationFinish = () => {
+        setIsPaymentAnimationVisible(false);
+    };
+
+    // Track previous shouldShowPaidIcon value
+    const prevIsSettledRef = useRef(shouldShowPaidIcon);
+
+    useEffect(() => {
+        if (shouldShowPaidIcon && !prevIsSettledRef.current) {
+            // Start the checkmark animation
+            checkMarkScale.value = withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION}));
+
+            // Start the payment complete animation
+            setIsPaymentAnimationVisible(true);
+        } else if (shouldShowPaidIcon) {
+            // Ensure the checkmark is visible without animation if already settled
+            checkMarkScale.value = 1;
+        } else {
+            // Hide the checkmark if not settled
+            checkMarkScale.value = 0;
+        }
+
+        // Update the previous value
+        prevIsSettledRef.current = shouldShowPaidIcon;
+    }, [checkMarkScale, shouldShowPaidIcon]);
 
     /*
      Show the merchant for IOUs and expenses only if:
@@ -387,13 +425,13 @@ function MoneyRequestPreviewContent({
                                                 >
                                                     {displayAmount}
                                                 </Text>
-                                                {ReportUtils.isSettled(iouReport?.reportID) && !isPartialHold && !isBillSplit && (
-                                                    <View style={styles.defaultCheckmarkWrapper}>
+                                                {shouldShowPaidIcon && (
+                                                    <Animated.View style={[checkMarkStyle, styles.defaultCheckmarkWrapper]}>
                                                         <Icon
                                                             src={Expensicons.Checkmark}
                                                             fill={theme.iconSuccessFill}
                                                         />
-                                                    </View>
+                                                    </Animated.View>
                                                 )}
                                             </View>
                                             {isBillSplit && (
@@ -433,6 +471,10 @@ function MoneyRequestPreviewContent({
                                                 <Text style={[styles.textMicroSupporting, styles.ml1, styles.amountSplitPadding]}>{pendingMessageProps.messageDescription}</Text>
                                             </View>
                                         )}
+                                        <PaymentCompleteAnimation
+                                            isVisible={isPaymentAnimationVisible}
+                                            onAnimationFinish={handleAnimationFinish}
+                                        />
                                     </View>
                                     {shouldShowCategoryOrTag && <View style={[styles.threadDividerLine, styles.ml0, styles.mr0, styles.mt1]} />}
                                     {shouldShowCategoryOrTag && (
diff --git a/src/components/SettlementButton/PaymentCompleteAnimation.tsx b/src/components/SettlementButton/PaymentCompleteAnimation.tsx
new file mode 100644
index 000000000000..b23d587c56d6
--- /dev/null
+++ b/src/components/SettlementButton/PaymentCompleteAnimation.tsx
@@ -0,0 +1,75 @@
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import variables from '@styles/variables';
+import React, { useEffect } from 'react';
+import Animated, { useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated';
+import { runOnJS } from 'react-native-reanimated';
+
+type PaymentCompleteAnimationProps = {
+    isVisible: boolean;
+    onAnimationFinish: () => void;
+};
+
+function PaymentCompleteAnimation({isVisible, onAnimationFinish}: PaymentCompleteAnimationProps) {
+    const styles = useThemeStyles();
+    const {translate} = useLocalize();
+
+    const paymentCompleteTextScale = useSharedValue(0);
+    const paymentCompleteTextOpacity = useSharedValue(1);
+    const containerHeight = useSharedValue<number>(variables.componentSizeNormal);
+
+    const paymentCompleteTextStyles = useAnimatedStyle(() => ({
+        transform: [{scale: paymentCompleteTextScale.value}],
+        opacity: paymentCompleteTextOpacity.value,
+        position: 'absolute',
+        alignSelf: 'center',
+    }));
+
+    const containerStyles = useAnimatedStyle(() => ({
+        height: containerHeight.value,
+        justifyContent: 'center',
+        overflow: 'hidden',
+    }));
+
+    useEffect(() => {
+        if (!isVisible) {
+            // Reset animation values
+            paymentCompleteTextScale.value = 0;
+            paymentCompleteTextOpacity.value = 1;
+            containerHeight.value = variables.componentSizeNormal;
+            return;
+        }
+
+        // Start animation
+        paymentCompleteTextScale.value = withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION});
+
+        // Wait before hiding the component
+        const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
+        containerHeight.value = withDelay(
+            totalDelay,
+            withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => {
+                if (onAnimationFinish) {
+                    runOnJS(onAnimationFinish)();
+                }
+            }),
+        );
+
+        paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}));
+    }, [containerHeight, isVisible, onAnimationFinish, paymentCompleteTextOpacity, paymentCompleteTextScale]);
+
+    return (
+        isVisible && (
+            <Animated.View style={containerStyles}>
+                <Animated.View style={paymentCompleteTextStyles}>
+                    <Text style={[styles.buttonMediumText]}>{translate('iou.paymentComplete')}</Text>
+                </Animated.View>
+            </Animated.View>
+        )
+    );
+}
+
+PaymentCompleteAnimation.displayName = 'PaymentCompleteAnimation';
+
+export default PaymentCompleteAnimation;

From 380f2a339c2ead84eb81ff2c3cf8b0bf7124ee4e Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Wed, 4 Dec 2024 19:43:33 +0100
Subject: [PATCH 02/37] animating pay button inside the money report header

---
 src/components/MoneyReportHeader.tsx          | 24 ++++--
 .../PaymentCompleteAnimation.tsx              | 75 -------------------
 2 files changed, 18 insertions(+), 81 deletions(-)
 delete mode 100644 src/components/SettlementButton/PaymentCompleteAnimation.tsx

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 008c1353cd68..a153712b66e1 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -10,6 +10,7 @@ import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import {getCurrentUserAccountID} from '@libs/actions/Report';
 import * as CurrencyUtils from '@libs/CurrencyUtils';
+import HapticFeedback from '@libs/HapticFeedback';
 import Navigation from '@libs/Navigation/Navigation';
 import * as PolicyUtils from '@libs/PolicyUtils';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
@@ -42,7 +43,7 @@ import type {ActionHandledType} from './ProcessMoneyReportHoldMenu';
 import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
 import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu';
 import ExportWithDropdownMenu from './ReportActionItem/ExportWithDropdownMenu';
-import SettlementButton from './SettlementButton';
+import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton';
 
 type MoneyReportHeaderProps = {
     /** The report currently being looked at */
@@ -120,6 +121,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction);
     const isArchivedReport = ReportUtils.isArchivedRoomWithID(moneyRequestReport?.reportID);
     const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID ?? '-1'}`, {selector: ReportUtils.getArchiveReason});
+    const [isPaidAnimationRunning, setIsPaidAnimationRunning] = useState(false);
 
     const getCanIOUBePaid = useCallback(
         (onlyShowPayElsewhere = false) => IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere),
@@ -133,7 +135,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         hasAllPendingRTERViolations ||
         (shouldShowBrokenConnectionViolation && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isCurrentUserSubmitter(moneyRequestReport?.reportID ?? '')));
 
-    const shouldShowPayButton = canIOUBePaid || onlyShowPayElsewhere;
+    const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
 
     const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions, [moneyRequestReport, policy, hasOnlyPendingTransactions]);
 
@@ -179,7 +181,11 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
 
     const isReportInRHP = route.name === SCREENS.SEARCH.REPORT_RHP;
     const shouldDisplaySearchRouter = !isReportInRHP || isSmallScreenWidth;
-
+    const stopAnimation = useCallback(() => setIsPaidAnimationRunning(false), []);
+    const startAnimation = useCallback(() => {
+        setIsPaidAnimationRunning(true);
+        HapticFeedback.longPress();
+    }, []);
     const confirmPayment = useCallback(
         (type?: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
             if (!type || !chatReport) {
@@ -192,12 +198,14 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
             } else if (isAnyTransactionOnHold) {
                 setIsHoldMenuVisible(true);
             } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) {
+                startAnimation();
                 IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness);
             } else {
+                startAnimation();
                 IOU.payMoneyRequest(type, chatReport, moneyRequestReport, true);
             }
         },
-        [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport],
+        [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport, startAnimation],
     );
 
     const confirmApproval = () => {
@@ -360,7 +368,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                 )}
                 {shouldShowSettlementButton && !shouldUseNarrowLayout && (
                     <View style={styles.pv2}>
-                        <SettlementButton
+                        <AnimatedSettlementButton
+                            isPaidAnimationRunning={isPaidAnimationRunning}
+                            onAnimationFinish={stopAnimation}
                             onlyShowPayElsewhere={onlyShowPayElsewhere}
                             currency={moneyRequestReport?.currency}
                             confirmApproval={confirmApproval}
@@ -425,7 +435,9 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                             />
                         )}
                         {shouldShowSettlementButton && shouldUseNarrowLayout && (
-                            <SettlementButton
+                            <AnimatedSettlementButton
+                                isPaidAnimationRunning={isPaidAnimationRunning}
+                                onAnimationFinish={stopAnimation}
                                 wrapperStyle={[styles.flex1]}
                                 onlyShowPayElsewhere={onlyShowPayElsewhere}
                                 currency={moneyRequestReport?.currency}
diff --git a/src/components/SettlementButton/PaymentCompleteAnimation.tsx b/src/components/SettlementButton/PaymentCompleteAnimation.tsx
deleted file mode 100644
index b23d587c56d6..000000000000
--- a/src/components/SettlementButton/PaymentCompleteAnimation.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import Text from '@components/Text';
-import useLocalize from '@hooks/useLocalize';
-import useThemeStyles from '@hooks/useThemeStyles';
-import CONST from '@src/CONST';
-import variables from '@styles/variables';
-import React, { useEffect } from 'react';
-import Animated, { useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated';
-import { runOnJS } from 'react-native-reanimated';
-
-type PaymentCompleteAnimationProps = {
-    isVisible: boolean;
-    onAnimationFinish: () => void;
-};
-
-function PaymentCompleteAnimation({isVisible, onAnimationFinish}: PaymentCompleteAnimationProps) {
-    const styles = useThemeStyles();
-    const {translate} = useLocalize();
-
-    const paymentCompleteTextScale = useSharedValue(0);
-    const paymentCompleteTextOpacity = useSharedValue(1);
-    const containerHeight = useSharedValue<number>(variables.componentSizeNormal);
-
-    const paymentCompleteTextStyles = useAnimatedStyle(() => ({
-        transform: [{scale: paymentCompleteTextScale.value}],
-        opacity: paymentCompleteTextOpacity.value,
-        position: 'absolute',
-        alignSelf: 'center',
-    }));
-
-    const containerStyles = useAnimatedStyle(() => ({
-        height: containerHeight.value,
-        justifyContent: 'center',
-        overflow: 'hidden',
-    }));
-
-    useEffect(() => {
-        if (!isVisible) {
-            // Reset animation values
-            paymentCompleteTextScale.value = 0;
-            paymentCompleteTextOpacity.value = 1;
-            containerHeight.value = variables.componentSizeNormal;
-            return;
-        }
-
-        // Start animation
-        paymentCompleteTextScale.value = withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION});
-
-        // Wait before hiding the component
-        const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
-        containerHeight.value = withDelay(
-            totalDelay,
-            withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => {
-                if (onAnimationFinish) {
-                    runOnJS(onAnimationFinish)();
-                }
-            }),
-        );
-
-        paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}));
-    }, [containerHeight, isVisible, onAnimationFinish, paymentCompleteTextOpacity, paymentCompleteTextScale]);
-
-    return (
-        isVisible && (
-            <Animated.View style={containerStyles}>
-                <Animated.View style={paymentCompleteTextStyles}>
-                    <Text style={[styles.buttonMediumText]}>{translate('iou.paymentComplete')}</Text>
-                </Animated.View>
-            </Animated.View>
-        )
-    );
-}
-
-PaymentCompleteAnimation.displayName = 'PaymentCompleteAnimation';
-
-export default PaymentCompleteAnimation;

From 16a8901d0dfe43b2b50319758517d1d2ebda3cbb Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Wed, 4 Dec 2024 19:54:12 +0100
Subject: [PATCH 03/37] refactoring

---
 .../MoneyRequestPreviewContent.tsx            | 20 +++----------------
 1 file changed, 3 insertions(+), 17 deletions(-)

diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index b9dd399488cc..77a6e9225367 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -1,10 +1,9 @@
-import { useRoute } from '@react-navigation/native';
+import {useRoute} from '@react-navigation/native';
 import lodashSortBy from 'lodash/sortBy';
 import truncate from 'lodash/truncate';
-import { default as React, useEffect, useMemo, useRef, useState } from 'react';
-import { View } from 'react-native';
-
+import {default as React, useEffect, useMemo, useRef, useState} from 'react';
 import type {GestureResponderEvent} from 'react-native';
+import {View} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import {useOnyx} from 'react-native-onyx';
 import Animated, {useAnimatedStyle, useSharedValue, withDelay, withSpring} from 'react-native-reanimated';
@@ -17,7 +16,6 @@ import MultipleAvatars from '@components/MultipleAvatars';
 import OfflineWithFeedback from '@components/OfflineWithFeedback';
 import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
 import ReportActionItemImages from '@components/ReportActionItem/ReportActionItemImages';
-import PaymentCompleteAnimation from '@components/SettlementButton/PaymentCompleteAnimation';
 import {showContextMenuForReport} from '@components/ShowContextMenuContext';
 import Text from '@components/Text';
 import useLocalize from '@hooks/useLocalize';
@@ -80,7 +78,6 @@ function MoneyRequestPreviewContent({
     const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || '-1'}`);
     const [session] = useOnyx(ONYXKEYS.SESSION);
     const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID || '-1'}`);
-    const [isPaymentAnimationVisible, setIsPaymentAnimationVisible] = useState(false);
 
     const policy = PolicyUtils.getPolicy(iouReport?.policyID);
     const isMoneyRequestAction = ReportActionsUtils.isMoneyRequestAction(action);
@@ -169,10 +166,6 @@ function MoneyRequestPreviewContent({
         transform: [{scale: checkMarkScale.value}],
     }));
 
-    const handleAnimationFinish = () => {
-        setIsPaymentAnimationVisible(false);
-    };
-
     // Track previous shouldShowPaidIcon value
     const prevIsSettledRef = useRef(shouldShowPaidIcon);
 
@@ -180,9 +173,6 @@ function MoneyRequestPreviewContent({
         if (shouldShowPaidIcon && !prevIsSettledRef.current) {
             // Start the checkmark animation
             checkMarkScale.value = withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION}));
-
-            // Start the payment complete animation
-            setIsPaymentAnimationVisible(true);
         } else if (shouldShowPaidIcon) {
             // Ensure the checkmark is visible without animation if already settled
             checkMarkScale.value = 1;
@@ -472,10 +462,6 @@ function MoneyRequestPreviewContent({
                                                 <Text style={[styles.textMicroSupporting, styles.ml1, styles.amountSplitPadding]}>{pendingMessageProps.messageDescription}</Text>
                                             </View>
                                         )}
-                                        <PaymentCompleteAnimation
-                                            isVisible={isPaymentAnimationVisible}
-                                            onAnimationFinish={handleAnimationFinish}
-                                        />
                                     </View>
                                     {shouldShowCategoryOrTag && <View style={[styles.threadDividerLine, styles.ml0, styles.mr0, styles.mt1]} />}
                                     {shouldShowCategoryOrTag && (

From 2b24c3d710bdef96ebc97422f4409059e765e2bd Mon Sep 17 00:00:00 2001
From: Abdelrahman Khattab <59809993+abzokhattab@users.noreply.github.com>
Date: Sat, 7 Dec 2024 00:46:31 +0100
Subject: [PATCH 04/37] reverting member.ts changes

---
 src/libs/actions/Policy/Member.ts | 23 +++--------------------
 1 file changed, 3 insertions(+), 20 deletions(-)

diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts
index 81dea1c828e7..8fb551cdec81 100644
--- a/src/libs/actions/Policy/Member.ts
+++ b/src/libs/actions/Policy/Member.ts
@@ -26,7 +26,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import type {InvitedEmailsToAccountIDs, PersonalDetailsList, Policy, PolicyEmployee, PolicyOwnershipChangeChecks, Report, ReportAction} from '@src/types/onyx';
 import type {PendingAction} from '@src/types/onyx/OnyxCommon';
 import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage';
-import type {ApprovalRule, Attributes, Rate} from '@src/types/onyx/Policy';
+import type {Attributes, Rate} from '@src/types/onyx/Policy';
 import type {OnyxData} from '@src/types/onyx/Request';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
 import {createPolicyExpenseChats} from './Policy';
@@ -296,17 +296,7 @@ function removeMembers(accountIDs: number[], policyID: string) {
         failureMembersState[email] = {errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericRemove')};
     });
 
-    const approvalRules: ApprovalRule[] = policy?.rules?.approvalRules ?? [];
-    const optimisticApprovalRules: ApprovalRule[] = [];
-
     Object.keys(policy?.employeeList ?? {}).forEach((employeeEmail) => {
-        approvalRules.forEach((rule) => {
-            if (employeeEmail === rule?.approver) {
-                return;
-            }
-            optimisticApprovalRules.push(rule);
-        });
-
         const employee = policy?.employeeList?.[employeeEmail];
         optimisticMembersState[employeeEmail] = optimisticMembersState[employeeEmail] ?? {};
         failureMembersState[employeeEmail] = failureMembersState[employeeEmail] ?? {};
@@ -346,14 +336,7 @@ function removeMembers(accountIDs: number[], policyID: string) {
         {
             onyxMethod: Onyx.METHOD.MERGE,
             key: policyKey,
-            value: {
-                employeeList: optimisticMembersState,
-                approver: emailList.includes(policy?.approver ?? '') ? policy?.owner : policy?.approver,
-                rules: {
-                    ...(policy?.rules ?? {}),
-                    approvalRules: optimisticApprovalRules,
-                },
-            },
+            value: {employeeList: optimisticMembersState, approver: emailList.includes(policy?.approver ?? '') ? policy?.owner : policy?.approver},
         },
     ];
     optimisticData.push(...announceRoomMembers.onyxOptimisticData);
@@ -371,7 +354,7 @@ function removeMembers(accountIDs: number[], policyID: string) {
         {
             onyxMethod: Onyx.METHOD.MERGE,
             key: policyKey,
-            value: {employeeList: failureMembersState, approver: policy?.approver, rules: policy?.rules},
+            value: {employeeList: failureMembersState, approver: policy?.approver},
         },
     ];
     failureData.push(...announceRoomMembers.onyxFailureData);

From bd7843dc4fe6066a8f2ab9bee6efb1347ddd470c Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Sat, 7 Dec 2024 00:51:23 +0100
Subject: [PATCH 05/37] change payment complete from text to animated button
 component

---
 .../AnimatedSettlementButton.tsx              | 103 +++++++++++-------
 1 file changed, 62 insertions(+), 41 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 65c2fd2f493b..6aa01a4aa5fc 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,11 +1,15 @@
-import React, {useCallback, useEffect} from 'react';
-import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
-import Text from '@components/Text';
+import Button from '@components/Button';
+import * as Expensicons from '@components/Icon/Expensicons';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
-import variables from '@styles/variables';
 import CONST from '@src/CONST';
+import variables from '@styles/variables';
+import React, { useCallback, useEffect, useState } from 'react';
+import { LayoutChangeEvent } from 'react-native';
+import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated';
+
 import SettlementButton from '.';
+
 import type SettlementButtonProps from './types';
 
 type AnimatedSettlementButtonProps = SettlementButtonProps & {
@@ -13,25 +17,21 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & {
     onAnimationFinish: () => void;
 };
 
-function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) {
+function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, ...settlementButtonProps}: AnimatedSettlementButtonProps) {
     const styles = useThemeStyles();
     const {translate} = useLocalize();
+
     const buttonScale = useSharedValue(1);
     const buttonOpacity = useSharedValue(1);
-    const paymentCompleteTextScale = useSharedValue(0);
-    const paymentCompleteTextOpacity = useSharedValue(1);
     const height = useSharedValue<number>(variables.componentSizeNormal);
     const buttonMarginTop = useSharedValue<number>(styles.expenseAndReportPreviewTextButtonContainer.gap);
+    const [isLoading, setIsLoading] = useState(false);
+    const [buttonWidth, setButtonWidth] = useState<number | undefined>(undefined);
+
     const buttonStyles = useAnimatedStyle(() => ({
         transform: [{scale: buttonScale.get()}],
         opacity: buttonOpacity.get(),
     }));
-    const paymentCompleteTextStyles = useAnimatedStyle(() => ({
-        transform: [{scale: paymentCompleteTextScale.get()}],
-        opacity: paymentCompleteTextOpacity.get(),
-        position: 'absolute',
-        alignSelf: 'center',
-    }));
     const containerStyles = useAnimatedStyle(() => ({
         height: height.get(),
         justifyContent: 'center',
@@ -48,48 +48,69 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
     const resetAnimation = useCallback(() => {
         buttonScale.set(1);
         buttonOpacity.set(1);
-        paymentCompleteTextScale.set(0);
-        paymentCompleteTextOpacity.set(1);
         height.set(variables.componentSizeNormal);
         buttonMarginTop.set(styles.expenseAndReportPreviewTextButtonContainer.gap);
-    }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]);
+        setIsLoading(false);
+    }, [buttonScale, buttonOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]);
 
     useEffect(() => {
         if (!isPaidAnimationRunning) {
             resetAnimation();
             return;
         }
-        buttonScale.set(withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}));
-        buttonOpacity.set(withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}));
-        paymentCompleteTextScale.set(withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION}));
+        setIsLoading(true);
+        const spinnerTimer = setTimeout(() => {
+            setIsLoading(false);
+
+            // Animate out
+            const totalDelay = CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
+            height.set(
+                withDelay(
+                    totalDelay,
+                    withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
+                ),
+            );
+            buttonMarginTop.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
+        }, CONST.TIMING.SHOW_LOADING_SPINNER_DEBOUNCE_TIME);
 
-        // Wait for the above animation + 1s delay before hiding the component
-        const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
-        height.set(
-            withDelay(
-                totalDelay,
-                withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
-            ),
-        );
-        buttonMarginTop.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
-        paymentCompleteTextOpacity.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
-    }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, buttonMarginTop, resetAnimation]);
+        return () => {
+            clearTimeout(spinnerTimer);
+        };
+    }, [isPaidAnimationRunning, onAnimationFinish, height, buttonMarginTop, resetAnimation]);
 
+    const handleLayout = useCallback(
+        (event: LayoutChangeEvent) => {
+            const newWidth = event.nativeEvent.layout.width;
+            if (newWidth !== buttonWidth) {
+                setButtonWidth(newWidth);
+            }
+        },
+        [buttonWidth],
+    );
     return (
         <Animated.View style={containerStyles}>
-            {isPaidAnimationRunning && (
-                <Animated.View style={paymentCompleteTextStyles}>
-                    <Text style={[styles.buttonMediumText]}>{translate('iou.paymentComplete')}</Text>
+            {isPaidAnimationRunning ? (
+                <Animated.View style={[buttonStyles]}>
+                    <Button
+                        style={[styles.buttonMediumText, {width: buttonWidth}]}
+                        text={translate('iou.paymentComplete')}
+                        isLoading={isLoading}
+                        success
+                        icon={!isLoading ? Expensicons.Checkmark : undefined}
+                    />
+                </Animated.View>
+            ) : (
+                <Animated.View
+                    style={buttonStyles}
+                    onLayout={handleLayout}
+                >
+                    <SettlementButton
+                        // eslint-disable-next-line react/jsx-props-no-spreading
+                        {...settlementButtonProps}
+                        disabledStyle={buttonDisabledStyle}
+                    />
                 </Animated.View>
             )}
-            <Animated.View style={buttonStyles}>
-                <SettlementButton
-                    // eslint-disable-next-line react/jsx-props-no-spreading
-                    {...settlementButtonProps}
-                    isDisabled={isPaidAnimationRunning || isDisabled}
-                    disabledStyle={buttonDisabledStyle}
-                />
-            </Animated.View>
         </Animated.View>
     );
 }

From fd94665cb8eb51149b9aa8f20271ff2ccc7962d1 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Fri, 13 Dec 2024 22:30:54 +0200
Subject: [PATCH 06/37] Creating a hook for animation configurations

---
 .../AnimatedSettlementButton.tsx              | 118 +++++-------------
 src/hooks/useButtonAnimation.ts               | 117 +++++++++++++++++
 tests/unit/useButtonAnimationTest.ts          |  69 ++++++++++
 3 files changed, 215 insertions(+), 89 deletions(-)
 create mode 100644 src/hooks/useButtonAnimation.ts
 create mode 100644 tests/unit/useButtonAnimationTest.ts

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 6aa01a4aa5fc..8618e5f19658 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,15 +1,11 @@
-import Button from '@components/Button';
-import * as Expensicons from '@components/Icon/Expensicons';
+// AnimatedSettlementButton.tsx
+import React from 'react';
+import Animated from 'react-native-reanimated';
+import Text from '@components/Text';
+import useButtonAnimation from '@hooks/useButtonAnimation';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
-import CONST from '@src/CONST';
-import variables from '@styles/variables';
-import React, { useCallback, useEffect, useState } from 'react';
-import { LayoutChangeEvent } from 'react-native';
-import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming } from 'react-native-reanimated';
-
 import SettlementButton from '.';
-
 import type SettlementButtonProps from './types';
 
 type AnimatedSettlementButtonProps = SettlementButtonProps & {
@@ -17,27 +13,22 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & {
     onAnimationFinish: () => void;
 };
 
-function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, ...settlementButtonProps}: AnimatedSettlementButtonProps) {
+function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) {
     const styles = useThemeStyles();
     const {translate} = useLocalize();
 
-    const buttonScale = useSharedValue(1);
-    const buttonOpacity = useSharedValue(1);
-    const height = useSharedValue<number>(variables.componentSizeNormal);
-    const buttonMarginTop = useSharedValue<number>(styles.expenseAndReportPreviewTextButtonContainer.gap);
-    const [isLoading, setIsLoading] = useState(false);
-    const [buttonWidth, setButtonWidth] = useState<number | undefined>(undefined);
+    // Utilize the custom hook
+    const {animatedButtonStyles, animatedContainerStyles, animatedTextStyles, resetAnimation} = useButtonAnimation({
+        isRunning: isPaidAnimationRunning,
+        onFinish: onAnimationFinish,
+        containerInitialMarginTop: styles.expenseAndReportPreviewTextButtonContainer.gap,
+        containerTargetMarginTop: 0,
+        textInitialScale: 0,
+        textTargetScale: 1,
+        textInitialOpacity: 1,
+        textTargetOpacity: 0,
+    });
 
-    const buttonStyles = useAnimatedStyle(() => ({
-        transform: [{scale: buttonScale.get()}],
-        opacity: buttonOpacity.get(),
-    }));
-    const containerStyles = useAnimatedStyle(() => ({
-        height: height.get(),
-        justifyContent: 'center',
-        overflow: 'hidden',
-        marginTop: buttonMarginTop.get(),
-    }));
     const buttonDisabledStyle = isPaidAnimationRunning
         ? {
               opacity: 1,
@@ -45,72 +36,21 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, ..
           }
         : undefined;
 
-    const resetAnimation = useCallback(() => {
-        buttonScale.set(1);
-        buttonOpacity.set(1);
-        height.set(variables.componentSizeNormal);
-        buttonMarginTop.set(styles.expenseAndReportPreviewTextButtonContainer.gap);
-        setIsLoading(false);
-    }, [buttonScale, buttonOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]);
-
-    useEffect(() => {
-        if (!isPaidAnimationRunning) {
-            resetAnimation();
-            return;
-        }
-        setIsLoading(true);
-        const spinnerTimer = setTimeout(() => {
-            setIsLoading(false);
-
-            // Animate out
-            const totalDelay = CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
-            height.set(
-                withDelay(
-                    totalDelay,
-                    withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
-                ),
-            );
-            buttonMarginTop.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
-        }, CONST.TIMING.SHOW_LOADING_SPINNER_DEBOUNCE_TIME);
-
-        return () => {
-            clearTimeout(spinnerTimer);
-        };
-    }, [isPaidAnimationRunning, onAnimationFinish, height, buttonMarginTop, resetAnimation]);
-
-    const handleLayout = useCallback(
-        (event: LayoutChangeEvent) => {
-            const newWidth = event.nativeEvent.layout.width;
-            if (newWidth !== buttonWidth) {
-                setButtonWidth(newWidth);
-            }
-        },
-        [buttonWidth],
-    );
     return (
-        <Animated.View style={containerStyles}>
-            {isPaidAnimationRunning ? (
-                <Animated.View style={[buttonStyles]}>
-                    <Button
-                        style={[styles.buttonMediumText, {width: buttonWidth}]}
-                        text={translate('iou.paymentComplete')}
-                        isLoading={isLoading}
-                        success
-                        icon={!isLoading ? Expensicons.Checkmark : undefined}
-                    />
-                </Animated.View>
-            ) : (
-                <Animated.View
-                    style={buttonStyles}
-                    onLayout={handleLayout}
-                >
-                    <SettlementButton
-                        // eslint-disable-next-line react/jsx-props-no-spreading
-                        {...settlementButtonProps}
-                        disabledStyle={buttonDisabledStyle}
-                    />
+        <Animated.View style={animatedContainerStyles}>
+            {isPaidAnimationRunning && (
+                <Animated.View style={animatedTextStyles}>
+                    <Text style={styles.buttonMediumText}>{translate('iou.paymentComplete')}</Text>
                 </Animated.View>
             )}
+            <Animated.View style={animatedButtonStyles}>
+                <SettlementButton
+                    // eslint-disable-next-line react/jsx-props-no-spreading
+                    {...settlementButtonProps}
+                    isDisabled={isPaidAnimationRunning || isDisabled}
+                    disabledStyle={buttonDisabledStyle}
+                />
+            </Animated.View>
         </Animated.View>
     );
 }
diff --git a/src/hooks/useButtonAnimation.ts b/src/hooks/useButtonAnimation.ts
new file mode 100644
index 000000000000..07dfb294413e
--- /dev/null
+++ b/src/hooks/useButtonAnimation.ts
@@ -0,0 +1,117 @@
+import {useCallback, useEffect, useState} from 'react';
+import type {ViewStyle} from 'react-native';
+import {runOnJS, useAnimatedStyle, withDelay, withTiming} from 'react-native-reanimated';
+import variables from '@styles/variables';
+import CONST from '@src/CONST';
+
+type UseButtonAnimationProps = {
+    isRunning: boolean;
+    onFinish: () => void;
+    buttonInitialScale?: number;
+    buttonTargetScale?: number;
+    buttonInitialOpacity?: number;
+    buttonTargetOpacity?: number;
+    containerInitialHeight?: number;
+    containerTargetHeight?: number;
+    containerInitialMarginTop?: number;
+    containerTargetMarginTop?: number;
+    textInitialScale?: number;
+    textTargetScale?: number;
+    textInitialOpacity?: number;
+    textTargetOpacity?: number;
+    delay?: number;
+    duration?: number;
+};
+
+type UseButtonAnimationReturn = {
+    animatedButtonStyles: ViewStyle;
+    animatedContainerStyles: ViewStyle;
+    animatedTextStyles: ViewStyle;
+    resetAnimation: () => void;
+};
+
+const useButtonAnimation = ({
+    isRunning,
+    onFinish,
+    buttonInitialScale = 1,
+    buttonTargetScale = 0,
+    buttonInitialOpacity = 1,
+    buttonTargetOpacity = 0,
+    containerInitialHeight = variables.componentSizeNormal,
+    containerTargetHeight = 0,
+    containerInitialMarginTop = 10,
+    containerTargetMarginTop = 0,
+    textInitialScale = 0,
+    textTargetScale = 1,
+    textInitialOpacity = 1,
+    textTargetOpacity = 0,
+    delay = CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY,
+    duration = CONST.ANIMATION_PAID_DURATION,
+}: UseButtonAnimationProps): UseButtonAnimationReturn => {
+    // Replace useSharedValue with useState
+    const [buttonScale, setButtonScale] = useState(buttonInitialScale);
+    const [buttonOpacity, setButtonOpacity] = useState(buttonInitialOpacity);
+    const [containerHeight, setContainerHeight] = useState(containerInitialHeight);
+    const [containerMarginTop, setContainerMarginTop] = useState(containerInitialMarginTop);
+    const [textScale, setTextScale] = useState(textInitialScale);
+    const [textOpacity, setTextOpacity] = useState(textInitialOpacity);
+
+    const animatedButtonStyles = useAnimatedStyle(() => ({
+        transform: [{scale: buttonScale}],
+        opacity: buttonOpacity,
+    }));
+
+    const animatedContainerStyles = useAnimatedStyle(() => ({
+        height: containerHeight,
+        justifyContent: 'center',
+        overflow: 'hidden',
+        marginTop: containerMarginTop,
+    }));
+
+    const animatedTextStyles = useAnimatedStyle(() => ({
+        transform: [{scale: textScale}],
+        opacity: textOpacity,
+        position: 'absolute',
+        alignSelf: 'center',
+    }));
+
+    const resetAnimation = useCallback(() => {
+        setButtonScale(buttonInitialScale);
+        setButtonOpacity(buttonInitialOpacity);
+        setContainerHeight(containerInitialHeight);
+        setContainerMarginTop(containerInitialMarginTop);
+        setTextScale(textInitialScale);
+        setTextOpacity(textInitialOpacity);
+    }, [buttonInitialScale, buttonInitialOpacity, containerInitialHeight, containerInitialMarginTop, textInitialScale, textInitialOpacity]);
+
+    useEffect(() => {
+        if (isRunning) {
+            setButtonScale(withTiming(buttonTargetScale, {duration}));
+            setButtonOpacity(withTiming(buttonTargetOpacity, {duration}));
+            setTextScale(withTiming(textTargetScale, {duration}));
+            setTextOpacity(withDelay(delay, withTiming(textTargetOpacity, {duration})));
+            setContainerHeight(
+                withDelay(
+                    delay,
+                    withTiming(containerTargetHeight, {duration}, () => {
+                        runOnJS(onFinish)();
+                    }),
+                ),
+            );
+            setContainerMarginTop(withDelay(delay, withTiming(containerTargetMarginTop, {duration})));
+        } else {
+            resetAnimation();
+        }
+        // eslint-disable-next-line react-compiler/react-compiler
+        // eslint-disable-next-line react-hooks/exhaustive-deps
+    }, [isRunning]);
+
+    return {
+        animatedButtonStyles,
+        animatedContainerStyles,
+        animatedTextStyles,
+        resetAnimation,
+    };
+};
+
+export default useButtonAnimation;
diff --git a/tests/unit/useButtonAnimationTest.ts b/tests/unit/useButtonAnimationTest.ts
new file mode 100644
index 000000000000..a95312ce7d57
--- /dev/null
+++ b/tests/unit/useButtonAnimationTest.ts
@@ -0,0 +1,69 @@
+import {act, renderHook} from '@testing-library/react-native';
+import {advanceAnimationByTime} from 'react-native-reanimated';
+import useButtonAnimation from '@hooks/useButtonAnimation';
+
+// eslint-disable-next-line @typescript-eslint/no-unsafe-return
+jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
+
+describe('useButtonAnimation', () => {
+    const defaultProps = {
+        isRunning: false,
+        onFinish: jest.fn(),
+        buttonInitialScale: 1,
+        buttonTargetScale: 0.5,
+        buttonInitialOpacity: 1,
+        buttonTargetOpacity: 0.5,
+        containerInitialHeight: 100,
+        containerTargetHeight: 0,
+        containerInitialMarginTop: 10,
+        containerTargetMarginTop: 0,
+        textInitialScale: 0,
+        textTargetScale: 1,
+        textInitialOpacity: 0.5,
+        textTargetOpacity: 1,
+        delay: 2000,
+        duration: 500,
+    };
+
+    it('should return default animated styles and resetAnimation function', () => {
+        const {result} = renderHook(() => useButtonAnimation(defaultProps));
+
+        expect(result.current.animatedButtonStyles).toBeDefined();
+        expect(result.current.animatedContainerStyles).toBeDefined();
+        expect(result.current.animatedTextStyles).toBeDefined();
+        expect(result.current.resetAnimation).toBeInstanceOf(Function);
+    });
+
+    it('should reset animation values when resetAnimation is called', () => {
+        const {result} = renderHook(() => useButtonAnimation(defaultProps));
+
+        act(() => {
+            result.current.resetAnimation();
+        });
+
+        // expect(result.current.animatedButtonStyles?.transform?.[0]?.scale).toEqual(defaultProps.buttonInitialScale);
+        expect(result.current.animatedTextStyles.opacity).toEqual(defaultProps.textInitialOpacity);
+        expect(result.current.animatedContainerStyles.height).toEqual(defaultProps.containerInitialHeight);
+    });
+    it('should animate values when isRunning is true', () => {
+        const {result, rerender} = renderHook((props) => useButtonAnimation(props), {
+            initialProps: {...defaultProps, isRunning: false},
+        });
+
+        rerender({...defaultProps, isRunning: true});
+
+        expect(result.current.animatedTextStyles.opacity).toEqual(defaultProps.textTargetOpacity);
+        expect(result.current.animatedContainerStyles.height).toEqual(defaultProps.containerTargetHeight);
+    });
+
+    it('should call onFinish callback after animation is complete', () => {
+        const {rerender} = renderHook((props) => useButtonAnimation(props), {
+            initialProps: {...defaultProps, isRunning: false},
+        });
+
+        rerender({...defaultProps, isRunning: true});
+        advanceAnimationByTime(defaultProps.delay + defaultProps.duration);
+
+        expect(defaultProps.onFinish).toHaveBeenCalled();
+    });
+});

From b46eb8adc330a7492cb6fa7d5b424dc7d22796a1 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Tue, 24 Dec 2024 03:31:14 +0200
Subject: [PATCH 07/37] fade out animation

---
 .../MoneyRequestPreviewContent.tsx            |  42 ++-----
 .../ReportActionItem/ReportPreview.tsx        |  28 +----
 src/hooks/useButtonAnimation.ts               | 117 ------------------
 src/hooks/usePaymentAnimations.ts             |  32 +++++
 tests/unit/useButtonAnimationTest.ts          |  69 -----------
 5 files changed, 45 insertions(+), 243 deletions(-)
 delete mode 100644 src/hooks/useButtonAnimation.ts
 create mode 100644 src/hooks/usePaymentAnimations.ts
 delete mode 100644 tests/unit/useButtonAnimationTest.ts

diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
index 8f4af21461fd..ba0cda25d59e 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
+++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx
@@ -1,12 +1,11 @@
 import {useRoute} from '@react-navigation/native';
 import lodashSortBy from 'lodash/sortBy';
 import truncate from 'lodash/truncate';
-import {default as React, useEffect, useMemo, useRef, useState} from 'react';
-import type {GestureResponderEvent} from 'react-native';
+import React, {useMemo} from 'react';
 import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
+import type {GestureResponderEvent} from 'react-native';
 import {useOnyx} from 'react-native-onyx';
-import Animated, {useAnimatedStyle, useSharedValue, withDelay, withSpring} from 'react-native-reanimated';
+import type {OnyxEntry} from 'react-native-onyx';
 import Button from '@components/Button';
 import Icon from '@components/Icon';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -35,8 +34,8 @@ import * as OptionsListUtils from '@libs/OptionsListUtils';
 import * as PolicyUtils from '@libs/PolicyUtils';
 import * as ReceiptUtils from '@libs/ReceiptUtils';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import type {TransactionDetails} from '@libs/ReportUtils';
 import * as ReportUtils from '@libs/ReportUtils';
+import type {TransactionDetails} from '@libs/ReportUtils';
 import StringUtils from '@libs/StringUtils';
 import * as TransactionUtils from '@libs/TransactionUtils';
 import ViolationsUtils from '@libs/Violations/ViolationsUtils';
@@ -158,33 +157,6 @@ function MoneyRequestPreviewContent({
     const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${route.params?.threadReportID}`);
     const parentReportAction = ReportActionsUtils.getReportAction(report?.parentReportID ?? '', report?.parentReportActionID ?? '');
     const reviewingTransactionID = ReportActionsUtils.isMoneyRequestAction(parentReportAction) ? ReportActionsUtils.getOriginalMessage(parentReportAction)?.IOUTransactionID ?? '-1' : '-1';
-    const shouldShowPaidIcon = ReportUtils.isSettled(iouReport?.reportID) && !isPartialHold && !isBillSplit;
-
-    const checkMarkScale = useSharedValue(shouldShowPaidIcon ? 1 : 0);
-
-    const checkMarkStyle = useAnimatedStyle(() => ({
-        ...styles.defaultCheckmarkWrapper,
-        transform: [{scale: checkMarkScale.value}],
-    }));
-
-    // Track previous shouldShowPaidIcon value
-    const prevIsSettledRef = useRef(shouldShowPaidIcon);
-
-    useEffect(() => {
-        if (shouldShowPaidIcon && !prevIsSettledRef.current) {
-            // Start the checkmark animation
-            checkMarkScale.value = withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION}));
-        } else if (shouldShowPaidIcon) {
-            // Ensure the checkmark is visible without animation if already settled
-            checkMarkScale.value = 1;
-        } else {
-            // Hide the checkmark if not settled
-            checkMarkScale.value = 0;
-        }
-
-        // Update the previous value
-        prevIsSettledRef.current = shouldShowPaidIcon;
-    }, [checkMarkScale, shouldShowPaidIcon]);
 
     /*
      Show the merchant for IOUs and expenses only if:
@@ -416,13 +388,13 @@ function MoneyRequestPreviewContent({
                                                 >
                                                     {displayAmount}
                                                 </Text>
-                                                {shouldShowPaidIcon && (
-                                                    <Animated.View style={[checkMarkStyle, styles.defaultCheckmarkWrapper]}>
+                                                {ReportUtils.isSettled(iouReport?.reportID) && !isPartialHold && !isBillSplit && (
+                                                    <View style={styles.defaultCheckmarkWrapper}>
                                                         <Icon
                                                             src={Expensicons.Checkmark}
                                                             fill={theme.iconSuccessFill}
                                                         />
-                                                    </Animated.View>
+                                                    </View>
                                                 )}
                                             </View>
                                             {isBillSplit && (
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 79497e5fab88..cdcf88959041 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -10,14 +10,15 @@ import Icon from '@components/Icon';
 import * as Expensicons from '@components/Icon/Expensicons';
 import OfflineWithFeedback from '@components/OfflineWithFeedback';
 import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
-import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu';
 import type {ActionHandledType} from '@components/ProcessMoneyReportHoldMenu';
+import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu';
 import AnimatedSettlementButton from '@components/SettlementButton/AnimatedSettlementButton';
 import {showContextMenuForReport} from '@components/ShowContextMenuContext';
 import Text from '@components/Text';
 import useDelegateUserDetails from '@hooks/useDelegateUserDetails';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
+import usePaymentAnimations from '@hooks/usePaymentAnimations';
 import usePolicy from '@hooks/usePolicy';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
@@ -25,7 +26,6 @@ import {getCurrentUserAccountID} from '@libs/actions/Report';
 import ControlSelection from '@libs/ControlSelection';
 import * as CurrencyUtils from '@libs/CurrencyUtils';
 import * as DeviceCapabilities from '@libs/DeviceCapabilities';
-import HapticFeedback from '@libs/HapticFeedback';
 import Navigation from '@libs/Navigation/Navigation';
 import Performance from '@libs/Performance';
 import * as PolicyUtils from '@libs/PolicyUtils';
@@ -123,8 +123,7 @@ function ReportPreview({
         [transactions, iouReportID, action],
     );
 
-    const [isPaidAnimationRunning, setIsPaidAnimationRunning] = useState(false);
-    const [isApprovedAnimationRunning, setIsApprovedAnimationRunning] = useState(false);
+    const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
     const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
     const [requestType, setRequestType] = useState<ActionHandledType>();
     const [paymentType, setPaymentType] = useState<PaymentMethodType>();
@@ -219,19 +218,6 @@ function ReportPreview({
     const {isDelegateAccessRestricted} = useDelegateUserDetails();
     const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);
 
-    const stopAnimation = useCallback(() => {
-        setIsPaidAnimationRunning(false);
-        setIsApprovedAnimationRunning(false);
-    }, []);
-    const startAnimation = useCallback(() => {
-        setIsPaidAnimationRunning(true);
-        HapticFeedback.longPress();
-    }, []);
-    const startApprovedAnimation = useCallback(() => {
-        setIsApprovedAnimationRunning(true);
-        HapticFeedback.longPress();
-    }, []);
-
     const confirmPayment = useCallback(
         (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
             if (!type) {
@@ -244,8 +230,7 @@ function ReportPreview({
             } else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
                 setIsHoldMenuVisible(true);
             } else if (chatReport && iouReport) {
-                setIsPaidAnimationRunning(true);
-                HapticFeedback.longPress();
+                startAnimation();
                 if (ReportUtils.isInvoiceReport(iouReport)) {
                     IOU.payInvoice(type, chatReport, iouReport, payAsBusiness);
                 } else {
@@ -253,7 +238,7 @@ function ReportPreview({
                 }
             }
         },
-        [chatReport, iouReport, isDelegateAccessRestricted],
+        [chatReport, iouReport, isDelegateAccessRestricted, startAnimation],
     );
 
     const confirmApproval = () => {
@@ -263,8 +248,7 @@ function ReportPreview({
         } else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) {
             setIsHoldMenuVisible(true);
         } else {
-            setIsApprovedAnimationRunning(true);
-            HapticFeedback.longPress();
+            startApprovedAnimation();
             IOU.approveMoneyRequest(iouReport, true);
         }
     };
diff --git a/src/hooks/useButtonAnimation.ts b/src/hooks/useButtonAnimation.ts
deleted file mode 100644
index 07dfb294413e..000000000000
--- a/src/hooks/useButtonAnimation.ts
+++ /dev/null
@@ -1,117 +0,0 @@
-import {useCallback, useEffect, useState} from 'react';
-import type {ViewStyle} from 'react-native';
-import {runOnJS, useAnimatedStyle, withDelay, withTiming} from 'react-native-reanimated';
-import variables from '@styles/variables';
-import CONST from '@src/CONST';
-
-type UseButtonAnimationProps = {
-    isRunning: boolean;
-    onFinish: () => void;
-    buttonInitialScale?: number;
-    buttonTargetScale?: number;
-    buttonInitialOpacity?: number;
-    buttonTargetOpacity?: number;
-    containerInitialHeight?: number;
-    containerTargetHeight?: number;
-    containerInitialMarginTop?: number;
-    containerTargetMarginTop?: number;
-    textInitialScale?: number;
-    textTargetScale?: number;
-    textInitialOpacity?: number;
-    textTargetOpacity?: number;
-    delay?: number;
-    duration?: number;
-};
-
-type UseButtonAnimationReturn = {
-    animatedButtonStyles: ViewStyle;
-    animatedContainerStyles: ViewStyle;
-    animatedTextStyles: ViewStyle;
-    resetAnimation: () => void;
-};
-
-const useButtonAnimation = ({
-    isRunning,
-    onFinish,
-    buttonInitialScale = 1,
-    buttonTargetScale = 0,
-    buttonInitialOpacity = 1,
-    buttonTargetOpacity = 0,
-    containerInitialHeight = variables.componentSizeNormal,
-    containerTargetHeight = 0,
-    containerInitialMarginTop = 10,
-    containerTargetMarginTop = 0,
-    textInitialScale = 0,
-    textTargetScale = 1,
-    textInitialOpacity = 1,
-    textTargetOpacity = 0,
-    delay = CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY,
-    duration = CONST.ANIMATION_PAID_DURATION,
-}: UseButtonAnimationProps): UseButtonAnimationReturn => {
-    // Replace useSharedValue with useState
-    const [buttonScale, setButtonScale] = useState(buttonInitialScale);
-    const [buttonOpacity, setButtonOpacity] = useState(buttonInitialOpacity);
-    const [containerHeight, setContainerHeight] = useState(containerInitialHeight);
-    const [containerMarginTop, setContainerMarginTop] = useState(containerInitialMarginTop);
-    const [textScale, setTextScale] = useState(textInitialScale);
-    const [textOpacity, setTextOpacity] = useState(textInitialOpacity);
-
-    const animatedButtonStyles = useAnimatedStyle(() => ({
-        transform: [{scale: buttonScale}],
-        opacity: buttonOpacity,
-    }));
-
-    const animatedContainerStyles = useAnimatedStyle(() => ({
-        height: containerHeight,
-        justifyContent: 'center',
-        overflow: 'hidden',
-        marginTop: containerMarginTop,
-    }));
-
-    const animatedTextStyles = useAnimatedStyle(() => ({
-        transform: [{scale: textScale}],
-        opacity: textOpacity,
-        position: 'absolute',
-        alignSelf: 'center',
-    }));
-
-    const resetAnimation = useCallback(() => {
-        setButtonScale(buttonInitialScale);
-        setButtonOpacity(buttonInitialOpacity);
-        setContainerHeight(containerInitialHeight);
-        setContainerMarginTop(containerInitialMarginTop);
-        setTextScale(textInitialScale);
-        setTextOpacity(textInitialOpacity);
-    }, [buttonInitialScale, buttonInitialOpacity, containerInitialHeight, containerInitialMarginTop, textInitialScale, textInitialOpacity]);
-
-    useEffect(() => {
-        if (isRunning) {
-            setButtonScale(withTiming(buttonTargetScale, {duration}));
-            setButtonOpacity(withTiming(buttonTargetOpacity, {duration}));
-            setTextScale(withTiming(textTargetScale, {duration}));
-            setTextOpacity(withDelay(delay, withTiming(textTargetOpacity, {duration})));
-            setContainerHeight(
-                withDelay(
-                    delay,
-                    withTiming(containerTargetHeight, {duration}, () => {
-                        runOnJS(onFinish)();
-                    }),
-                ),
-            );
-            setContainerMarginTop(withDelay(delay, withTiming(containerTargetMarginTop, {duration})));
-        } else {
-            resetAnimation();
-        }
-        // eslint-disable-next-line react-compiler/react-compiler
-        // eslint-disable-next-line react-hooks/exhaustive-deps
-    }, [isRunning]);
-
-    return {
-        animatedButtonStyles,
-        animatedContainerStyles,
-        animatedTextStyles,
-        resetAnimation,
-    };
-};
-
-export default useButtonAnimation;
diff --git a/src/hooks/usePaymentAnimations.ts b/src/hooks/usePaymentAnimations.ts
new file mode 100644
index 000000000000..47f51d9cafa5
--- /dev/null
+++ b/src/hooks/usePaymentAnimations.ts
@@ -0,0 +1,32 @@
+import {useCallback, useState} from 'react';
+import HapticFeedback from '@libs/HapticFeedback';
+
+function usePaymentAnimations() {
+    const [isPaidAnimationRunning, setIsPaidAnimationRunning] = useState(false);
+    const [isApprovedAnimationRunning, setIsApprovedAnimationRunning] = useState(false);
+
+    const stopAnimation = useCallback(() => {
+        setIsPaidAnimationRunning(false);
+        setIsApprovedAnimationRunning(false);
+    }, []);
+
+    const startAnimation = useCallback(() => {
+        setIsPaidAnimationRunning(true);
+        HapticFeedback.longPress();
+    }, []);
+
+    const startApprovedAnimation = useCallback(() => {
+        setIsApprovedAnimationRunning(true);
+        HapticFeedback.longPress();
+    }, []);
+
+    return {
+        isPaidAnimationRunning,
+        isApprovedAnimationRunning,
+        stopAnimation,
+        startAnimation,
+        startApprovedAnimation,
+    };
+}
+
+export default usePaymentAnimations;
diff --git a/tests/unit/useButtonAnimationTest.ts b/tests/unit/useButtonAnimationTest.ts
deleted file mode 100644
index a95312ce7d57..000000000000
--- a/tests/unit/useButtonAnimationTest.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import {act, renderHook} from '@testing-library/react-native';
-import {advanceAnimationByTime} from 'react-native-reanimated';
-import useButtonAnimation from '@hooks/useButtonAnimation';
-
-// eslint-disable-next-line @typescript-eslint/no-unsafe-return
-jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
-
-describe('useButtonAnimation', () => {
-    const defaultProps = {
-        isRunning: false,
-        onFinish: jest.fn(),
-        buttonInitialScale: 1,
-        buttonTargetScale: 0.5,
-        buttonInitialOpacity: 1,
-        buttonTargetOpacity: 0.5,
-        containerInitialHeight: 100,
-        containerTargetHeight: 0,
-        containerInitialMarginTop: 10,
-        containerTargetMarginTop: 0,
-        textInitialScale: 0,
-        textTargetScale: 1,
-        textInitialOpacity: 0.5,
-        textTargetOpacity: 1,
-        delay: 2000,
-        duration: 500,
-    };
-
-    it('should return default animated styles and resetAnimation function', () => {
-        const {result} = renderHook(() => useButtonAnimation(defaultProps));
-
-        expect(result.current.animatedButtonStyles).toBeDefined();
-        expect(result.current.animatedContainerStyles).toBeDefined();
-        expect(result.current.animatedTextStyles).toBeDefined();
-        expect(result.current.resetAnimation).toBeInstanceOf(Function);
-    });
-
-    it('should reset animation values when resetAnimation is called', () => {
-        const {result} = renderHook(() => useButtonAnimation(defaultProps));
-
-        act(() => {
-            result.current.resetAnimation();
-        });
-
-        // expect(result.current.animatedButtonStyles?.transform?.[0]?.scale).toEqual(defaultProps.buttonInitialScale);
-        expect(result.current.animatedTextStyles.opacity).toEqual(defaultProps.textInitialOpacity);
-        expect(result.current.animatedContainerStyles.height).toEqual(defaultProps.containerInitialHeight);
-    });
-    it('should animate values when isRunning is true', () => {
-        const {result, rerender} = renderHook((props) => useButtonAnimation(props), {
-            initialProps: {...defaultProps, isRunning: false},
-        });
-
-        rerender({...defaultProps, isRunning: true});
-
-        expect(result.current.animatedTextStyles.opacity).toEqual(defaultProps.textTargetOpacity);
-        expect(result.current.animatedContainerStyles.height).toEqual(defaultProps.containerTargetHeight);
-    });
-
-    it('should call onFinish callback after animation is complete', () => {
-        const {rerender} = renderHook((props) => useButtonAnimation(props), {
-            initialProps: {...defaultProps, isRunning: false},
-        });
-
-        rerender({...defaultProps, isRunning: true});
-        advanceAnimationByTime(defaultProps.delay + defaultProps.duration);
-
-        expect(defaultProps.onFinish).toHaveBeenCalled();
-    });
-});

From ccc5bc86e9ddb4c7afc1c12aef47fedfb479a3cc Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Tue, 24 Dec 2024 03:33:09 +0200
Subject: [PATCH 08/37] revert Member.ts

---
 src/libs/actions/Policy/Member.ts | 66 +++++++++++--------------------
 1 file changed, 23 insertions(+), 43 deletions(-)

diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts
index e0af1d719883..81dea1c828e7 100644
--- a/src/libs/actions/Policy/Member.ts
+++ b/src/libs/actions/Policy/Member.ts
@@ -19,7 +19,6 @@ import enhanceParameters from '@libs/Network/enhanceParameters';
 import Parser from '@libs/Parser';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
 import * as PhoneNumber from '@libs/PhoneNumber';
-import {getDefaultApprover} from '@libs/PolicyUtils';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import * as ReportUtils from '@libs/ReportUtils';
 import CONST from '@src/CONST';
@@ -27,7 +26,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
 import type {InvitedEmailsToAccountIDs, PersonalDetailsList, Policy, PolicyEmployee, PolicyOwnershipChangeChecks, Report, ReportAction} from '@src/types/onyx';
 import type {PendingAction} from '@src/types/onyx/OnyxCommon';
 import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage';
-import type {Attributes, Rate} from '@src/types/onyx/Policy';
+import type {ApprovalRule, Attributes, Rate} from '@src/types/onyx/Policy';
 import type {OnyxData} from '@src/types/onyx/Request';
 import {isEmptyObject} from '@src/types/utils/EmptyObject';
 import {createPolicyExpenseChats} from './Policy';
@@ -297,7 +296,17 @@ function removeMembers(accountIDs: number[], policyID: string) {
         failureMembersState[email] = {errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericRemove')};
     });
 
+    const approvalRules: ApprovalRule[] = policy?.rules?.approvalRules ?? [];
+    const optimisticApprovalRules: ApprovalRule[] = [];
+
     Object.keys(policy?.employeeList ?? {}).forEach((employeeEmail) => {
+        approvalRules.forEach((rule) => {
+            if (employeeEmail === rule?.approver) {
+                return;
+            }
+            optimisticApprovalRules.push(rule);
+        });
+
         const employee = policy?.employeeList?.[employeeEmail];
         optimisticMembersState[employeeEmail] = optimisticMembersState[employeeEmail] ?? {};
         failureMembersState[employeeEmail] = failureMembersState[employeeEmail] ?? {};
@@ -333,14 +342,18 @@ function removeMembers(accountIDs: number[], policyID: string) {
         }
     });
 
-    const approvalRules: ApprovalRule[] = policy?.rules?.approvalRules ?? [];
-    const optimisticApprovalRules = approvalRules.filter((rule) => !emailList.includes(rule?.approver ?? ''));
-
     const optimisticData: OnyxUpdate[] = [
         {
             onyxMethod: Onyx.METHOD.MERGE,
             key: policyKey,
-            value: {employeeList: optimisticMembersState, approver: emailList.includes(policy?.approver ?? '') ? policy?.owner : policy?.approver},
+            value: {
+                employeeList: optimisticMembersState,
+                approver: emailList.includes(policy?.approver ?? '') ? policy?.owner : policy?.approver,
+                rules: {
+                    ...(policy?.rules ?? {}),
+                    approvalRules: optimisticApprovalRules,
+                },
+            },
         },
     ];
     optimisticData.push(...announceRoomMembers.onyxOptimisticData);
@@ -358,7 +371,7 @@ function removeMembers(accountIDs: number[], policyID: string) {
         {
             onyxMethod: Onyx.METHOD.MERGE,
             key: policyKey,
-            value: {employeeList: failureMembersState, approver: policy?.approver},
+            value: {employeeList: failureMembersState, approver: policy?.approver, rules: policy?.rules},
         },
     ];
     failureData.push(...announceRoomMembers.onyxFailureData);
@@ -524,7 +537,7 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR
 
 function requestWorkspaceOwnerChange(policyID: string) {
     const policy = getPolicy(policyID);
-    const ownershipChecks = {...policyOwnershipChecks?.[policyID]};
+    const ownershipChecks = {...policyOwnershipChecks?.[policyID]} ?? {};
 
     const changeOwnerErrors = Object.keys(policy?.errorFields?.changeOwner ?? {});
 
@@ -631,12 +644,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
     const successMembersState: OnyxCollectionInputValue<PolicyEmployee> = {};
     const failureMembersState: OnyxCollectionInputValue<PolicyEmployee> = {};
     logins.forEach((email) => {
-        optimisticMembersState[email] = {
-            email,
-            pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
-            role: CONST.POLICY.ROLE.USER,
-            submitsTo: getDefaultApprover(allPolicies?.[policyKey]),
-        };
+        optimisticMembersState[email] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, role: CONST.POLICY.ROLE.USER};
         successMembersState[email] = {pendingAction: null};
         failureMembersState[email] = {
             errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd'),
@@ -685,9 +693,7 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
         employees: JSON.stringify(logins.map((login) => ({email: login}))),
         ...(optimisticAnnounceChat.announceChatReportID ? {announceChatReportID: optimisticAnnounceChat.announceChatReportID} : {}),
         ...(optimisticAnnounceChat.announceChatReportActionID ? {announceCreatedReportActionID: optimisticAnnounceChat.announceChatReportActionID} : {}),
-        welcomeNote: Parser.replace(welcomeNote, {
-            shouldEscapeText: false,
-        }),
+        welcomeNote: Parser.replace(welcomeNote),
         policyID,
     };
     if (!isEmptyObject(membersChats.reportCreationData)) {
@@ -743,31 +749,6 @@ function inviteMemberToWorkspace(policyID: string, inviterEmail: string) {
     API.write(WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK, params, {optimisticData, failureData});
 }
 
-/**
- * Add member to the selected private domain workspace based on policyID
- */
-function joinAccessiblePolicy(policyID: string) {
-    const memberJoinKey = `${ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER}${policyID}` as const;
-
-    const optimisticData: OnyxUpdate[] = [
-        {
-            onyxMethod: Onyx.METHOD.MERGE,
-            key: memberJoinKey,
-            value: {policyID},
-        },
-    ];
-
-    const failureData: OnyxUpdate[] = [
-        {
-            onyxMethod: Onyx.METHOD.MERGE,
-            key: memberJoinKey,
-            value: {policyID, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd')},
-        },
-    ];
-
-    API.write(WRITE_COMMANDS.JOIN_ACCESSIBLE_POLICY, {policyID}, {optimisticData, failureData});
-}
-
 /**
  * Removes an error after trying to delete a member
  */
@@ -959,7 +940,6 @@ export {
     openWorkspaceMembersPage,
     setWorkspaceInviteMembersDraft,
     inviteMemberToWorkspace,
-    joinAccessiblePolicy,
     acceptJoinRequest,
     declineJoinRequest,
     isApprover,

From eaac9e5ca1f9b1bf2e07e9cacdb49eebd1cef939 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Tue, 24 Dec 2024 03:34:06 +0200
Subject: [PATCH 09/37] Revert Member.ts

---
 src/libs/actions/Policy/Member.ts | 53 +++++++++++++++++++++++--------
 1 file changed, 40 insertions(+), 13 deletions(-)

diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts
index 81dea1c828e7..a40617f27cbd 100644
--- a/src/libs/actions/Policy/Member.ts
+++ b/src/libs/actions/Policy/Member.ts
@@ -19,6 +19,7 @@ import enhanceParameters from '@libs/Network/enhanceParameters';
 import Parser from '@libs/Parser';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
 import * as PhoneNumber from '@libs/PhoneNumber';
+import {getDefaultApprover} from '@libs/PolicyUtils';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import * as ReportUtils from '@libs/ReportUtils';
 import CONST from '@src/CONST';
@@ -296,17 +297,7 @@ function removeMembers(accountIDs: number[], policyID: string) {
         failureMembersState[email] = {errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericRemove')};
     });
 
-    const approvalRules: ApprovalRule[] = policy?.rules?.approvalRules ?? [];
-    const optimisticApprovalRules: ApprovalRule[] = [];
-
     Object.keys(policy?.employeeList ?? {}).forEach((employeeEmail) => {
-        approvalRules.forEach((rule) => {
-            if (employeeEmail === rule?.approver) {
-                return;
-            }
-            optimisticApprovalRules.push(rule);
-        });
-
         const employee = policy?.employeeList?.[employeeEmail];
         optimisticMembersState[employeeEmail] = optimisticMembersState[employeeEmail] ?? {};
         failureMembersState[employeeEmail] = failureMembersState[employeeEmail] ?? {};
@@ -342,6 +333,9 @@ function removeMembers(accountIDs: number[], policyID: string) {
         }
     });
 
+    const approvalRules: ApprovalRule[] = policy?.rules?.approvalRules ?? [];
+    const optimisticApprovalRules = approvalRules.filter((rule) => !emailList.includes(rule?.approver ?? ''));
+
     const optimisticData: OnyxUpdate[] = [
         {
             onyxMethod: Onyx.METHOD.MERGE,
@@ -537,7 +531,7 @@ function updateWorkspaceMembersRole(policyID: string, accountIDs: number[], newR
 
 function requestWorkspaceOwnerChange(policyID: string) {
     const policy = getPolicy(policyID);
-    const ownershipChecks = {...policyOwnershipChecks?.[policyID]} ?? {};
+    const ownershipChecks = {...policyOwnershipChecks?.[policyID]};
 
     const changeOwnerErrors = Object.keys(policy?.errorFields?.changeOwner ?? {});
 
@@ -644,7 +638,12 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
     const successMembersState: OnyxCollectionInputValue<PolicyEmployee> = {};
     const failureMembersState: OnyxCollectionInputValue<PolicyEmployee> = {};
     logins.forEach((email) => {
-        optimisticMembersState[email] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, role: CONST.POLICY.ROLE.USER};
+        optimisticMembersState[email] = {
+            email,
+            pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+            role: CONST.POLICY.ROLE.USER,
+            submitsTo: getDefaultApprover(allPolicies?.[policyKey]),
+        };
         successMembersState[email] = {pendingAction: null};
         failureMembersState[email] = {
             errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd'),
@@ -693,7 +692,9 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount
         employees: JSON.stringify(logins.map((login) => ({email: login}))),
         ...(optimisticAnnounceChat.announceChatReportID ? {announceChatReportID: optimisticAnnounceChat.announceChatReportID} : {}),
         ...(optimisticAnnounceChat.announceChatReportActionID ? {announceCreatedReportActionID: optimisticAnnounceChat.announceChatReportActionID} : {}),
-        welcomeNote: Parser.replace(welcomeNote),
+        welcomeNote: Parser.replace(welcomeNote, {
+            shouldEscapeText: false,
+        }),
         policyID,
     };
     if (!isEmptyObject(membersChats.reportCreationData)) {
@@ -749,6 +750,31 @@ function inviteMemberToWorkspace(policyID: string, inviterEmail: string) {
     API.write(WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK, params, {optimisticData, failureData});
 }
 
+/**
+ * Add member to the selected private domain workspace based on policyID
+ */
+function joinAccessiblePolicy(policyID: string) {
+    const memberJoinKey = `${ONYXKEYS.COLLECTION.POLICY_JOIN_MEMBER}${policyID}` as const;
+
+    const optimisticData: OnyxUpdate[] = [
+        {
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: memberJoinKey,
+            value: {policyID},
+        },
+    ];
+
+    const failureData: OnyxUpdate[] = [
+        {
+            onyxMethod: Onyx.METHOD.MERGE,
+            key: memberJoinKey,
+            value: {policyID, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericAdd')},
+        },
+    ];
+
+    API.write(WRITE_COMMANDS.JOIN_ACCESSIBLE_POLICY, {policyID}, {optimisticData, failureData});
+}
+
 /**
  * Removes an error after trying to delete a member
  */
@@ -940,6 +966,7 @@ export {
     openWorkspaceMembersPage,
     setWorkspaceInviteMembersDraft,
     inviteMemberToWorkspace,
+    joinAccessiblePolicy,
     acceptJoinRequest,
     declineJoinRequest,
     isApprover,

From d96bd0694d9f732076f7fc6e32c753114900825b Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Tue, 24 Dec 2024 03:46:54 +0200
Subject: [PATCH 10/37] Remove canIOUBePaid prop from the
 AnimatedSettlementButton

---
 src/components/MoneyReportHeader.tsx             |  3 ---
 .../ReportActionItem/ReportPreview.tsx           |  2 --
 .../AnimatedSettlementButton.tsx                 | 16 +++-------------
 3 files changed, 3 insertions(+), 18 deletions(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 448ad07844c3..f4697ebd0da9 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -139,7 +139,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         (shouldShowBrokenConnectionViolation && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isCurrentUserSubmitter(moneyRequestReport?.reportID ?? '')));
 
     const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
-    const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]);
 
     const shouldShowApproveButton = useMemo(
         () => (IOU.canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions) || isApprovedAnimationRunning,
@@ -377,7 +376,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                             isPaidAnimationRunning={isPaidAnimationRunning}
                             isApprovedAnimationRunning={isApprovedAnimationRunning}
                             onAnimationFinish={stopAnimation}
-                            canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                             onlyShowPayElsewhere={onlyShowPayElsewhere}
                             currency={moneyRequestReport?.currency}
                             confirmApproval={confirmApproval}
@@ -447,7 +445,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                                 isPaidAnimationRunning={isPaidAnimationRunning}
                                 isApprovedAnimationRunning={isApprovedAnimationRunning}
                                 onAnimationFinish={stopAnimation}
-                                canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                                 wrapperStyle={[styles.flex1]}
                                 onlyShowPayElsewhere={onlyShowPayElsewhere}
                                 currency={moneyRequestReport?.currency}
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index cdcf88959041..9bd86a246252 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -135,7 +135,6 @@ function ReportPreview({
     );
 
     const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
-    const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]);
     const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
     const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
     const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]) || isApprovedAnimationRunning;
@@ -557,7 +556,6 @@ function ReportPreview({
                                         onlyShowPayElsewhere={onlyShowPayElsewhere}
                                         isPaidAnimationRunning={isPaidAnimationRunning}
                                         isApprovedAnimationRunning={isApprovedAnimationRunning}
-                                        canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                                         onAnimationFinish={stopAnimation}
                                         formattedAmount={getSettlementAmount() ?? ''}
                                         currency={iouReport?.currency}
diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 45eb2944ff7c..a5f50306dee7 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -5,7 +5,6 @@ import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
-import variables from '@styles/variables';
 import CONST from '@src/CONST';
 import SettlementButton from '.';
 import type SettlementButtonProps from './types';
@@ -14,17 +13,9 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & {
     isPaidAnimationRunning: boolean;
     onAnimationFinish: () => void;
     isApprovedAnimationRunning: boolean;
-    canIOUBePaid: boolean;
 };
 
-function AnimatedSettlementButton({
-    isPaidAnimationRunning,
-    onAnimationFinish,
-    isApprovedAnimationRunning,
-    isDisabled,
-    canIOUBePaid,
-    ...settlementButtonProps
-}: AnimatedSettlementButtonProps) {
+function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) {
     const styles = useThemeStyles();
     const {translate} = useLocalize();
 
@@ -67,12 +58,11 @@ function AnimatedSettlementButton({
 
         const spinnerTimer = setTimeout(() => {
             setIsLoading(false);
-            const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
 
             buttonScale.set(
                 withDelay(
                     CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY,
-                    withTiming(willShowPaymentButton ? variables.componentSizeNormal : 0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
+                    withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
                 ),
             );
 
@@ -82,7 +72,7 @@ function AnimatedSettlementButton({
         return () => {
             clearTimeout(spinnerTimer);
         };
-    }, [isPaidAnimationRunning, onAnimationFinish, buttonScale, buttonOpacity, resetAnimation, isApprovedAnimationRunning, canIOUBePaid]);
+    }, [isPaidAnimationRunning, onAnimationFinish, buttonScale, buttonOpacity, resetAnimation, isApprovedAnimationRunning]);
 
     const handleLayout = useCallback(
         (event: LayoutChangeEvent) => {

From 21ddf3229f7006639ab3c8b6c63ddba7b54c8acd Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Tue, 24 Dec 2024 03:54:27 +0200
Subject: [PATCH 11/37] removing the onLayout in AnimatedSettlementButton

---
 .../AnimatedSettlementButton.tsx              | 19 ++-----------------
 1 file changed, 2 insertions(+), 17 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index a5f50306dee7..059bd0ec0ae5 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,5 +1,4 @@
 import React, {useCallback, useEffect, useState} from 'react';
-import {LayoutChangeEvent} from 'react-native';
 import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -22,7 +21,6 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
     const buttonScale = useSharedValue(1);
     const buttonOpacity = useSharedValue(1);
     const [isLoading, setIsLoading] = useState(false);
-    const [buttonWidth, setButtonWidth] = useState<number | undefined>(undefined);
 
     const buttonStyles = useAnimatedStyle(() => ({
         transform: [{scale: buttonScale.get()}],
@@ -74,22 +72,12 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
         };
     }, [isPaidAnimationRunning, onAnimationFinish, buttonScale, buttonOpacity, resetAnimation, isApprovedAnimationRunning]);
 
-    const handleLayout = useCallback(
-        (event: LayoutChangeEvent) => {
-            const newWidth = event.nativeEvent.layout.width;
-            if (newWidth !== buttonWidth) {
-                setButtonWidth(newWidth);
-            }
-        },
-        [buttonWidth],
-    );
-
     return (
         <Animated.View style={containerStyles}>
             {isPaidAnimationRunning || isApprovedAnimationRunning ? (
                 <Animated.View style={buttonStyles}>
                     <Button
-                        style={[styles.buttonMediumText, {width: buttonWidth}]}
+                        style={[styles.buttonMediumText]}
                         text={isPaidAnimationRunning ? translate('iou.paymentComplete') : translate('iou.approved')}
                         isLoading={isLoading}
                         success
@@ -97,10 +85,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
                     />
                 </Animated.View>
             ) : (
-                <Animated.View
-                    style={buttonStyles}
-                    onLayout={handleLayout}
-                >
+                <Animated.View style={buttonStyles}>
                     <SettlementButton
                         // eslint-disable-next-line react/jsx-props-no-spreading
                         {...settlementButtonProps}

From 8acf3a12daa7c9728a894ceeb0764bbade6b5583 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Tue, 14 Jan 2025 01:08:56 +0100
Subject: [PATCH 12/37] Refactoring

---
 src/CONST.ts                                  |   1 +
 src/components/MoneyReportHeader.tsx          |  12 +-
 .../ReportActionItem/ReportPreview.tsx        |   3 +
 .../AnimatedSettlementButton.tsx              | 107 +++++++++++++-----
 4 files changed, 93 insertions(+), 30 deletions(-)

diff --git a/src/CONST.ts b/src/CONST.ts
index f2b9bf9a63fa..5f59ad179389 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -314,6 +314,7 @@ const CONST = {
     ANIMATION_GYROSCOPE_VALUE: 0.4,
     ANIMATION_PAID_DURATION: 200,
     ANIMATION_PAID_CHECKMARK_DELAY: 300,
+    ANIMATION_LOADING_DURATION: 500,
     ANIMATION_THUMBSUP_DURATION: 250,
     ANIMATION_THUMBSUP_DELAY: 200,
     ANIMATION_PAID_BUTTON_HIDE_DELAY: 1000,
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index f4697ebd0da9..4034ebcaa4ad 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -6,6 +6,7 @@ import {useOnyx} from 'react-native-onyx';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
 import usePaymentAnimations from '@hooks/usePaymentAnimations';
+import usePrevious from '@hooks/usePrevious';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
@@ -98,7 +99,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const isOnHold = TransactionUtils.isOnHold(transaction);
     const isDeletedParentAction = !!requestParentReportAction && ReportActionsUtils.isDeletedAction(requestParentReportAction);
     const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID ?? '');
-
+    const prevIsDuplicate = usePrevious(isDuplicate);
     // Only the requestor can delete the request, admins can only edit it.
     const isActionOwner =
         typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID;
@@ -123,6 +124,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const isArchivedReport = ReportUtils.isArchivedRoomWithID(moneyRequestReport?.reportID);
     const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID ?? '-1'}`, {selector: ReportUtils.getArchiveReason});
     const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
+    const shouldShowIsDuplicate = isDuplicate || (prevIsDuplicate && (isPaidAnimationRunning || isApprovedAnimationRunning));
 
     const getCanIOUBePaid = useCallback(
         (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>
@@ -131,7 +133,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     );
 
     const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
-
+    const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]);
     const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
 
     const shouldShowMarkAsCashButton =
@@ -357,7 +359,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                 // Shows border if no buttons or banners are showing below the header
                 shouldShowBorderBottom={!isMoreContentShown}
             >
-                {isDuplicate && !shouldUseNarrowLayout && (
+                {shouldShowIsDuplicate && !shouldUseNarrowLayout && (
                     <View style={[shouldDuplicateButtonBeSuccess ? styles.ml2 : styles.mh2]}>
                         <Button
                             success={shouldDuplicateButtonBeSuccess}
@@ -376,6 +378,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                             isPaidAnimationRunning={isPaidAnimationRunning}
                             isApprovedAnimationRunning={isApprovedAnimationRunning}
                             onAnimationFinish={stopAnimation}
+                            canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                             onlyShowPayElsewhere={onlyShowPayElsewhere}
                             currency={moneyRequestReport?.currency}
                             confirmApproval={confirmApproval}
@@ -429,7 +432,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
             {!!isMoreContentShown && (
                 <View style={[styles.dFlex, styles.flexColumn, shouldAddGapToContents && styles.gap3, styles.pb3, styles.ph5, styles.borderBottom]}>
                     <View style={[styles.dFlex, styles.w100, styles.flexRow, styles.gap3]}>
-                        {isDuplicate && shouldUseNarrowLayout && (
+                        {shouldShowIsDuplicate && shouldUseNarrowLayout && (
                             <Button
                                 success={shouldDuplicateButtonBeSuccess}
                                 text={translate('iou.reviewDuplicates')}
@@ -445,6 +448,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                                 isPaidAnimationRunning={isPaidAnimationRunning}
                                 isApprovedAnimationRunning={isApprovedAnimationRunning}
                                 onAnimationFinish={stopAnimation}
+                                canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                                 wrapperStyle={[styles.flex1]}
                                 onlyShowPayElsewhere={onlyShowPayElsewhere}
                                 currency={moneyRequestReport?.currency}
diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx
index 20a2864cc05d..4c8fe4029cf2 100644
--- a/src/components/ReportActionItem/ReportPreview.tsx
+++ b/src/components/ReportActionItem/ReportPreview.tsx
@@ -135,6 +135,7 @@ function ReportPreview({
     );
 
     const canIOUBePaid = useMemo(() => getCanIOUBePaid(), [getCanIOUBePaid]);
+    const canIOUBePaidAndApproved = useMemo(() => getCanIOUBePaid(false, false), [getCanIOUBePaid]);
     const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
     const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
     const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]) || isApprovedAnimationRunning;
@@ -557,6 +558,7 @@ function ReportPreview({
                                         onlyShowPayElsewhere={onlyShowPayElsewhere}
                                         isPaidAnimationRunning={isPaidAnimationRunning}
                                         isApprovedAnimationRunning={isApprovedAnimationRunning}
+                                        canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                                         onAnimationFinish={stopAnimation}
                                         formattedAmount={getSettlementAmount() ?? ''}
                                         currency={iouReport?.currency}
@@ -569,6 +571,7 @@ function ReportPreview({
                                         confirmApproval={confirmApproval}
                                         enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
                                         addBankAccountRoute={bankAccountRoute}
+                                        shouldAddTopMargin
                                         shouldHidePaymentOptions={!shouldShowPayButton}
                                         shouldShowApproveButton={shouldShowApproveButton}
                                         shouldDisableApproveButton={shouldDisableApproveButton}
diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 059bd0ec0ae5..69a84005ddfe 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,9 +1,11 @@
 import React, {useCallback, useEffect, useState} from 'react';
+import type {LayoutChangeEvent} from 'react-native';
 import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import useLocalize from '@hooks/useLocalize';
 import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
 import CONST from '@src/CONST';
 import SettlementButton from '.';
 import type SettlementButtonProps from './types';
@@ -12,14 +14,28 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & {
     isPaidAnimationRunning: boolean;
     onAnimationFinish: () => void;
     isApprovedAnimationRunning: boolean;
+    shouldAddTopMargin?: boolean;
+    canIOUBePaid: boolean;
 };
 
-function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isApprovedAnimationRunning, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) {
+function AnimatedSettlementButton({
+    isPaidAnimationRunning,
+    onAnimationFinish,
+    isApprovedAnimationRunning,
+    shouldAddTopMargin = false,
+    isDisabled,
+    canIOUBePaid,
+    wrapperStyle,
+    ...settlementButtonProps
+}: AnimatedSettlementButtonProps) {
     const styles = useThemeStyles();
     const {translate} = useLocalize();
 
     const buttonScale = useSharedValue(1);
     const buttonOpacity = useSharedValue(1);
+    const height = useSharedValue<number>(variables.componentSizeNormal);
+    const buttonMarginTop = useSharedValue<number>(styles.expenseAndReportPreviewTextButtonContainer.gap);
+    const [buttonWidth, setButtonWidth] = useState<number | undefined>(undefined);
     const [isLoading, setIsLoading] = useState(false);
 
     const buttonStyles = useAnimatedStyle(() => ({
@@ -28,8 +44,10 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
     }));
 
     const containerStyles = useAnimatedStyle(() => ({
+        height: height.get(),
         justifyContent: 'center',
         overflow: 'hidden',
+        ...(shouldAddTopMargin && {marginTop: buttonMarginTop.get()}),
     }));
 
     const buttonDisabledStyle =
@@ -44,7 +62,11 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
         buttonScale.set(1);
         buttonOpacity.set(1);
         setIsLoading(false);
-    }, [buttonScale, buttonOpacity]);
+        height.set(variables.componentSizeNormal);
+        if (shouldAddTopMargin) {
+            buttonMarginTop.set(styles.expenseAndReportPreviewTextButtonContainer.gap);
+        }
+    }, [buttonScale, buttonOpacity, height, shouldAddTopMargin, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]);
 
     useEffect(() => {
         if (!isApprovedAnimationRunning && !isPaidAnimationRunning) {
@@ -53,46 +75,79 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
         }
 
         setIsLoading(true);
+        const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
+        const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
 
         const spinnerTimer = setTimeout(() => {
             setIsLoading(false);
 
-            buttonScale.set(
+            buttonScale.set(withDelay(CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
+            buttonOpacity.set(withDelay(CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
+
+            // Wait for the above animation + 1s delay before hiding the component
+            height.set(
                 withDelay(
-                    CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY,
-                    withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
+                    totalDelay,
+                    withTiming(willShowPaymentButton ? variables.componentSizeNormal : 0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
                 ),
             );
 
-            buttonOpacity.set(withDelay(CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
-        }, CONST.TIMING.SHOW_LOADING_SPINNER_DEBOUNCE_TIME);
+            if (shouldAddTopMargin) {
+                buttonMarginTop.set(
+                    withDelay(totalDelay, withTiming(willShowPaymentButton ? styles.expenseAndReportPreviewTextButtonContainer.gap : 0, {duration: CONST.ANIMATION_PAID_DURATION})),
+                );
+                buttonMarginTop.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
+            }
+        }, CONST.ANIMATION_LOADING_DURATION);
 
         return () => {
             clearTimeout(spinnerTimer);
         };
-    }, [isPaidAnimationRunning, onAnimationFinish, buttonScale, buttonOpacity, resetAnimation, isApprovedAnimationRunning]);
+    }, [
+        isPaidAnimationRunning,
+        onAnimationFinish,
+        buttonScale,
+        buttonOpacity,
+        resetAnimation,
+        isApprovedAnimationRunning,
+        canIOUBePaid,
+        buttonMarginTop,
+        styles.expenseAndReportPreviewTextButtonContainer.gap,
+        height,
+        shouldAddTopMargin,
+    ]);
+
+    const handleLayout = useCallback(
+        (event: LayoutChangeEvent) => {
+            const newWidth = event.nativeEvent.layout.width;
+            if (newWidth !== buttonWidth) {
+                setButtonWidth(newWidth);
+            }
+        },
+        [buttonWidth],
+    );
 
     return (
-        <Animated.View style={containerStyles}>
+        <Animated.View
+            style={[containerStyles, wrapperStyle, buttonStyles]}
+            onLayout={handleLayout}
+        >
             {isPaidAnimationRunning || isApprovedAnimationRunning ? (
-                <Animated.View style={buttonStyles}>
-                    <Button
-                        style={[styles.buttonMediumText]}
-                        text={isPaidAnimationRunning ? translate('iou.paymentComplete') : translate('iou.approved')}
-                        isLoading={isLoading}
-                        success
-                        icon={!isLoading ? Expensicons.Checkmark : undefined}
-                    />
-                </Animated.View>
+                <Button
+                    style={[styles.buttonMediumText, {width: buttonWidth}]}
+                    text={isPaidAnimationRunning ? translate('iou.paymentComplete') : translate('iou.approved')}
+                    isLoading={isLoading}
+                    success
+                    icon={isLoading ? undefined : (isPaidAnimationRunning && Expensicons.Checkmark) || Expensicons.ThumbsUp}
+                />
             ) : (
-                <Animated.View style={buttonStyles}>
-                    <SettlementButton
-                        // eslint-disable-next-line react/jsx-props-no-spreading
-                        {...settlementButtonProps}
-                        isDisabled={isPaidAnimationRunning || isApprovedAnimationRunning || isDisabled}
-                        disabledStyle={buttonDisabledStyle}
-                    />
-                </Animated.View>
+                <SettlementButton
+                    // eslint-disable-next-line react/jsx-props-no-spreading
+                    {...settlementButtonProps}
+                    wrapperStyle={wrapperStyle}
+                    isDisabled={isPaidAnimationRunning || isApprovedAnimationRunning || isDisabled}
+                    disabledStyle={buttonDisabledStyle}
+                />
             )}
         </Animated.View>
     );

From f006f53c61b8d5cb5ce5a9e099170c9f40055138 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Tue, 14 Jan 2025 01:48:10 +0100
Subject: [PATCH 13/37] eslint fix

---
 src/components/MoneyReportHeader.tsx | 150 ++++++++++++++++-----------
 1 file changed, 91 insertions(+), 59 deletions(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index c97a3ddb79ea..3ddae5a64804 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -10,16 +10,55 @@ import usePrevious from '@hooks/usePrevious';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
+import {
+    approveMoneyRequest,
+    canApproveIOU,
+    canIOUBePaid as canIOUBePaidIOUActions,
+    deleteMoneyRequest,
+    deleteTrackExpense,
+    dismissHoldUseExplanation,
+    payInvoice,
+    payMoneyRequest,
+    submitReport,
+} from '@libs/actions/IOU';
 import {getCurrentUserAccountID} from '@libs/actions/Report';
-import * as CurrencyUtils from '@libs/CurrencyUtils';
+import {convertToDisplayString} from '@libs/CurrencyUtils';
 import Navigation from '@libs/Navigation/Navigation';
-import * as PolicyUtils from '@libs/PolicyUtils';
-import * as ReportActionsUtils from '@libs/ReportActionsUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import * as TransactionUtils from '@libs/TransactionUtils';
+import {getConnectedIntegration, isPolicyAdmin} from '@libs/PolicyUtils';
+import {getOriginalMessage, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
+import {
+    canBeExported,
+    canDeleteTransaction,
+    getArchiveReason,
+    getBankAccountRoute,
+    getMoneyRequestSpendBreakdown,
+    getNonHeldAndFullAmount,
+    getTransactionsWithReceipts as getTransactionsWithReceiptsReportUtils,
+    hasHeldExpenses as hasHeldExpensesReportUtils,
+    hasOnlyHeldExpenses as hasOnlyHeldExpensesReportUtils,
+    hasUpdatedTotal,
+    isAllowedToApproveExpenseReport,
+    isAllowedToSubmitDraftExpenseReport,
+    isArchivedReport as isArchivedReportUtils,
+    isClosedExpenseReportWithNoExpenses,
+    isCurrentUserSubmitter,
+    isInvoiceReport,
+    isOpenExpenseReport,
+    navigateBackOnDeleteTransaction,
+} from '@libs/ReportUtils';
+import {
+    allHavePendingRTERViolation,
+    getAllReportTransactions,
+    isDuplicate as isDuplicateTransactionUtils,
+    isExpensifyCardTransaction,
+    isOnHold as isOnHoldTransactionUtils,
+    isPayAtEndExpense as isPayAtEndExpenseTransactionUtils,
+    isPending,
+    isReceiptBeingScanned,
+    shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils,
+} from '@libs/TransactionUtils';
 import variables from '@styles/variables';
-import * as IOU from '@userActions/IOU';
-import * as TransactionActions from '@userActions/Transaction';
+import {markAsCash as markAsCashTransactionActions} from '@userActions/Transaction';
 import CONST from '@src/CONST';
 import useDelegateUserDetails from '@src/hooks/useDelegateUserDetails';
 import ONYXKEYS from '@src/ONYXKEYS';
@@ -83,11 +122,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true});
     const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult);
     const transaction =
-        transactions?.[
-            `${ONYXKEYS.COLLECTION.TRANSACTION}${
-                ReportActionsUtils.isMoneyRequestAction(requestParentReportAction) && ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID
-            }`
-        ] ?? undefined;
+        transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${isMoneyRequestAction(requestParentReportAction) && getOriginalMessage(requestParentReportAction)?.IOUTransactionID}`] ??
+        undefined;
 
     const styles = useThemeStyles();
     const theme = useTheme();
@@ -95,40 +131,40 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false);
     const {translate} = useLocalize();
     const {isOffline} = useNetwork();
-    const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport);
-    const isOnHold = TransactionUtils.isOnHold(transaction);
-    const isDeletedParentAction = !!requestParentReportAction && ReportActionsUtils.isDeletedAction(requestParentReportAction);
-    const isDuplicate = TransactionUtils.isDuplicate(transaction?.transactionID);
+    const {reimbursableSpend} = getMoneyRequestSpendBreakdown(moneyRequestReport);
+    const isOnHold = isOnHoldTransactionUtils(transaction);
+    const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction);
+    const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID);
     const prevIsDuplicate = usePrevious(isDuplicate);
     // Only the requestor can delete the request, admins can only edit it.
     const isActionOwner =
         typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID;
-    const canDeleteRequest = isActionOwner && ReportUtils.canDeleteTransaction(moneyRequestReport) && !isDeletedParentAction;
+    const canDeleteRequest = isActionOwner && canDeleteTransaction(moneyRequestReport) && !isDeletedParentAction;
     const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false);
     const [paymentType, setPaymentType] = useState<PaymentMethodType>();
     const [requestType, setRequestType] = useState<ActionHandledType>();
-    const allTransactions = useMemo(() => TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID, transactions), [moneyRequestReport?.reportID, transactions]);
-    const canAllowSettlement = ReportUtils.hasUpdatedTotal(moneyRequestReport, policy);
+    const allTransactions = useMemo(() => getAllReportTransactions(moneyRequestReport?.reportID, transactions), [moneyRequestReport?.reportID, transactions]);
+    const canAllowSettlement = hasUpdatedTotal(moneyRequestReport, policy);
     const policyType = policy?.type;
-    const isDraft = ReportUtils.isOpenExpenseReport(moneyRequestReport);
-    const connectedIntegration = PolicyUtils.getConnectedIntegration(policy);
+    const isDraft = isOpenExpenseReport(moneyRequestReport);
+    const connectedIntegration = getConnectedIntegration(policy);
     const navigateBackToAfterDelete = useRef<Route>();
-    const hasHeldExpenses = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID);
-    const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t));
-    const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => TransactionUtils.isExpensifyCardTransaction(t) && TransactionUtils.isPending(t));
+    const hasHeldExpenses = hasHeldExpensesReportUtils(moneyRequestReport?.reportID);
+    const hasScanningReceipt = getTransactionsWithReceiptsReportUtils(moneyRequestReport?.reportID).some((t) => isReceiptBeingScanned(t));
+    const hasOnlyPendingTransactions = allTransactions.length > 0 && allTransactions.every((t) => isExpensifyCardTransaction(t) && isPending(t));
     const transactionIDs = allTransactions.map((t) => t.transactionID);
-    const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation([transaction?.transactionID]);
-    const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID, moneyRequestReport, policy);
-    const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport?.reportID);
-    const isPayAtEndExpense = TransactionUtils.isPayAtEndExpense(transaction);
-    const isArchivedReport = ReportUtils.isArchivedReport(moneyRequestReport);
-    const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: ReportUtils.getArchiveReason});
+    const hasAllPendingRTERViolations = allHavePendingRTERViolation([transaction?.transactionID]);
+    const shouldShowBrokenConnectionViolation = shouldShowBrokenConnectionViolationTransactionUtils(transaction?.transactionID, moneyRequestReport, policy);
+    const hasOnlyHeldExpenses = hasOnlyHeldExpensesReportUtils(moneyRequestReport?.reportID);
+    const isPayAtEndExpense = isPayAtEndExpenseTransactionUtils(transaction);
+    const isArchivedReport = isArchivedReportUtils(moneyRequestReport);
+    const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: getArchiveReason});
     const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
     const shouldShowIsDuplicate = isDuplicate || (prevIsDuplicate && (isPaidAnimationRunning || isApprovedAnimationRunning));
 
     const getCanIOUBePaid = useCallback(
         (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>
-            IOU.canIOUBePaid(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState),
+            canIOUBePaidIOUActions(moneyRequestReport, chatReport, policy, transaction ? [transaction] : undefined, onlyShowPayElsewhere, undefined, undefined, shouldCheckApprovedState),
         [moneyRequestReport, chatReport, policy, transaction],
     );
 
@@ -137,16 +173,16 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const onlyShowPayElsewhere = useMemo(() => !canIOUBePaid && getCanIOUBePaid(true), [canIOUBePaid, getCanIOUBePaid]);
 
     const shouldShowMarkAsCashButton =
-        hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isCurrentUserSubmitter(moneyRequestReport?.reportID)));
+        hasAllPendingRTERViolations || (shouldShowBrokenConnectionViolation && (!isPolicyAdmin(policy) || isCurrentUserSubmitter(moneyRequestReport?.reportID)));
 
     const shouldShowPayButton = isPaidAnimationRunning || canIOUBePaid || onlyShowPayElsewhere;
 
     const shouldShowApproveButton = useMemo(
-        () => (IOU.canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions) || isApprovedAnimationRunning,
+        () => (canApproveIOU(moneyRequestReport, policy) && !hasOnlyPendingTransactions) || isApprovedAnimationRunning,
         [moneyRequestReport, policy, hasOnlyPendingTransactions, isApprovedAnimationRunning],
     );
 
-    const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport);
+    const shouldDisableApproveButton = shouldShowApproveButton && !isAllowedToApproveExpenseReport(moneyRequestReport);
 
     const currentUserAccountID = getCurrentUserAccountID();
     const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
@@ -160,16 +196,16 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         !shouldShowBrokenConnectionViolation &&
         (moneyRequestReport?.ownerAccountID === currentUserAccountID || isAdmin || moneyRequestReport?.managerID === currentUserAccountID);
 
-    const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(moneyRequestReport);
+    const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && canBeExported(moneyRequestReport);
 
     const shouldShowSettlementButton =
         (shouldShowPayButton || shouldShowApproveButton) && !hasAllPendingRTERViolations && !shouldShowExportIntegrationButton && !shouldShowBrokenConnectionViolation;
 
-    const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
+    const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
     const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
     const shouldShowStatusBar =
         hasAllPendingRTERViolations || shouldShowBrokenConnectionViolation || hasOnlyHeldExpenses || hasScanningReceipt || isPayAtEndExpense || hasOnlyPendingTransactions;
-    const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar;
+    const shouldShowNextStep = !isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar;
     const shouldShowAnyButton =
         isDuplicate ||
         shouldShowSettlementButton ||
@@ -178,10 +214,10 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         shouldShowNextStep ||
         shouldShowMarkAsCashButton ||
         shouldShowExportIntegrationButton;
-    const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
-    const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency);
-    const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton);
-    const isAnyTransactionOnHold = ReportUtils.hasHeldExpenses(moneyRequestReport?.reportID);
+    const bankAccountRoute = getBankAccountRoute(chatReport);
+    const formattedAmount = convertToDisplayString(reimbursableSpend, moneyRequestReport?.currency);
+    const {nonHeldAmount, fullAmount, hasValidNonHeldAmount} = getNonHeldAndFullAmount(moneyRequestReport, shouldShowPayButton);
+    const isAnyTransactionOnHold = hasHeldExpensesReportUtils(moneyRequestReport?.reportID);
     const displayedAmount = isAnyTransactionOnHold && canAllowSettlement && hasValidNonHeldAmount ? nonHeldAmount : formattedAmount;
     const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout);
     const {isDelegateAccessRestricted} = useDelegateUserDetails();
@@ -201,12 +237,12 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                 setIsNoDelegateAccessMenuVisible(true);
             } else if (isAnyTransactionOnHold) {
                 setIsHoldMenuVisible(true);
-            } else if (ReportUtils.isInvoiceReport(moneyRequestReport)) {
+            } else if (isInvoiceReport(moneyRequestReport)) {
                 startAnimation();
-                IOU.payInvoice(type, chatReport, moneyRequestReport, payAsBusiness);
+                payInvoice(type, chatReport, moneyRequestReport, payAsBusiness);
             } else {
                 startAnimation();
-                IOU.payMoneyRequest(type, chatReport, moneyRequestReport, true);
+                payMoneyRequest(type, chatReport, moneyRequestReport, true);
             }
         },
         [chatReport, isAnyTransactionOnHold, isDelegateAccessRestricted, moneyRequestReport, startAnimation],
@@ -220,19 +256,17 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
             setIsHoldMenuVisible(true);
         } else {
             startApprovedAnimation();
-            IOU.approveMoneyRequest(moneyRequestReport, true);
+            approveMoneyRequest(moneyRequestReport, true);
         }
     };
 
     const deleteTransaction = useCallback(() => {
         if (requestParentReportAction) {
-            const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(requestParentReportAction)
-                ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID
-                : undefined;
-            if (ReportActionsUtils.isTrackExpenseAction(requestParentReportAction)) {
-                navigateBackToAfterDelete.current = IOU.deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, true);
+            const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined;
+            if (isTrackExpenseAction(requestParentReportAction)) {
+                navigateBackToAfterDelete.current = deleteTrackExpense(moneyRequestReport?.reportID, iouTransactionID, requestParentReportAction, true);
             } else {
-                navigateBackToAfterDelete.current = IOU.deleteMoneyRequest(iouTransactionID, requestParentReportAction, true);
+                navigateBackToAfterDelete.current = deleteMoneyRequest(iouTransactionID, requestParentReportAction, true);
             }
         }
 
@@ -243,15 +277,13 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         if (!requestParentReportAction) {
             return;
         }
-        const iouTransactionID = ReportActionsUtils.isMoneyRequestAction(requestParentReportAction)
-            ? ReportActionsUtils.getOriginalMessage(requestParentReportAction)?.IOUTransactionID
-            : undefined;
+        const iouTransactionID = isMoneyRequestAction(requestParentReportAction) ? getOriginalMessage(requestParentReportAction)?.IOUTransactionID : undefined;
         const reportID = transactionThreadReport?.reportID;
 
         if (!iouTransactionID || !reportID) {
             return;
         }
-        TransactionActions.markAsCash(iouTransactionID, reportID);
+        markAsCashTransactionActions(iouTransactionID, reportID);
     }, [requestParentReportAction, transactionThreadReport?.reportID]);
 
     const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => (
@@ -337,7 +369,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     }, [isSmallScreenWidth, shouldShowHoldMenu]);
 
     const handleHoldRequestClose = () => {
-        IOU.dismissHoldUseExplanation();
+        dismissHoldUseExplanation();
     };
 
     useEffect(() => {
@@ -416,7 +448,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                             success={isWaitingForSubmissionFromCurrentUser}
                             text={translate('common.submit')}
                             style={[styles.mnw120, styles.pv2, styles.pr0]}
-                            onPress={() => IOU.submitReport(moneyRequestReport)}
+                            onPress={() => submitReport(moneyRequestReport)}
                             isDisabled={shouldDisableSubmitButton}
                         />
                     </View>
@@ -482,7 +514,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                                 success={isWaitingForSubmissionFromCurrentUser}
                                 text={translate('common.submit')}
                                 style={[styles.flex1, styles.pr0]}
-                                onPress={() => IOU.submitReport(moneyRequestReport)}
+                                onPress={() => submitReport(moneyRequestReport)}
                                 isDisabled={shouldDisableSubmitButton}
                             />
                         )}
@@ -534,7 +566,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                 isVisible={isDeleteRequestModalVisible}
                 onConfirm={deleteTransaction}
                 onCancel={() => setIsDeleteRequestModalVisible(false)}
-                onModalHide={() => ReportUtils.navigateBackOnDeleteTransaction(navigateBackToAfterDelete.current)}
+                onModalHide={() => navigateBackOnDeleteTransaction(navigateBackToAfterDelete.current)}
                 prompt={translate('iou.deleteConfirmation', {count: 1})}
                 confirmText={translate('common.delete')}
                 cancelText={translate('common.cancel')}

From 402bb4376febe261ede3e0678d808c6f1f04f346 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Sat, 18 Jan 2025 23:45:25 +0100
Subject: [PATCH 14/37] fixing approved/paid race condition

---
 .../AnimatedSettlementButton.tsx              | 92 +++++++++----------
 1 file changed, 46 insertions(+), 46 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 69a84005ddfe..73a34d8d4f83 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,5 +1,4 @@
 import React, {useCallback, useEffect, useState} from 'react';
-import type {LayoutChangeEvent} from 'react-native';
 import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -20,8 +19,8 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & {
 
 function AnimatedSettlementButton({
     isPaidAnimationRunning,
-    onAnimationFinish,
     isApprovedAnimationRunning,
+    onAnimationFinish,
     shouldAddTopMargin = false,
     isDisabled,
     canIOUBePaid,
@@ -33,15 +32,17 @@ function AnimatedSettlementButton({
 
     const buttonScale = useSharedValue(1);
     const buttonOpacity = useSharedValue(1);
+    const [shouldHideButton, setShouldHideButton] = useState(true);
+    const [isLoading, setIsLoading] = useState(false);
     const height = useSharedValue<number>(variables.componentSizeNormal);
     const buttonMarginTop = useSharedValue<number>(styles.expenseAndReportPreviewTextButtonContainer.gap);
-    const [buttonWidth, setButtonWidth] = useState<number | undefined>(undefined);
-    const [isLoading, setIsLoading] = useState(false);
 
-    const buttonStyles = useAnimatedStyle(() => ({
-        transform: [{scale: buttonScale.get()}],
-        opacity: buttonOpacity.get(),
-    }));
+    useEffect(() => {
+        setShouldHideButton(!(isPaidAnimationRunning || isApprovedAnimationRunning));
+    }, [isPaidAnimationRunning, isApprovedAnimationRunning]);
+
+    const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
+    const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
 
     const containerStyles = useAnimatedStyle(() => ({
         height: height.get(),
@@ -50,6 +51,11 @@ function AnimatedSettlementButton({
         ...(shouldAddTopMargin && {marginTop: buttonMarginTop.get()}),
     }));
 
+    const buttonStyles = useAnimatedStyle(() => ({
+        transform: [{scale: buttonScale.get()}],
+        opacity: buttonOpacity.get(),
+    }));
+
     const buttonDisabledStyle =
         isPaidAnimationRunning || isApprovedAnimationRunning
             ? {
@@ -61,13 +67,19 @@ function AnimatedSettlementButton({
     const resetAnimation = useCallback(() => {
         buttonScale.set(1);
         buttonOpacity.set(1);
-        setIsLoading(false);
         height.set(variables.componentSizeNormal);
+        setIsLoading(false);
+        setShouldHideButton(true);
         if (shouldAddTopMargin) {
             buttonMarginTop.set(styles.expenseAndReportPreviewTextButtonContainer.gap);
         }
     }, [buttonScale, buttonOpacity, height, shouldAddTopMargin, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]);
 
+    const handleFadeOutComplete = useCallback(() => {
+        setShouldHideButton(true);
+        onAnimationFinish();
+    }, [onAnimationFinish]);
+
     useEffect(() => {
         if (!isApprovedAnimationRunning && !isPaidAnimationRunning) {
             resetAnimation();
@@ -75,20 +87,17 @@ function AnimatedSettlementButton({
         }
 
         setIsLoading(true);
-        const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
-        const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
 
         const spinnerTimer = setTimeout(() => {
             setIsLoading(false);
 
             buttonScale.set(withDelay(CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
-            buttonOpacity.set(withDelay(CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
-
-            // Wait for the above animation + 1s delay before hiding the component
-            height.set(
+            buttonOpacity.set(
                 withDelay(
-                    totalDelay,
-                    withTiming(willShowPaymentButton ? variables.componentSizeNormal : 0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()),
+                    CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY,
+                    withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => {
+                        runOnJS(handleFadeOutComplete)();
+                    }),
                 ),
             );
 
@@ -96,7 +105,10 @@ function AnimatedSettlementButton({
                 buttonMarginTop.set(
                     withDelay(totalDelay, withTiming(willShowPaymentButton ? styles.expenseAndReportPreviewTextButtonContainer.gap : 0, {duration: CONST.ANIMATION_PAID_DURATION})),
                 );
-                buttonMarginTop.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
+            }
+
+            if (!willShowPaymentButton) {
+                height.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
             }
         }, CONST.ANIMATION_LOADING_DURATION);
 
@@ -104,43 +116,23 @@ function AnimatedSettlementButton({
             clearTimeout(spinnerTimer);
         };
     }, [
+        isApprovedAnimationRunning,
         isPaidAnimationRunning,
         onAnimationFinish,
+        willShowPaymentButton,
+        resetAnimation,
         buttonScale,
         buttonOpacity,
-        resetAnimation,
-        isApprovedAnimationRunning,
-        canIOUBePaid,
         buttonMarginTop,
-        styles.expenseAndReportPreviewTextButtonContainer.gap,
         height,
         shouldAddTopMargin,
+        styles,
+        totalDelay,
+        handleFadeOutComplete,
     ]);
-
-    const handleLayout = useCallback(
-        (event: LayoutChangeEvent) => {
-            const newWidth = event.nativeEvent.layout.width;
-            if (newWidth !== buttonWidth) {
-                setButtonWidth(newWidth);
-            }
-        },
-        [buttonWidth],
-    );
-
     return (
-        <Animated.View
-            style={[containerStyles, wrapperStyle, buttonStyles]}
-            onLayout={handleLayout}
-        >
-            {isPaidAnimationRunning || isApprovedAnimationRunning ? (
-                <Button
-                    style={[styles.buttonMediumText, {width: buttonWidth}]}
-                    text={isPaidAnimationRunning ? translate('iou.paymentComplete') : translate('iou.approved')}
-                    isLoading={isLoading}
-                    success
-                    icon={isLoading ? undefined : (isPaidAnimationRunning && Expensicons.Checkmark) || Expensicons.ThumbsUp}
-                />
-            ) : (
+        <Animated.View style={[containerStyles, wrapperStyle, buttonStyles]}>
+            {shouldHideButton ? (
                 <SettlementButton
                     // eslint-disable-next-line react/jsx-props-no-spreading
                     {...settlementButtonProps}
@@ -148,6 +140,14 @@ function AnimatedSettlementButton({
                     isDisabled={isPaidAnimationRunning || isApprovedAnimationRunning || isDisabled}
                     disabledStyle={buttonDisabledStyle}
                 />
+            ) : (
+                <Button
+                    style={styles.buttonMediumText}
+                    text={isPaidAnimationRunning ? translate('iou.paymentComplete') : translate('iou.approved')}
+                    isLoading={isLoading}
+                    success
+                    icon={isLoading ? undefined : (isPaidAnimationRunning && Expensicons.Checkmark) || Expensicons.ThumbsUp}
+                />
             )}
         </Animated.View>
     );

From 91ce21f27f5fac289f926c4245b8fb9fdf1da610 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Sat, 18 Jan 2025 23:48:37 +0100
Subject: [PATCH 15/37] eslint fix

---
 src/components/MoneyReportHeader.tsx | 42 ++++++++++++++--------------
 1 file changed, 21 insertions(+), 21 deletions(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 38e8939b15f9..dfc25a1efdeb 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -1,3 +1,8 @@
+import {useRoute} from '@react-navigation/native';
+import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
 import usePaymentAnimations from '@hooks/usePaymentAnimations';
@@ -5,10 +10,10 @@ import usePrevious from '@hooks/usePrevious';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
-import { convertToDisplayString } from '@libs/CurrencyUtils';
+import {convertToDisplayString} from '@libs/CurrencyUtils';
 import Navigation from '@libs/Navigation/Navigation';
-import { getConnectedIntegration, isPolicyAdmin } from '@libs/PolicyUtils';
-import { getOriginalMessage, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction } from '@libs/ReportActionsUtils';
+import {getConnectedIntegration, isPolicyAdmin} from '@libs/PolicyUtils';
+import {getOriginalMessage, isDeletedAction, isMoneyRequestAction, isTrackExpenseAction} from '@libs/ReportActionsUtils';
 import {
     canBeExported,
     canDeleteTransaction,
@@ -39,17 +44,6 @@ import {
     isReceiptBeingScanned,
     shouldShowBrokenConnectionViolation as shouldShowBrokenConnectionViolationTransactionUtils,
 } from '@libs/TransactionUtils';
-import { useRoute } from '@react-navigation/native';
-import CONST from '@src/CONST';
-import useDelegateUserDetails from '@src/hooks/useDelegateUserDetails';
-import ONYXKEYS from '@src/ONYXKEYS';
-import type { Route } from '@src/ROUTES';
-import ROUTES from '@src/ROUTES';
-import SCREENS from '@src/SCREENS';
-import type * as OnyxTypes from '@src/types/onyx';
-import type { PaymentMethodType } from '@src/types/onyx/OriginalMessage';
-import type IconAsset from '@src/types/utils/IconAsset';
-import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
 import variables from '@styles/variables';
 import {
     approveMoneyRequest,
@@ -62,11 +56,17 @@ import {
     payMoneyRequest,
     submitReport,
 } from '@userActions/IOU';
-import { markAsCash as markAsCashAction } from '@userActions/Transaction';
-import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { View } from 'react-native';
-import type { OnyxEntry } from 'react-native-onyx';
-import { useOnyx } from 'react-native-onyx';
+import {markAsCash as markAsCashAction} from '@userActions/Transaction';
+import CONST from '@src/CONST';
+import useDelegateUserDetails from '@src/hooks/useDelegateUserDetails';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {Route} from '@src/ROUTES';
+import ROUTES from '@src/ROUTES';
+import SCREENS from '@src/SCREENS';
+import type * as OnyxTypes from '@src/types/onyx';
+import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage';
+import type IconAsset from '@src/types/utils/IconAsset';
+import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue';
 import BrokenConnectionDescription from './BrokenConnectionDescription';
 import Button from './Button';
 import ConfirmModal from './ConfirmModal';
@@ -75,9 +75,9 @@ import HeaderWithBackButton from './HeaderWithBackButton';
 import Icon from './Icon';
 import * as Expensicons from './Icon/Expensicons';
 import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
-import type { MoneyRequestHeaderStatusBarProps } from './MoneyRequestHeaderStatusBar';
+import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar';
 import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar';
-import type { ActionHandledType } from './ProcessMoneyReportHoldMenu';
+import type {ActionHandledType} from './ProcessMoneyReportHoldMenu';
 import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu';
 import ExportWithDropdownMenu from './ReportActionItem/ExportWithDropdownMenu';
 import AnimatedSettlementButton from './SettlementButton/AnimatedSettlementButton';

From d7aa16b40c0ce0378dfddd550ed63296f6b5ca48 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Sat, 18 Jan 2025 23:54:02 +0100
Subject: [PATCH 16/37] fixing eslint

---
 src/components/MoneyReportHeader.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index dfc25a1efdeb..acb412d05193 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -132,7 +132,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const {reimbursableSpend} = getMoneyRequestSpendBreakdown(moneyRequestReport);
     const isOnHold = isOnHoldTransactionUtils(transaction);
     const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction);
-    const [isDuplicate, setIsDuplicate] = useState(isDuplicateTransactionUtils(transaction?.transactionID));
+    const [isDuplicate, setIsDuplicate] = useState(() => isDuplicateTransactionUtils(transaction?.transactionID));
     const prevIsDuplicate = usePrevious(isDuplicate);
 
     // Only the requestor can delete the request, admins can only edit it.

From 39197686e18f825b7d6e8b97511076d3ad609059 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Thu, 23 Jan 2025 23:39:31 +0100
Subject: [PATCH 17/37] fixing eslint

---
 src/components/MoneyReportHeader.tsx | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 10d2cf7ff245..b79018db7f9b 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -167,7 +167,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
 
     useEffect(() => {
         setIsDuplicate(isDuplicateTransactionUtils(transaction?.transactionID));
-    }, [isPaidAnimationRunning, isApprovedAnimationRunning]);
+    }, [isPaidAnimationRunning, isApprovedAnimationRunning, transaction?.transactionID]);
 
     const getCanIOUBePaid = useCallback(
         (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>
@@ -366,8 +366,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         setIsDeleteRequestModalVisible(false);
     }, [canDeleteRequest]);
 
-    console.log('alllooo', shouldShowSettlementButton, shouldUseNarrowLayout);
-
     return (
         <View style={[styles.pt0]}>
             <HeaderWithBackButton

From 1752bf95d778ee40cf7b0d4cb1b6844ef9ab4391 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Fri, 24 Jan 2025 01:21:10 +0100
Subject: [PATCH 18/37] Making button size large

---
 src/components/DecisionModal.tsx     |  2 ++
 src/components/MoneyReportHeader.tsx | 12 ++----------
 2 files changed, 4 insertions(+), 10 deletions(-)

diff --git a/src/components/DecisionModal.tsx b/src/components/DecisionModal.tsx
index 927ba1ecab11..3d54aec08da2 100644
--- a/src/components/DecisionModal.tsx
+++ b/src/components/DecisionModal.tsx
@@ -62,12 +62,14 @@ function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, o
                         onPress={onFirstOptionSubmit}
                         pressOnEnter
                         text={firstOptionText}
+                        large
                     />
                 )}
                 <Button
                     style={[styles.mt3, styles.noSelect]}
                     onPress={onSecondOptionSubmit}
                     text={secondOptionText}
+                    large
                 />
             </View>
         </Modal>
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index b79018db7f9b..d0525e282adc 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -133,8 +133,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const {reimbursableSpend} = getMoneyRequestSpendBreakdown(moneyRequestReport);
     const isOnHold = isOnHoldTransactionUtils(transaction);
     const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction);
-    const [isDuplicate, setIsDuplicate] = useState(() => isDuplicateTransactionUtils(transaction?.transactionID));
-    const prevIsDuplicate = usePrevious(isDuplicate);
+    const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID);
 
     // Only the requestor can delete the request, admins can only edit it.
     const isActionOwner =
@@ -160,14 +159,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const isArchivedReport = isArchivedReportWithID(moneyRequestReport?.reportID);
     const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: getArchiveReason});
     const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
-    const shouldShowIsDuplicate = useMemo(
-        () => isDuplicate || (prevIsDuplicate && (isPaidAnimationRunning || isApprovedAnimationRunning)),
-        [isApprovedAnimationRunning, isDuplicate, isPaidAnimationRunning, prevIsDuplicate],
-    );
-
-    useEffect(() => {
-        setIsDuplicate(isDuplicateTransactionUtils(transaction?.transactionID));
-    }, [isPaidAnimationRunning, isApprovedAnimationRunning, transaction?.transactionID]);
+    const shouldShowIsDuplicate = useMemo(() => isDuplicate, [isDuplicate]);
 
     const getCanIOUBePaid = useCallback(
         (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>

From 0c915306248d9355aab8eeb43ccf788375a56ded Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Fri, 24 Jan 2025 01:24:21 +0100
Subject: [PATCH 19/37] Fixing eslint errs

---
 src/components/MoneyReportHeader.tsx | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index d0525e282adc..035c314c3304 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -6,7 +6,6 @@ import {useOnyx} from 'react-native-onyx';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
 import usePaymentAnimations from '@hooks/usePaymentAnimations';
-import usePrevious from '@hooks/usePrevious';
 import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';

From 073ff3006a13603297384a30b1e030ff3bd6b6a4 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Wed, 29 Jan 2025 04:15:06 +0100
Subject: [PATCH 20/37] Tweaking the pay/approve animations

---
 src/components/DecisionModal.tsx              |  6 +-
 src/components/MoneyReportHeader.tsx          | 16 +++-
 .../AnimatedSettlementButton.tsx              | 77 ++++++++++++-------
 3 files changed, 67 insertions(+), 32 deletions(-)

diff --git a/src/components/DecisionModal.tsx b/src/components/DecisionModal.tsx
index 3d54aec08da2..1872ccb2b6fc 100644
--- a/src/components/DecisionModal.tsx
+++ b/src/components/DecisionModal.tsx
@@ -47,7 +47,7 @@ function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, o
         >
             <View style={[styles.m5]}>
                 <View>
-                    <View style={[styles.flexRow, styles.mb4]}>
+                    <View style={[styles.flexRow, styles.mb5]}>
                         <Header
                             title={title}
                             containerStyles={[styles.alignItemsCenter]}
@@ -58,7 +58,7 @@ function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, o
                 {!!firstOptionText && (
                     <Button
                         success
-                        style={[styles.mt4]}
+                        style={[styles.mt5]}
                         onPress={onFirstOptionSubmit}
                         pressOnEnter
                         text={firstOptionText}
@@ -66,7 +66,7 @@ function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, o
                     />
                 )}
                 <Button
-                    style={[styles.mt3, styles.noSelect]}
+                    style={[styles.mt5, styles.noSelect]}
                     onPress={onSecondOptionSubmit}
                     text={secondOptionText}
                     large
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 7e344bf2c318..76d2566b998b 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -135,6 +135,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const isOnHold = isOnHoldTransactionUtils(transaction);
     const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction);
     const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID);
+    const [hideReviewDuplicates, setHideReviewDuplicates] = useState(false);
 
     // Only the requestor can delete the request, admins can only edit it.
     const isActionOwner =
@@ -160,7 +161,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const isArchivedReport = isArchivedReportWithID(moneyRequestReport?.reportID);
     const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: getArchiveReason});
     const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
-    const shouldShowIsDuplicate = useMemo(() => isDuplicate, [isDuplicate]);
 
     const getCanIOUBePaid = useCallback(
         (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>
@@ -360,6 +360,14 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         setIsDeleteRequestModalVisible(false);
     }, [canDeleteRequest]);
 
+    useEffect(() => {
+        setHideReviewDuplicates(false);
+    }, [moneyRequestReport?.reportID]);
+
+    const handleLoadingEnd = useCallback(() => {
+        setHideReviewDuplicates(true);
+    }, []);
+
     return (
         <View style={[styles.pt0, styles.borderBottom]}>
             <HeaderWithBackButton
@@ -373,7 +381,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                 onBackButtonPress={onBackButtonPress}
                 shouldShowBorderBottom={false}
             >
-                {shouldShowIsDuplicate && !shouldUseNarrowLayout && (
+                {isDuplicate && !hideReviewDuplicates && !shouldUseNarrowLayout && (
                     <View style={[shouldDuplicateButtonBeSuccess ? styles.ml2 : styles.mh2]}>
                         <Button
                             success={shouldDuplicateButtonBeSuccess}
@@ -392,6 +400,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                             isPaidAnimationRunning={isPaidAnimationRunning}
                             isApprovedAnimationRunning={isApprovedAnimationRunning}
                             onAnimationFinish={stopAnimation}
+                            onLoadingEnd={handleLoadingEnd}
                             canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                             onlyShowPayElsewhere={onlyShowPayElsewhere}
                             currency={moneyRequestReport?.currency}
@@ -446,7 +455,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
             {!!isMoreContentShown && (
                 <View style={[styles.dFlex, styles.flexColumn, shouldAddGapToContents && styles.gap3, styles.pb3, styles.ph5]}>
                     <View style={[styles.dFlex, styles.w100, styles.flexRow, styles.gap3]}>
-                        {shouldShowIsDuplicate && shouldUseNarrowLayout && (
+                        {isDuplicate && !hideReviewDuplicates && shouldUseNarrowLayout && (
                             <Button
                                 success={shouldDuplicateButtonBeSuccess}
                                 text={translate('iou.reviewDuplicates')}
@@ -462,6 +471,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                                 isPaidAnimationRunning={isPaidAnimationRunning}
                                 isApprovedAnimationRunning={isApprovedAnimationRunning}
                                 onAnimationFinish={stopAnimation}
+                                onLoadingEnd={handleLoadingEnd}
                                 canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                                 wrapperStyle={[styles.flex1]}
                                 onlyShowPayElsewhere={onlyShowPayElsewhere}
diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 73a34d8d4f83..23c4e33da61b 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,4 +1,5 @@
 import React, {useCallback, useEffect, useState} from 'react';
+import type {LayoutChangeEvent} from 'react-native';
 import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -11,7 +12,9 @@ import type SettlementButtonProps from './types';
 
 type AnimatedSettlementButtonProps = SettlementButtonProps & {
     isPaidAnimationRunning: boolean;
+    shouldUseSuccessStyle?: boolean;
     onAnimationFinish: () => void;
+    onLoadingEnd?: () => void;
     isApprovedAnimationRunning: boolean;
     shouldAddTopMargin?: boolean;
     canIOUBePaid: boolean;
@@ -20,7 +23,9 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & {
 function AnimatedSettlementButton({
     isPaidAnimationRunning,
     isApprovedAnimationRunning,
+    shouldUseSuccessStyle,
     onAnimationFinish,
+    onLoadingEnd,
     shouldAddTopMargin = false,
     isDisabled,
     canIOUBePaid,
@@ -32,15 +37,13 @@ function AnimatedSettlementButton({
 
     const buttonScale = useSharedValue(1);
     const buttonOpacity = useSharedValue(1);
-    const [shouldHideButton, setShouldHideButton] = useState(true);
     const [isLoading, setIsLoading] = useState(false);
+    const [stableShouldUseSuccessStyle, setStableShouldUseSuccessStyle] = useState(shouldUseSuccessStyle);
+    const [buttonWidth, setButtonWidth] = useState<number | null>(null);
+
     const height = useSharedValue<number>(variables.componentSizeNormal);
     const buttonMarginTop = useSharedValue<number>(styles.expenseAndReportPreviewTextButtonContainer.gap);
-
-    useEffect(() => {
-        setShouldHideButton(!(isPaidAnimationRunning || isApprovedAnimationRunning));
-    }, [isPaidAnimationRunning, isApprovedAnimationRunning]);
-
+    const isAnimationRunning = isPaidAnimationRunning || isApprovedAnimationRunning;
     const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
     const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
 
@@ -56,27 +59,24 @@ function AnimatedSettlementButton({
         opacity: buttonOpacity.get(),
     }));
 
-    const buttonDisabledStyle =
-        isPaidAnimationRunning || isApprovedAnimationRunning
-            ? {
-                  opacity: 1,
-                  ...styles.cursorDefault,
-              }
-            : undefined;
+    const buttonDisabledStyle = isAnimationRunning
+        ? {
+              opacity: 1,
+              ...styles.cursorDefault,
+          }
+        : undefined;
 
     const resetAnimation = useCallback(() => {
         buttonScale.set(1);
         buttonOpacity.set(1);
         height.set(variables.componentSizeNormal);
         setIsLoading(false);
-        setShouldHideButton(true);
         if (shouldAddTopMargin) {
             buttonMarginTop.set(styles.expenseAndReportPreviewTextButtonContainer.gap);
         }
     }, [buttonScale, buttonOpacity, height, shouldAddTopMargin, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]);
 
     const handleFadeOutComplete = useCallback(() => {
-        setShouldHideButton(true);
         onAnimationFinish();
     }, [onAnimationFinish]);
 
@@ -110,6 +110,8 @@ function AnimatedSettlementButton({
             if (!willShowPaymentButton) {
                 height.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
             }
+
+            onLoadingEnd?.();
         }, CONST.ANIMATION_LOADING_DURATION);
 
         return () => {
@@ -129,25 +131,48 @@ function AnimatedSettlementButton({
         styles,
         totalDelay,
         handleFadeOutComplete,
+        onLoadingEnd,
     ]);
+
+    useEffect(() => {
+        if (isAnimationRunning) {
+            return;
+        }
+        setStableShouldUseSuccessStyle(shouldUseSuccessStyle);
+    }, [isAnimationRunning, shouldUseSuccessStyle]);
+
+    const handleLayout = useCallback(
+        (event: LayoutChangeEvent) => {
+            const newWidth = event.nativeEvent.layout.width;
+            if (newWidth !== buttonWidth) {
+                setButtonWidth(newWidth);
+            }
+        },
+        [buttonWidth],
+    );
+
     return (
-        <Animated.View style={[containerStyles, wrapperStyle, buttonStyles]}>
-            {shouldHideButton ? (
+        <Animated.View
+            style={[containerStyles, wrapperStyle, buttonStyles]}
+            onLayout={handleLayout}
+        >
+            {isAnimationRunning ? (
+                <Button
+                    style={[styles.buttonMediumText, isLoading && {width: buttonWidth}]}
+                    text={isPaidAnimationRunning ? translate('iou.paymentComplete') : translate('iou.approved')}
+                    isLoading={isLoading}
+                    success={stableShouldUseSuccessStyle}
+                    icon={isLoading ? undefined : (isPaidAnimationRunning && Expensicons.Checkmark) || Expensicons.ThumbsUp}
+                />
+            ) : (
                 <SettlementButton
                     // eslint-disable-next-line react/jsx-props-no-spreading
                     {...settlementButtonProps}
+                    shouldUseSuccessStyle={shouldUseSuccessStyle}
                     wrapperStyle={wrapperStyle}
-                    isDisabled={isPaidAnimationRunning || isApprovedAnimationRunning || isDisabled}
+                    isDisabled={isAnimationRunning || isDisabled}
                     disabledStyle={buttonDisabledStyle}
                 />
-            ) : (
-                <Button
-                    style={styles.buttonMediumText}
-                    text={isPaidAnimationRunning ? translate('iou.paymentComplete') : translate('iou.approved')}
-                    isLoading={isLoading}
-                    success
-                    icon={isLoading ? undefined : (isPaidAnimationRunning && Expensicons.Checkmark) || Expensicons.ThumbsUp}
-                />
             )}
         </Animated.View>
     );

From 351a978755be66b1437996b188b2c33536a56f4c Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Wed, 29 Jan 2025 04:40:46 +0100
Subject: [PATCH 21/37] removing shouldUseSuccessStyle from
 AnimatedSettlementButton

---
 .../SettlementButton/AnimatedSettlementButton.tsx | 15 ++-------------
 1 file changed, 2 insertions(+), 13 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 23c4e33da61b..f9acc9880c20 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -12,7 +12,6 @@ import type SettlementButtonProps from './types';
 
 type AnimatedSettlementButtonProps = SettlementButtonProps & {
     isPaidAnimationRunning: boolean;
-    shouldUseSuccessStyle?: boolean;
     onAnimationFinish: () => void;
     onLoadingEnd?: () => void;
     isApprovedAnimationRunning: boolean;
@@ -23,7 +22,6 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & {
 function AnimatedSettlementButton({
     isPaidAnimationRunning,
     isApprovedAnimationRunning,
-    shouldUseSuccessStyle,
     onAnimationFinish,
     onLoadingEnd,
     shouldAddTopMargin = false,
@@ -38,7 +36,6 @@ function AnimatedSettlementButton({
     const buttonScale = useSharedValue(1);
     const buttonOpacity = useSharedValue(1);
     const [isLoading, setIsLoading] = useState(false);
-    const [stableShouldUseSuccessStyle, setStableShouldUseSuccessStyle] = useState(shouldUseSuccessStyle);
     const [buttonWidth, setButtonWidth] = useState<number | null>(null);
 
     const height = useSharedValue<number>(variables.componentSizeNormal);
@@ -128,19 +125,12 @@ function AnimatedSettlementButton({
         buttonMarginTop,
         height,
         shouldAddTopMargin,
-        styles,
+        styles.expenseAndReportPreviewTextButtonContainer,
         totalDelay,
         handleFadeOutComplete,
         onLoadingEnd,
     ]);
 
-    useEffect(() => {
-        if (isAnimationRunning) {
-            return;
-        }
-        setStableShouldUseSuccessStyle(shouldUseSuccessStyle);
-    }, [isAnimationRunning, shouldUseSuccessStyle]);
-
     const handleLayout = useCallback(
         (event: LayoutChangeEvent) => {
             const newWidth = event.nativeEvent.layout.width;
@@ -161,14 +151,13 @@ function AnimatedSettlementButton({
                     style={[styles.buttonMediumText, isLoading && {width: buttonWidth}]}
                     text={isPaidAnimationRunning ? translate('iou.paymentComplete') : translate('iou.approved')}
                     isLoading={isLoading}
-                    success={stableShouldUseSuccessStyle}
+                    success
                     icon={isLoading ? undefined : (isPaidAnimationRunning && Expensicons.Checkmark) || Expensicons.ThumbsUp}
                 />
             ) : (
                 <SettlementButton
                     // eslint-disable-next-line react/jsx-props-no-spreading
                     {...settlementButtonProps}
-                    shouldUseSuccessStyle={shouldUseSuccessStyle}
                     wrapperStyle={wrapperStyle}
                     isDisabled={isAnimationRunning || isDisabled}
                     disabledStyle={buttonDisabledStyle}

From ba537b3f9671f6a348e537de5be66909fb2fbbae Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Thu, 30 Jan 2025 01:07:07 +0100
Subject: [PATCH 22/37] Using useLayoutEffect instead of useEffect to avoid the
 brief text change in the settlement button when approved

---
 src/components/SettlementButton/AnimatedSettlementButton.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index f9acc9880c20..47b988bcd17d 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useEffect, useState} from 'react';
+import React, {useCallback, useLayoutEffect, useState} from 'react';
 import type {LayoutChangeEvent} from 'react-native';
 import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
 import Button from '@components/Button';
@@ -77,7 +77,7 @@ function AnimatedSettlementButton({
         onAnimationFinish();
     }, [onAnimationFinish]);
 
-    useEffect(() => {
+    useLayoutEffect(() => {
         if (!isApprovedAnimationRunning && !isPaidAnimationRunning) {
             resetAnimation();
             return;

From 7c2135bc14f99066f18d6b3d603460150fc0d3e9 Mon Sep 17 00:00:00 2001
From: Abdelrahman Khattab <59809993+abzokhattab@users.noreply.github.com>
Date: Mon, 3 Feb 2025 19:38:19 +0100
Subject: [PATCH 23/37] Reducing vertical margin

Co-authored-by: wentao <ntdiary@163.com>
---
 src/components/DecisionModal.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/DecisionModal.tsx b/src/components/DecisionModal.tsx
index 1872ccb2b6fc..7241dabb1dea 100644
--- a/src/components/DecisionModal.tsx
+++ b/src/components/DecisionModal.tsx
@@ -45,7 +45,7 @@ function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, o
             isVisible={isVisible}
             type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
         >
-            <View style={[styles.m5]}>
+            <View style={[styles.mv1, styles.mh5]}>
                 <View>
                     <View style={[styles.flexRow, styles.mb5]}>
                         <Header

From a2370b91c9fddef69e095f742224bde0af112e76 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Tue, 11 Feb 2025 22:17:32 +0100
Subject: [PATCH 24/37] Use keyframe animation

---
 src/CONST.ts                                  |   1 +
 src/components/MoneyReportHeader.tsx          |  18 +-
 .../AnimatedSettlementButton.tsx              | 178 ++++++------------
 3 files changed, 67 insertions(+), 130 deletions(-)

diff --git a/src/CONST.ts b/src/CONST.ts
index 241b1bae385e..8061650841ea 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -381,6 +381,7 @@ const CONST = {
     // Multiplier for gyroscope animation in order to make it a bit more subtle
     ANIMATION_GYROSCOPE_VALUE: 0.4,
     ANIMATION_PAID_DURATION: 200,
+    ANIMATION_HIDE_DELAY: 50,
     ANIMATION_PAID_CHECKMARK_DELAY: 300,
     ANIMATION_LOADING_DURATION: 500,
     ANIMATION_THUMBSUP_DURATION: 250,
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 7ff009ea7630..2de196e66752 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -30,6 +30,7 @@ import {
     isClosedExpenseReportWithNoExpenses,
     isCurrentUserSubmitter,
     isInvoiceReport,
+    isReportApproved,
     navigateBackOnDeleteTransaction,
     reportTransactionsSelector,
 } from '@libs/ReportUtils';
@@ -133,8 +134,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const {reimbursableSpend} = getMoneyRequestSpendBreakdown(moneyRequestReport);
     const isOnHold = isOnHoldTransactionUtils(transaction);
     const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction);
-    const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID);
-    const [hideReviewDuplicates, setHideReviewDuplicates] = useState(false);
+    const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID) && !isReportApproved({report: moneyRequestReport});
 
     // Only the requestor can delete the request, admins can only edit it.
     const isActionOwner =
@@ -359,14 +359,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         setIsDeleteRequestModalVisible(false);
     }, [canDeleteRequest]);
 
-    useEffect(() => {
-        setHideReviewDuplicates(false);
-    }, [moneyRequestReport?.reportID]);
-
-    const handleLoadingEnd = useCallback(() => {
-        setHideReviewDuplicates(true);
-    }, []);
-
     return (
         <View style={[styles.pt0, styles.borderBottom]}>
             <HeaderWithBackButton
@@ -380,7 +372,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                 onBackButtonPress={onBackButtonPress}
                 shouldShowBorderBottom={false}
             >
-                {isDuplicate && !hideReviewDuplicates && !shouldUseNarrowLayout && (
+                {isDuplicate && !shouldUseNarrowLayout && (
                     <View style={[shouldDuplicateButtonBeSuccess ? styles.ml2 : styles.mh2]}>
                         <Button
                             success={shouldDuplicateButtonBeSuccess}
@@ -398,7 +390,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                             isPaidAnimationRunning={isPaidAnimationRunning}
                             isApprovedAnimationRunning={isApprovedAnimationRunning}
                             onAnimationFinish={stopAnimation}
-                            onLoadingEnd={handleLoadingEnd}
                             canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                             onlyShowPayElsewhere={onlyShowPayElsewhere}
                             currency={moneyRequestReport?.currency}
@@ -453,7 +444,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
             {!!isMoreContentShown && (
                 <View style={[styles.dFlex, styles.flexColumn, shouldAddGapToContents && styles.gap3, styles.pb3, styles.ph5]}>
                     <View style={[styles.dFlex, styles.w100, styles.flexRow, styles.gap3]}>
-                        {isDuplicate && !hideReviewDuplicates && shouldUseNarrowLayout && (
+                        {isDuplicate && shouldUseNarrowLayout && (
                             <Button
                                 success={shouldDuplicateButtonBeSuccess}
                                 text={translate('iou.reviewDuplicates')}
@@ -468,7 +459,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                                 isPaidAnimationRunning={isPaidAnimationRunning}
                                 isApprovedAnimationRunning={isApprovedAnimationRunning}
                                 onAnimationFinish={stopAnimation}
-                                onLoadingEnd={handleLoadingEnd}
                                 canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                                 wrapperStyle={[styles.flex1]}
                                 onlyShowPayElsewhere={onlyShowPayElsewhere}
diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 47b988bcd17d..fff084708fb2 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,6 +1,5 @@
-import React, {useCallback, useLayoutEffect, useState} from 'react';
-import type {LayoutChangeEvent} from 'react-native';
-import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
+import React, {useEffect, useMemo} from 'react';
+import Animated, {Keyframe, runOnJS} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import useLocalize from '@hooks/useLocalize';
@@ -33,134 +32,81 @@ function AnimatedSettlementButton({
     const styles = useThemeStyles();
     const {translate} = useLocalize();
 
-    const buttonScale = useSharedValue(1);
-    const buttonOpacity = useSharedValue(1);
-    const [isLoading, setIsLoading] = useState(false);
-    const [buttonWidth, setButtonWidth] = useState<number | null>(null);
-
-    const height = useSharedValue<number>(variables.componentSizeNormal);
-    const buttonMarginTop = useSharedValue<number>(styles.expenseAndReportPreviewTextButtonContainer.gap);
     const isAnimationRunning = isPaidAnimationRunning || isApprovedAnimationRunning;
-    const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
-    const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
-
-    const containerStyles = useAnimatedStyle(() => ({
-        height: height.get(),
-        justifyContent: 'center',
-        overflow: 'hidden',
-        ...(shouldAddTopMargin && {marginTop: buttonMarginTop.get()}),
-    }));
-
-    const buttonStyles = useAnimatedStyle(() => ({
-        transform: [{scale: buttonScale.get()}],
-        opacity: buttonOpacity.get(),
-    }));
-
-    const buttonDisabledStyle = isAnimationRunning
-        ? {
-              opacity: 1,
-              ...styles.cursorDefault,
-          }
-        : undefined;
+    const buttonDuration = CONST.ANIMATION_PAID_DURATION;
+    const marginTop = styles.expenseAndReportPreviewTextButtonContainer.gap;
+    const [canShow, setCanShow] = React.useState(true);
+
+    const buttonAnimation = useMemo(
+        () =>
+            new Keyframe({
+                from: {
+                    opacity: 1,
+                    transform: [{scale: 1}],
+                },
+                to: {
+                    opacity: 0,
+                    transform: [{scale: 0}],
+                },
+            })
+                .duration(buttonDuration)
+                .withCallback(() => {
+                    'worklet';
+
+                    runOnJS(onAnimationFinish)();
+                }),
+        [buttonDuration, onAnimationFinish],
+    );
 
-    const resetAnimation = useCallback(() => {
-        buttonScale.set(1);
-        buttonOpacity.set(1);
-        height.set(variables.componentSizeNormal);
-        setIsLoading(false);
-        if (shouldAddTopMargin) {
-            buttonMarginTop.set(styles.expenseAndReportPreviewTextButtonContainer.gap);
-        }
-    }, [buttonScale, buttonOpacity, height, shouldAddTopMargin, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]);
+    const containerExitingAnimation = useMemo(
+        () =>
+            new Keyframe({
+                from: {
+                    height: variables.componentSizeNormal,
+                },
+                to: {
+                    height: 0,
+                },
+            }).duration(buttonDuration * 2),
+        [buttonDuration],
+    );
 
-    const handleFadeOutComplete = useCallback(() => {
-        onAnimationFinish();
-    }, [onAnimationFinish]);
+    let icon;
+    if (isApprovedAnimationRunning) {
+        icon = Expensicons.ThumbsUp;
+    } else if (isPaidAnimationRunning) {
+        icon = Expensicons.Checkmark;
+    }
 
-    useLayoutEffect(() => {
-        if (!isApprovedAnimationRunning && !isPaidAnimationRunning) {
-            resetAnimation();
+    useEffect(() => {
+        if (!isAnimationRunning) {
+            setCanShow(true);
             return;
         }
-
-        setIsLoading(true);
-
-        const spinnerTimer = setTimeout(() => {
-            setIsLoading(false);
-
-            buttonScale.set(withDelay(CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
-            buttonOpacity.set(
-                withDelay(
-                    CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY,
-                    withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => {
-                        runOnJS(handleFadeOutComplete)();
-                    }),
-                ),
-            );
-
-            if (shouldAddTopMargin) {
-                buttonMarginTop.set(
-                    withDelay(totalDelay, withTiming(willShowPaymentButton ? styles.expenseAndReportPreviewTextButtonContainer.gap : 0, {duration: CONST.ANIMATION_PAID_DURATION})),
-                );
-            }
-
-            if (!willShowPaymentButton) {
-                height.set(withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})));
-            }
-
-            onLoadingEnd?.();
-        }, CONST.ANIMATION_LOADING_DURATION);
-
-        return () => {
-            clearTimeout(spinnerTimer);
-        };
-    }, [
-        isApprovedAnimationRunning,
-        isPaidAnimationRunning,
-        onAnimationFinish,
-        willShowPaymentButton,
-        resetAnimation,
-        buttonScale,
-        buttonOpacity,
-        buttonMarginTop,
-        height,
-        shouldAddTopMargin,
-        styles.expenseAndReportPreviewTextButtonContainer,
-        totalDelay,
-        handleFadeOutComplete,
-        onLoadingEnd,
-    ]);
-
-    const handleLayout = useCallback(
-        (event: LayoutChangeEvent) => {
-            const newWidth = event.nativeEvent.layout.width;
-            if (newWidth !== buttonWidth) {
-                setButtonWidth(newWidth);
-            }
-        },
-        [buttonWidth],
-    );
+        const frame = setTimeout(() => setCanShow(false), CONST.ANIMATION_HIDE_DELAY);
+        return () => clearTimeout(frame);
+    }, [isAnimationRunning]);
 
     return (
         <Animated.View
-            style={[containerStyles, wrapperStyle, buttonStyles]}
-            onLayout={handleLayout}
+            exiting={isAnimationRunning ? containerExitingAnimation : undefined}
+            style={[shouldAddTopMargin && {marginTop}, {height: variables.componentSizeNormal}, styles.justifyContentCenter, styles.overflowHidden, wrapperStyle]}
         >
-            {isAnimationRunning ? (
-                <Button
-                    style={[styles.buttonMediumText, isLoading && {width: buttonWidth}]}
-                    text={isPaidAnimationRunning ? translate('iou.paymentComplete') : translate('iou.approved')}
-                    isLoading={isLoading}
-                    success
-                    icon={isLoading ? undefined : (isPaidAnimationRunning && Expensicons.Checkmark) || Expensicons.ThumbsUp}
-                />
-            ) : (
+            {isAnimationRunning && canShow && (
+                <Animated.View exiting={buttonAnimation}>
+                    <Button
+                        text={isApprovedAnimationRunning ? translate('iou.approved') : translate('iou.paymentComplete')}
+                        success
+                        icon={icon}
+                    />
+                </Animated.View>
+            )}
+            {!isAnimationRunning && (
                 <SettlementButton
                     // eslint-disable-next-line react/jsx-props-no-spreading
                     {...settlementButtonProps}
                     wrapperStyle={wrapperStyle}
                     isDisabled={isAnimationRunning || isDisabled}
-                    disabledStyle={buttonDisabledStyle}
                 />
             )}
         </Animated.View>

From 49c3d2d3a8a622c4899a2bdfe57bc9f76efee668 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Wed, 12 Feb 2025 16:11:04 +0100
Subject: [PATCH 25/37] Prevent the container height shrinking on approval

---
 src/CONST.ts                                             | 2 +-
 .../SettlementButton/AnimatedSettlementButton.tsx        | 9 ++++-----
 2 files changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/CONST.ts b/src/CONST.ts
index 8061650841ea..4734b80b1f39 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -381,7 +381,7 @@ const CONST = {
     // Multiplier for gyroscope animation in order to make it a bit more subtle
     ANIMATION_GYROSCOPE_VALUE: 0.4,
     ANIMATION_PAID_DURATION: 200,
-    ANIMATION_HIDE_DELAY: 50,
+    ANIMATION_HIDE_DELAY: 200,
     ANIMATION_PAID_CHECKMARK_DELAY: 300,
     ANIMATION_LOADING_DURATION: 500,
     ANIMATION_THUMBSUP_DURATION: 250,
diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index fff084708fb2..b1e913e96dbc 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -12,7 +12,6 @@ import type SettlementButtonProps from './types';
 type AnimatedSettlementButtonProps = SettlementButtonProps & {
     isPaidAnimationRunning: boolean;
     onAnimationFinish: () => void;
-    onLoadingEnd?: () => void;
     isApprovedAnimationRunning: boolean;
     shouldAddTopMargin?: boolean;
     canIOUBePaid: boolean;
@@ -22,7 +21,6 @@ function AnimatedSettlementButton({
     isPaidAnimationRunning,
     isApprovedAnimationRunning,
     onAnimationFinish,
-    onLoadingEnd,
     shouldAddTopMargin = false,
     isDisabled,
     canIOUBePaid,
@@ -33,8 +31,9 @@ function AnimatedSettlementButton({
     const {translate} = useLocalize();
 
     const isAnimationRunning = isPaidAnimationRunning || isApprovedAnimationRunning;
-    const buttonDuration = CONST.ANIMATION_PAID_DURATION;
+    const buttonDuration = isPaidAnimationRunning ? CONST.ANIMATION_PAID_DURATION : CONST.ANIMATION_THUMBSUP_DURATION;
     const marginTop = styles.expenseAndReportPreviewTextButtonContainer.gap;
+    const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
     const [canShow, setCanShow] = React.useState(true);
 
     const buttonAnimation = useMemo(
@@ -67,7 +66,7 @@ function AnimatedSettlementButton({
                 to: {
                     height: 0,
                 },
-            }).duration(buttonDuration * 2),
+            }).duration(buttonDuration),
         [buttonDuration],
     );
 
@@ -89,7 +88,7 @@ function AnimatedSettlementButton({
 
     return (
         <Animated.View
-            exiting={isAnimationRunning ? containerExitingAnimation : undefined}
+            exiting={isAnimationRunning && !willShowPaymentButton ? containerExitingAnimation : undefined}
             style={[shouldAddTopMargin && {marginTop}, {height: variables.componentSizeNormal}, styles.justifyContentCenter, styles.overflowHidden, wrapperStyle]}
         >
             {isAnimationRunning && canShow && (

From f396e51887508383c033102240e47d05192deed4 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Wed, 12 Feb 2025 20:08:17 +0100
Subject: [PATCH 26/37] reverting the upper and lower margin for descision
 modal

---
 src/components/DecisionModal.tsx | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/DecisionModal.tsx b/src/components/DecisionModal.tsx
index 7241dabb1dea..1872ccb2b6fc 100644
--- a/src/components/DecisionModal.tsx
+++ b/src/components/DecisionModal.tsx
@@ -45,7 +45,7 @@ function DecisionModal({title, prompt = '', firstOptionText, secondOptionText, o
             isVisible={isVisible}
             type={isSmallScreenWidth ? CONST.MODAL.MODAL_TYPE.BOTTOM_DOCKED : CONST.MODAL.MODAL_TYPE.CONFIRM}
         >
-            <View style={[styles.mv1, styles.mh5]}>
+            <View style={[styles.m5]}>
                 <View>
                     <View style={[styles.flexRow, styles.mb5]}>
                         <Header

From 6d464e560de640a0eeb070984a395aa5f0321f8e Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Wed, 12 Feb 2025 20:13:25 +0100
Subject: [PATCH 27/37] using useAnimatedStyle for the container styles

---
 .../AnimatedSettlementButton.tsx                | 17 +++++++++++++----
 1 file changed, 13 insertions(+), 4 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index b1e913e96dbc..250392791ba6 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,5 +1,7 @@
 import React, {useEffect, useMemo} from 'react';
 import Animated, {Keyframe, runOnJS} from 'react-native-reanimated';
+import {useAnimatedStyle} from 'react-native-reanimated';
+import {useSharedValue} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import useLocalize from '@hooks/useLocalize';
@@ -32,9 +34,16 @@ function AnimatedSettlementButton({
 
     const isAnimationRunning = isPaidAnimationRunning || isApprovedAnimationRunning;
     const buttonDuration = isPaidAnimationRunning ? CONST.ANIMATION_PAID_DURATION : CONST.ANIMATION_THUMBSUP_DURATION;
-    const marginTop = styles.expenseAndReportPreviewTextButtonContainer.gap;
-    const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
+    const buttonMarginTop = useSharedValue<number>(styles.expenseAndReportPreviewTextButtonContainer.gap);
     const [canShow, setCanShow] = React.useState(true);
+    const height = useSharedValue<number>(variables.componentSizeNormal);
+
+    const containerStyles = useAnimatedStyle(() => ({
+        height: height.get(),
+        justifyContent: 'center',
+        overflow: 'hidden',
+        ...(shouldAddTopMargin && {marginTop: buttonMarginTop.get()}),
+    }));
 
     const buttonAnimation = useMemo(
         () =>
@@ -88,8 +97,8 @@ function AnimatedSettlementButton({
 
     return (
         <Animated.View
-            exiting={isAnimationRunning && !willShowPaymentButton ? containerExitingAnimation : undefined}
-            style={[shouldAddTopMargin && {marginTop}, {height: variables.componentSizeNormal}, styles.justifyContentCenter, styles.overflowHidden, wrapperStyle]}
+            exiting={isAnimationRunning ? containerExitingAnimation : undefined}
+            style={[containerStyles, wrapperStyle]}
         >
             {isAnimationRunning && canShow && (
                 <Animated.View exiting={buttonAnimation}>

From 1517d2b6cae3963ceef827a93bb58072be3e961b Mon Sep 17 00:00:00 2001
From: Abdelrahman Khattab <abzokhattab@gmail.com>
Date: Thu, 13 Feb 2025 09:59:57 +0100
Subject: [PATCH 28/37] fixing eslint

---
 src/components/SettlementButton/AnimatedSettlementButton.tsx | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 250392791ba6..7a5e9c47aa85 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,7 +1,5 @@
 import React, {useEffect, useMemo} from 'react';
-import Animated, {Keyframe, runOnJS} from 'react-native-reanimated';
-import {useAnimatedStyle} from 'react-native-reanimated';
-import {useSharedValue} from 'react-native-reanimated';
+import Animated, {Keyframe, runOnJS, useAnimatedStyle, useSharedValue} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import useLocalize from '@hooks/useLocalize';

From 7bac9a81cf90c722bcbaf38ce4a57e33e276d7d0 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Thu, 13 Feb 2025 23:30:48 +0100
Subject: [PATCH 29/37] Animated button refactoring

---
 src/CONST.ts                                  |  3 +-
 .../AnimatedSettlementButton.tsx              | 59 +++++++++----------
 2 files changed, 30 insertions(+), 32 deletions(-)

diff --git a/src/CONST.ts b/src/CONST.ts
index 4734b80b1f39..3f92456a3ba8 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -383,10 +383,9 @@ const CONST = {
     ANIMATION_PAID_DURATION: 200,
     ANIMATION_HIDE_DELAY: 200,
     ANIMATION_PAID_CHECKMARK_DELAY: 300,
-    ANIMATION_LOADING_DURATION: 500,
     ANIMATION_THUMBSUP_DURATION: 250,
     ANIMATION_THUMBSUP_DELAY: 200,
-    ANIMATION_PAID_BUTTON_HIDE_DELAY: 1000,
+    ANIMATION_PAID_BUTTON_HIDE_DELAY: 300,
     BACKGROUND_IMAGE_TRANSITION_DURATION: 1000,
     SCREEN_TRANSITION_END_TIMEOUT: 1000,
     ARROW_HIDE_DELAY: 3000,
diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index 7a5e9c47aa85..e1b445184140 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,5 +1,5 @@
-import React, {useEffect, useMemo} from 'react';
-import Animated, {Keyframe, runOnJS, useAnimatedStyle, useSharedValue} from 'react-native-reanimated';
+import React, {useCallback, useEffect, useMemo} from 'react';
+import Animated, {Keyframe, runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import useLocalize from '@hooks/useLocalize';
@@ -32,9 +32,11 @@ function AnimatedSettlementButton({
 
     const isAnimationRunning = isPaidAnimationRunning || isApprovedAnimationRunning;
     const buttonDuration = isPaidAnimationRunning ? CONST.ANIMATION_PAID_DURATION : CONST.ANIMATION_THUMBSUP_DURATION;
-    const buttonMarginTop = useSharedValue<number>(styles.expenseAndReportPreviewTextButtonContainer.gap);
-    const [canShow, setCanShow] = React.useState(true);
+    const buttonDelay = CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY;
+    const gap = styles.expenseAndReportPreviewTextButtonContainer.gap;
+    const buttonMarginTop = useSharedValue<number>(gap);
     const height = useSharedValue<number>(variables.componentSizeNormal);
+    const [canShow, setCanShow] = React.useState(true);
 
     const containerStyles = useAnimatedStyle(() => ({
         height: height.get(),
@@ -43,6 +45,20 @@ function AnimatedSettlementButton({
         ...(shouldAddTopMargin && {marginTop: buttonMarginTop.get()}),
     }));
 
+    const willShowPaymentButton = canIOUBePaid && isApprovedAnimationRunning;
+    const stretchOutY = useCallback(() => {
+        'worklet';
+
+        if (shouldAddTopMargin) {
+            buttonMarginTop.set(withTiming(willShowPaymentButton ? gap : 0, {duration: buttonDuration}));
+        }
+        if (willShowPaymentButton) {
+            runOnJS(onAnimationFinish)();
+            return;
+        }
+        height.set(withTiming(0, {duration: buttonDuration}, () => runOnJS(onAnimationFinish)()));
+    }, [buttonDuration, buttonMarginTop, gap, height, onAnimationFinish, shouldAddTopMargin, willShowPaymentButton]);
+
     const buttonAnimation = useMemo(
         () =>
             new Keyframe({
@@ -55,26 +71,10 @@ function AnimatedSettlementButton({
                     transform: [{scale: 0}],
                 },
             })
+                .delay(buttonDelay)
                 .duration(buttonDuration)
-                .withCallback(() => {
-                    'worklet';
-
-                    runOnJS(onAnimationFinish)();
-                }),
-        [buttonDuration, onAnimationFinish],
-    );
-
-    const containerExitingAnimation = useMemo(
-        () =>
-            new Keyframe({
-                from: {
-                    height: variables.componentSizeNormal,
-                },
-                to: {
-                    height: 0,
-                },
-            }).duration(buttonDuration),
-        [buttonDuration],
+                .withCallback(stretchOutY),
+        [buttonDelay, buttonDuration, stretchOutY],
     );
 
     let icon;
@@ -87,17 +87,16 @@ function AnimatedSettlementButton({
     useEffect(() => {
         if (!isAnimationRunning) {
             setCanShow(true);
+            height.set(variables.componentSizeNormal);
+            buttonMarginTop.set(shouldAddTopMargin ? gap : 0);
             return;
         }
-        const frame = setTimeout(() => setCanShow(false), CONST.ANIMATION_HIDE_DELAY);
-        return () => clearTimeout(frame);
-    }, [isAnimationRunning]);
+        const timer = setTimeout(() => setCanShow(false), CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY);
+        return () => clearTimeout(timer);
+    }, [buttonMarginTop, gap, height, isAnimationRunning, shouldAddTopMargin]);
 
     return (
-        <Animated.View
-            exiting={isAnimationRunning ? containerExitingAnimation : undefined}
-            style={[containerStyles, wrapperStyle]}
-        >
+        <Animated.View style={[containerStyles, wrapperStyle]}>
             {isAnimationRunning && canShow && (
                 <Animated.View exiting={buttonAnimation}>
                     <Button

From 949a1baa90b7b72386ebc01f43da75a9c629855f Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Fri, 14 Feb 2025 13:31:17 +0100
Subject: [PATCH 30/37] Cleaning unused ANIMATION_HIDE_DELAY

---
 src/CONST.ts | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/CONST.ts b/src/CONST.ts
index ef6d1bf4850c..28abe2a028ba 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -382,7 +382,6 @@ const CONST = {
     // Multiplier for gyroscope animation in order to make it a bit more subtle
     ANIMATION_GYROSCOPE_VALUE: 0.4,
     ANIMATION_PAID_DURATION: 200,
-    ANIMATION_HIDE_DELAY: 200,
     ANIMATION_PAID_CHECKMARK_DELAY: 300,
     ANIMATION_THUMBSUP_DURATION: 250,
     ANIMATION_THUMBSUP_DELAY: 200,

From 76c1b848fbbf871602008b5633dafe2fd431212d Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Sun, 16 Feb 2025 23:16:59 +0100
Subject: [PATCH 31/37] removing hidden overflow

---
 src/components/SettlementButton/AnimatedSettlementButton.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index e1b445184140..bd936a9a7e54 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,5 +1,6 @@
 import React, {useCallback, useEffect, useMemo} from 'react';
-import Animated, {Keyframe, runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
+import {View} from 'react-native';
+import Animated, {FadeOut, Keyframe, runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import useLocalize from '@hooks/useLocalize';
@@ -41,7 +42,6 @@ function AnimatedSettlementButton({
     const containerStyles = useAnimatedStyle(() => ({
         height: height.get(),
         justifyContent: 'center',
-        overflow: 'hidden',
         ...(shouldAddTopMargin && {marginTop: buttonMarginTop.get()}),
     }));
 

From 33f75ead723f901123d8cc349cda5a90b5f95ef8 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Sun, 16 Feb 2025 23:26:51 +0100
Subject: [PATCH 32/37] fixing eslint

---
 src/components/SettlementButton/AnimatedSettlementButton.tsx | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index bd936a9a7e54..a95b4df440e7 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,6 +1,5 @@
 import React, {useCallback, useEffect, useMemo} from 'react';
-import {View} from 'react-native';
-import Animated, {FadeOut, Keyframe, runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
+import Animated, {Keyframe, runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
 import useLocalize from '@hooks/useLocalize';

From da2a5530184dc74874d0b57065f521ed1fb35f48 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Fri, 21 Feb 2025 07:54:03 +0100
Subject: [PATCH 33/37] Fixing animation for web

---
 .../SettlementButton/AnimatedSettlementButton.tsx   | 13 ++++++++++---
 1 file changed, 10 insertions(+), 3 deletions(-)

diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx
index a95b4df440e7..388297075082 100644
--- a/src/components/SettlementButton/AnimatedSettlementButton.tsx
+++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useEffect, useMemo} from 'react';
+import React, {useCallback, useEffect, useMemo, useRef} from 'react';
 import Animated, {Keyframe, runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
 import Button from '@components/Button';
 import * as Expensicons from '@components/Icon/Expensicons';
@@ -37,6 +37,8 @@ function AnimatedSettlementButton({
     const buttonMarginTop = useSharedValue<number>(gap);
     const height = useSharedValue<number>(variables.componentSizeNormal);
     const [canShow, setCanShow] = React.useState(true);
+    const [minWidth, setMinWidth] = React.useState<number>(0);
+    const viewRef = useRef<HTMLElement | null>();
 
     const containerStyles = useAnimatedStyle(() => ({
         height: height.get(),
@@ -85,19 +87,24 @@ function AnimatedSettlementButton({
 
     useEffect(() => {
         if (!isAnimationRunning) {
+            setMinWidth(0);
             setCanShow(true);
             height.set(variables.componentSizeNormal);
             buttonMarginTop.set(shouldAddTopMargin ? gap : 0);
             return;
         }
+        setMinWidth(viewRef.current?.getBoundingClientRect?.().width ?? 0);
         const timer = setTimeout(() => setCanShow(false), CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY);
         return () => clearTimeout(timer);
     }, [buttonMarginTop, gap, height, isAnimationRunning, shouldAddTopMargin]);
 
     return (
-        <Animated.View style={[containerStyles, wrapperStyle]}>
+        <Animated.View style={[containerStyles, wrapperStyle, {minWidth}]}>
             {isAnimationRunning && canShow && (
-                <Animated.View exiting={buttonAnimation}>
+                <Animated.View
+                    ref={(el) => (viewRef.current = el as HTMLElement | null)}
+                    exiting={buttonAnimation}
+                >
                     <Button
                         text={isApprovedAnimationRunning ? translate('iou.approved') : translate('iou.paymentComplete')}
                         success

From 5f32d04f01a825ec0219affbe68c6783564fe6eb Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Fri, 21 Feb 2025 08:26:25 +0100
Subject: [PATCH 34/37] Keeping the "review duplicate" button visible until the
 animation is complete

---
 src/components/MoneyReportHeader.tsx | 20 +++++++++++++++-----
 1 file changed, 15 insertions(+), 5 deletions(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 580c0e93a288..d916255b7264 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -135,7 +135,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const {reimbursableSpend} = getMoneyRequestSpendBreakdown(moneyRequestReport);
     const isOnHold = isOnHoldTransactionUtils(transaction);
     const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction);
-    const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID) && !isReportApproved({report: moneyRequestReport});
+    const [showReviewDuplicates, setShowReviewDuplicates] = useState(true);
+    const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID);
 
     // Only the requestor can delete the request, admins can only edit it.
     const isActionOwner =
@@ -351,6 +352,15 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         [isDuplicate, shouldShowSettlementButton, shouldShowExportIntegrationButton, shouldShowSubmitButton, shouldShowMarkAsCashButton],
     );
 
+    const onAnimationFinish = useCallback(() => {
+        stopAnimation();
+        setShowReviewDuplicates(false);
+    }, [stopAnimation]);
+
+    useEffect(() => {
+        setShowReviewDuplicates(true);
+    }, [moneyRequestReport?.reportID]);
+
     useEffect(() => {
         // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
         if (isLoadingHoldUseExplained || dismissedHoldUseExplanation || !isOnHold) {
@@ -380,7 +390,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                 onBackButtonPress={onBackButtonPress}
                 shouldShowBorderBottom={false}
             >
-                {isDuplicate && !shouldUseNarrowLayout && (
+                {isDuplicate && showReviewDuplicates && !shouldUseNarrowLayout && (
                     <View style={[shouldDuplicateButtonBeSuccess ? styles.ml2 : styles.mh2]}>
                         <Button
                             success={shouldDuplicateButtonBeSuccess}
@@ -397,7 +407,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                         <AnimatedSettlementButton
                             isPaidAnimationRunning={isPaidAnimationRunning}
                             isApprovedAnimationRunning={isApprovedAnimationRunning}
-                            onAnimationFinish={stopAnimation}
+                            onAnimationFinish={onAnimationFinish}
                             canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                             onlyShowPayElsewhere={onlyShowPayElsewhere}
                             currency={moneyRequestReport?.currency}
@@ -452,7 +462,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
             {!!isMoreContentShown && (
                 <View style={[styles.dFlex, styles.flexColumn, shouldAddGapToContents && styles.gap3, styles.pb3, styles.ph5]}>
                     <View style={[styles.dFlex, styles.w100, styles.flexRow, styles.gap3]}>
-                        {isDuplicate && shouldUseNarrowLayout && (
+                        {isDuplicate && showReviewDuplicates && shouldUseNarrowLayout && (
                             <Button
                                 success={shouldDuplicateButtonBeSuccess}
                                 text={translate('iou.reviewDuplicates')}
@@ -466,7 +476,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                             <AnimatedSettlementButton
                                 isPaidAnimationRunning={isPaidAnimationRunning}
                                 isApprovedAnimationRunning={isApprovedAnimationRunning}
-                                onAnimationFinish={stopAnimation}
+                                onAnimationFinish={onAnimationFinish}
                                 canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                                 wrapperStyle={[styles.flex1]}
                                 onlyShowPayElsewhere={onlyShowPayElsewhere}

From a152c55279f70c9089b187bd7e5989774bb39bb6 Mon Sep 17 00:00:00 2001
From: abzokhattab <abzokhattab@gmail.com>
Date: Fri, 21 Feb 2025 08:36:56 +0100
Subject: [PATCH 35/37] fixing linting

---
 src/components/MoneyReportHeader.tsx | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index d916255b7264..97172107b378 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -3,10 +3,10 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import {View} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import {useOnyx} from 'react-native-onyx';
+import useResponsiveLayout from '@hooks/useisReportApprovedResponsiveLayout';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
 import usePaymentAnimations from '@hooks/usePaymentAnimations';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import {convertToDisplayString} from '@libs/CurrencyUtils';
@@ -30,7 +30,6 @@ import {
     isClosedExpenseReportWithNoExpenses,
     isCurrentUserSubmitter,
     isInvoiceReport,
-    isReportApproved,
     navigateBackOnDeleteTransaction,
     reportTransactionsSelector,
 } from '@libs/ReportUtils';

From 836a4e8bd7da2a4cf85c4923942d900e4d87b12c Mon Sep 17 00:00:00 2001
From: Abdelrahman Khattab <abzokhattab@gmail.com>
Date: Sun, 23 Feb 2025 23:21:59 +0100
Subject: [PATCH 36/37] Removing showReviewDuplicates state

---
 src/components/MoneyReportHeader.tsx | 21 ++++++---------------
 1 file changed, 6 insertions(+), 15 deletions(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 97172107b378..5ca3b96dae27 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -3,10 +3,10 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
 import {View} from 'react-native';
 import type {OnyxEntry} from 'react-native-onyx';
 import {useOnyx} from 'react-native-onyx';
-import useResponsiveLayout from '@hooks/useisReportApprovedResponsiveLayout';
 import useLocalize from '@hooks/useLocalize';
 import useNetwork from '@hooks/useNetwork';
 import usePaymentAnimations from '@hooks/usePaymentAnimations';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
 import useTheme from '@hooks/useTheme';
 import useThemeStyles from '@hooks/useThemeStyles';
 import {convertToDisplayString} from '@libs/CurrencyUtils';
@@ -30,6 +30,7 @@ import {
     isClosedExpenseReportWithNoExpenses,
     isCurrentUserSubmitter,
     isInvoiceReport,
+    isReportApproved,
     navigateBackOnDeleteTransaction,
     reportTransactionsSelector,
 } from '@libs/ReportUtils';
@@ -126,6 +127,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const [dismissedHoldUseExplanation, dismissedHoldUseExplanationResult] = useOnyx(ONYXKEYS.NVP_DISMISSED_HOLD_USE_EXPLANATION, {initialValue: true});
     const isLoadingHoldUseExplained = isLoadingOnyxValue(dismissedHoldUseExplanationResult);
 
+    const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
     const styles = useThemeStyles();
     const theme = useTheme();
     const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false);
@@ -134,8 +136,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const {reimbursableSpend} = getMoneyRequestSpendBreakdown(moneyRequestReport);
     const isOnHold = isOnHoldTransactionUtils(transaction);
     const isDeletedParentAction = !!requestParentReportAction && isDeletedAction(requestParentReportAction);
-    const [showReviewDuplicates, setShowReviewDuplicates] = useState(true);
-    const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID);
+    const isDuplicate = isDuplicateTransactionUtils(transaction?.transactionID) && (!isReportApproved({report: moneyRequestReport}) || isApprovedAnimationRunning);
 
     // Only the requestor can delete the request, admins can only edit it.
     const isActionOwner =
@@ -165,7 +166,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
     const isPayAtEndExpense = isPayAtEndExpenseTransactionUtils(transaction);
     const isArchivedReport = isArchivedReportWithID(moneyRequestReport?.reportID);
     const [archiveReason] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${moneyRequestReport?.reportID}`, {selector: getArchiveReason});
-    const {isPaidAnimationRunning, isApprovedAnimationRunning, stopAnimation, startAnimation, startApprovedAnimation} = usePaymentAnimations();
 
     const getCanIOUBePaid = useCallback(
         (onlyShowPayElsewhere = false, shouldCheckApprovedState = true) =>
@@ -351,15 +351,6 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
         [isDuplicate, shouldShowSettlementButton, shouldShowExportIntegrationButton, shouldShowSubmitButton, shouldShowMarkAsCashButton],
     );
 
-    const onAnimationFinish = useCallback(() => {
-        stopAnimation();
-        setShowReviewDuplicates(false);
-    }, [stopAnimation]);
-
-    useEffect(() => {
-        setShowReviewDuplicates(true);
-    }, [moneyRequestReport?.reportID]);
-
     useEffect(() => {
         // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
         if (isLoadingHoldUseExplained || dismissedHoldUseExplanation || !isOnHold) {
@@ -389,7 +380,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                 onBackButtonPress={onBackButtonPress}
                 shouldShowBorderBottom={false}
             >
-                {isDuplicate && showReviewDuplicates && !shouldUseNarrowLayout && (
+                {isDuplicate && !shouldUseNarrowLayout && (
                     <View style={[shouldDuplicateButtonBeSuccess ? styles.ml2 : styles.mh2]}>
                         <Button
                             success={shouldDuplicateButtonBeSuccess}
@@ -406,7 +397,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                         <AnimatedSettlementButton
                             isPaidAnimationRunning={isPaidAnimationRunning}
                             isApprovedAnimationRunning={isApprovedAnimationRunning}
-                            onAnimationFinish={onAnimationFinish}
+                            onAnimationFinish={stopAnimation}
                             canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                             onlyShowPayElsewhere={onlyShowPayElsewhere}
                             currency={moneyRequestReport?.currency}

From 1710cd58bf845217f52f7dc4a837c63e6724581f Mon Sep 17 00:00:00 2001
From: Abdelrahman Khattab <abzokhattab@gmail.com>
Date: Sun, 23 Feb 2025 23:26:29 +0100
Subject: [PATCH 37/37] cleaning

---
 src/components/MoneyReportHeader.tsx | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index 5ca3b96dae27..ec398e3b364d 100644
--- a/src/components/MoneyReportHeader.tsx
+++ b/src/components/MoneyReportHeader.tsx
@@ -452,7 +452,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
             {!!isMoreContentShown && (
                 <View style={[styles.dFlex, styles.flexColumn, shouldAddGapToContents && styles.gap3, styles.pb3, styles.ph5]}>
                     <View style={[styles.dFlex, styles.w100, styles.flexRow, styles.gap3]}>
-                        {isDuplicate && showReviewDuplicates && shouldUseNarrowLayout && (
+                        {isDuplicate && shouldUseNarrowLayout && (
                             <Button
                                 success={shouldDuplicateButtonBeSuccess}
                                 text={translate('iou.reviewDuplicates')}
@@ -466,7 +466,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea
                             <AnimatedSettlementButton
                                 isPaidAnimationRunning={isPaidAnimationRunning}
                                 isApprovedAnimationRunning={isApprovedAnimationRunning}
-                                onAnimationFinish={onAnimationFinish}
+                                onAnimationFinish={stopAnimation}
                                 canIOUBePaid={canIOUBePaidAndApproved || isPaidAnimationRunning}
                                 wrapperStyle={[styles.flex1]}
                                 onlyShowPayElsewhere={onlyShowPayElsewhere}
