diff --git a/src/CONST.ts b/src/CONST.ts index b351acc77769..55648dd82b91 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -189,8 +189,9 @@ const CONST = { }, // Multiplier for gyroscope animation in order to make it a bit more subtle ANIMATION_GYROSCOPE_VALUE: 0.4, - ANIMATION_PAY_BUTTON_DURATION: 200, - ANIMATION_PAY_BUTTON_HIDE_DELAY: 1000, + ANIMATION_PAID_DURATION: 200, + ANIMATION_PAID_CHECKMARK_DELAY: 300, + ANIMATION_PAID_BUTTON_HIDE_DELAY: 1000, BACKGROUND_IMAGE_TRANSITION_DURATION: 1000, SCREEN_TRANSITION_END_TIMEOUT: 1000, ARROW_HIDE_DELAY: 3000, diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 1b4d51088019..50ab8e9ee08c 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -1,9 +1,9 @@ import truncate from 'lodash/truncate'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import Animated, {useAnimatedStyle, useSharedValue, withDelay, withSpring, withTiming} from 'react-native-reanimated'; import Button from '@components/Button'; import DelegateNoAccessModal from '@components/DelegateNoAccessModal'; import Icon from '@components/Icon'; @@ -18,6 +18,7 @@ import Text from '@components/Text'; import useDelegateUserDetails from '@hooks/useDelegateUserDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePolicy from '@hooks/usePolicy'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; @@ -39,33 +40,13 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy, Report, ReportAction, Transaction, TransactionViolations, UserWallet} from '@src/types/onyx'; +import type {ReportAction} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ExportWithDropdownMenu from './ExportWithDropdownMenu'; import type {PendingMessageProps} from './MoneyRequestPreview/types'; import ReportActionItemImages from './ReportActionItemImages'; -type ReportPreviewOnyxProps = { - /** The policy tied to the expense report */ - policy: OnyxEntry; - - /** ChatReport associated with iouReport */ - chatReport: OnyxEntry; - - /** Active IOU Report for current report */ - iouReport: OnyxEntry; - - /** All the transactions, used to update ReportPreview label and status */ - transactions: OnyxCollection; - - /** All of the transaction violations */ - transactionViolations: OnyxCollection; - - /** The user's wallet account */ - userWallet: OnyxEntry; -}; - -type ReportPreviewProps = ReportPreviewOnyxProps & { +type ReportPreviewProps = { /** All the data of the action */ action: ReportAction; @@ -101,24 +82,24 @@ type ReportPreviewProps = ReportPreviewOnyxProps & { }; function ReportPreview({ - iouReport, - policy, iouReportID, policyID, chatReportID, - chatReport, action, containerStyles, contextMenuAnchor, - transactions, - transactionViolations, isHovered = false, isWhisper = false, checkIfContextMenuActive = () => {}, onPaymentOptionsShow, onPaymentOptionsHide, - userWallet, }: ReportPreviewProps) { + const policy = usePolicy(policyID); + const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`); + const [iouReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`); + const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); + const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS); + const [userWallet] = useOnyx(ONYXKEYS.USER_WALLET); const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -151,6 +132,18 @@ function ReportPreview({ const {totalDisplaySpend, reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(iouReport); const iouSettled = ReportUtils.isSettled(iouReportID) || action?.childStatusNum === CONST.REPORT.STATUS_NUM.REIMBURSED; + const previewMessageOpacity = useSharedValue(1); + const previewMessageStyle = useAnimatedStyle(() => ({ + ...styles.flex1, + ...styles.flexRow, + ...styles.alignItemsCenter, + opacity: previewMessageOpacity.value, + })); + const checkMarkScale = useSharedValue(iouSettled ? 1 : 0); + const checkMarkStyle = useAnimatedStyle(() => ({ + ...styles.defaultCheckmarkWrapper, + transform: [{scale: checkMarkScale.value}], + })); const moneyRequestComment = action?.childLastMoneyRequestComment ?? ''; const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(chatReport); @@ -285,14 +278,14 @@ function ReportPreview({ return !Number.isNaN(amount) && amount === 0; } - const getPreviewMessage = () => { + const previewMessage = useMemo(() => { if (isScanning) { return translate('common.receipt'); } let payerOrApproverName; if (isPolicyExpenseChat) { - payerOrApproverName = ReportUtils.getPolicyName(chatReport); + payerOrApproverName = ReportUtils.getPolicyName(chatReport, undefined, policy); } else if (isInvoiceRoom) { payerOrApproverName = ReportUtils.getInvoicePayerName(chatReport, invoiceReceiverPolicy); } else { @@ -310,7 +303,20 @@ function ReportPreview({ payerOrApproverName = ReportUtils.getDisplayNameForParticipant(chatReport?.ownerAccountID, true); } return translate(paymentVerb, {payer: payerOrApproverName}); - }; + }, [ + isScanning, + isPolicyExpenseChat, + policy, + chatReport, + isInvoiceRoom, + invoiceReceiverPolicy, + managerID, + isApproved, + iouSettled, + iouReport?.isWaitingOnBankAccount, + hasNonReimbursableTransactions, + translate, + ]); const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); @@ -400,6 +406,33 @@ function ReportPreview({ const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; const shouldShowExportIntegrationButton = !shouldShowPayButton && !shouldShowSubmitButton && connectedIntegration && isAdmin && ReportUtils.canBeExported(iouReport); + useEffect(() => { + if (!isPaidAnimationRunning) { + return; + } + + // eslint-disable-next-line react-compiler/react-compiler + previewMessageOpacity.value = withTiming(0.75, {duration: CONST.ANIMATION_PAID_DURATION / 2}, () => { + // eslint-disable-next-line react-compiler/react-compiler + previewMessageOpacity.value = withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION / 2}); + }); + // We only want to animate the text when the text changes + // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps + }, [previewMessage, previewMessageOpacity]); + + useEffect(() => { + if (!iouSettled) { + return; + } + + if (isPaidAnimationRunning) { + // eslint-disable-next-line react-compiler/react-compiler + checkMarkScale.value = withDelay(CONST.ANIMATION_PAID_CHECKMARK_DELAY, withSpring(1, {duration: CONST.ANIMATION_PAID_DURATION})); + } else { + checkMarkScale.value = 1; + } + }, [isPaidAnimationRunning, iouSettled, checkMarkScale]); + return ( )} - + - - {getPreviewMessage()} - + + {previewMessage} + {shouldShowRBR && ( {getDisplayAmount()} {iouSettled && ( - + - + )} @@ -562,23 +595,4 @@ function ReportPreview({ ReportPreview.displayName = 'ReportPreview'; -export default withOnyx({ - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - chatReport: { - key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`, - }, - iouReport: { - key: ({iouReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${iouReportID}`, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, -})(ReportPreview); +export default ReportPreview; diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index c2dc4937503d..648c1dad36c3 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -19,6 +19,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is const paymentCompleteTextScale = useSharedValue(0); const paymentCompleteTextOpacity = useSharedValue(1); const height = useSharedValue(variables.componentSizeNormal); + const buttonMarginTop = useSharedValue(styles.expenseAndReportPreviewTextButtonContainer.gap); const buttonStyles = useAnimatedStyle(() => ({ transform: [{scale: buttonScale.value}], opacity: buttonOpacity.value, @@ -33,6 +34,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is height: height.value, justifyContent: 'center', overflow: 'hidden', + marginTop: buttonMarginTop.value, })); const buttonDisabledStyle = isPaidAnimationRunning ? { @@ -48,7 +50,8 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is paymentCompleteTextScale.value = 0; paymentCompleteTextOpacity.value = 1; height.value = variables.componentSizeNormal; - }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height]); + buttonMarginTop.value = styles.expenseAndReportPreviewTextButtonContainer.gap; + }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height, buttonMarginTop, styles.expenseAndReportPreviewTextButtonContainer.gap]); useEffect(() => { if (!isPaidAnimationRunning) { @@ -56,18 +59,19 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is return; } // eslint-disable-next-line react-compiler/react-compiler - buttonScale.value = withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION}); - buttonOpacity.value = withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION}); - paymentCompleteTextScale.value = withTiming(1, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION}); + buttonScale.value = withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}); + buttonOpacity.value = withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}); + paymentCompleteTextScale.value = withTiming(1, {duration: CONST.ANIMATION_PAID_DURATION}); // Wait for the above animation + 1s delay before hiding the component - const totalDelay = CONST.ANIMATION_PAY_BUTTON_DURATION + CONST.ANIMATION_PAY_BUTTON_HIDE_DELAY; + const totalDelay = CONST.ANIMATION_PAID_DURATION + CONST.ANIMATION_PAID_BUTTON_HIDE_DELAY; height.value = withDelay( totalDelay, - withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION}, () => runOnJS(onAnimationFinish)()), + withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION}, () => runOnJS(onAnimationFinish)()), ); - paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION})); - }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, resetAnimation]); + buttonMarginTop.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); + paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAID_DURATION})); + }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, buttonMarginTop, resetAnimation]); return (