Skip to content

Commit

Permalink
Merge pull request #49438 from bernhardoj/feat/48036-animate-checkmark
Browse files Browse the repository at this point in the history
Animate the paid checkmark when paying expense
  • Loading branch information
roryabraham authored Sep 20, 2024
2 parents 33d7127 + a46ffb8 commit 0d0fb4e
Show file tree
Hide file tree
Showing 3 changed files with 89 additions and 70 deletions.
5 changes: 3 additions & 2 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
134 changes: 74 additions & 60 deletions src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -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<Policy>;

/** ChatReport associated with iouReport */
chatReport: OnyxEntry<Report>;

/** Active IOU Report for current report */
iouReport: OnyxEntry<Report>;

/** All the transactions, used to update ReportPreview label and status */
transactions: OnyxCollection<Transaction>;

/** All of the transaction violations */
transactionViolations: OnyxCollection<TransactionViolations>;

/** The user's wallet account */
userWallet: OnyxEntry<UserWallet>;
};

type ReportPreviewProps = ReportPreviewOnyxProps & {
type ReportPreviewProps = {
/** All the data of the action */
action: ReportAction;

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand All @@ -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);

Expand Down Expand Up @@ -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 (
<OfflineWithFeedback
pendingAction={iouReport?.pendingFields?.preview}
Expand Down Expand Up @@ -429,12 +462,12 @@ function ReportPreview({
/>
)}
<View style={[styles.expenseAndReportPreviewBoxBody, hasReceipts ? styles.mtn1 : {}]}>
<View style={styles.expenseAndReportPreviewTextButtonContainer}>
<View style={shouldShowSettlementButton ? {} : styles.expenseAndReportPreviewTextButtonContainer}>
<View style={styles.expenseAndReportPreviewTextContainer}>
<View style={styles.flexRow}>
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}>
<Text style={[styles.textLabelSupporting, styles.lh16]}>{getPreviewMessage()}</Text>
</View>
<Animated.View style={previewMessageStyle}>
<Text style={[styles.textLabelSupporting, styles.lh16]}>{previewMessage}</Text>
</Animated.View>
{shouldShowRBR && (
<Icon
src={Expensicons.DotIndicator}
Expand All @@ -454,12 +487,12 @@ function ReportPreview({
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter]}>
<Text style={styles.textHeadlineH1}>{getDisplayAmount()}</Text>
{iouSettled && (
<View style={styles.defaultCheckmarkWrapper}>
<Animated.View style={checkMarkStyle}>
<Icon
src={Expensicons.Checkmark}
fill={theme.iconSuccessFill}
/>
</View>
</Animated.View>
)}
</View>
</View>
Expand Down Expand Up @@ -562,23 +595,4 @@ function ReportPreview({

ReportPreview.displayName = 'ReportPreview';

export default withOnyx<ReportPreviewProps, ReportPreviewOnyxProps>({
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;
20 changes: 12 additions & 8 deletions src/components/SettlementButton/AnimatedSettlementButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
const paymentCompleteTextScale = useSharedValue(0);
const paymentCompleteTextOpacity = useSharedValue(1);
const height = useSharedValue<number>(variables.componentSizeNormal);
const buttonMarginTop = useSharedValue<number>(styles.expenseAndReportPreviewTextButtonContainer.gap);
const buttonStyles = useAnimatedStyle(() => ({
transform: [{scale: buttonScale.value}],
opacity: buttonOpacity.value,
Expand All @@ -33,6 +34,7 @@ function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, is
height: height.value,
justifyContent: 'center',
overflow: 'hidden',
marginTop: buttonMarginTop.value,
}));
const buttonDisabledStyle = isPaidAnimationRunning
? {
Expand All @@ -48,26 +50,28 @@ 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) {
resetAnimation();
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 (
<Animated.View style={containerStyles}>
Expand Down

0 comments on commit 0d0fb4e

Please sign in to comment.