From 43857d6dc4457610a4171cb8eb0c9d6bebcf8186 Mon Sep 17 00:00:00 2001 From: abzokhattab 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} - {ReportUtils.isSettled(iouReport?.reportID) && !isPartialHold && !isBillSplit && ( - + {shouldShowPaidIcon && ( + - + )} {isBillSplit && ( @@ -433,6 +471,10 @@ function MoneyRequestPreviewContent({ {pendingMessageProps.messageDescription} )} + {shouldShowCategoryOrTag && } {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(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 && ( + + + {translate('iou.paymentComplete')} + + + ) + ); +} + +PaymentCompleteAnimation.displayName = 'PaymentCompleteAnimation'; + +export default PaymentCompleteAnimation; From 380f2a339c2ead84eb81ff2c3cf8b0bf7124ee4e Mon Sep 17 00:00:00 2001 From: abzokhattab 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 && ( - )} {shouldShowSettlementButton && shouldUseNarrowLayout && ( - void; -}; - -function PaymentCompleteAnimation({isVisible, onAnimationFinish}: PaymentCompleteAnimationProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - - const paymentCompleteTextScale = useSharedValue(0); - const paymentCompleteTextOpacity = useSharedValue(1); - const containerHeight = useSharedValue(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 && ( - - - {translate('iou.paymentComplete')} - - - ) - ); -} - -PaymentCompleteAnimation.displayName = 'PaymentCompleteAnimation'; - -export default PaymentCompleteAnimation; From 16a8901d0dfe43b2b50319758517d1d2ebda3cbb Mon Sep 17 00:00:00 2001 From: abzokhattab 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({ {pendingMessageProps.messageDescription} )} - {shouldShowCategoryOrTag && } {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 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(variables.componentSizeNormal); const buttonMarginTop = useSharedValue(styles.expenseAndReportPreviewTextButtonContainer.gap); + const [isLoading, setIsLoading] = useState(false); + const [buttonWidth, setButtonWidth] = useState(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 ( - {isPaidAnimationRunning && ( - - {translate('iou.paymentComplete')} + {isPaidAnimationRunning ? ( + +