Skip to content
3 changes: 3 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ const CONST = {
ANIMATION_THUMBSUP_DURATION: 250,
ANIMATION_THUMBSUP_DELAY: 200,
ANIMATION_PAID_BUTTON_HIDE_DELAY: 1000,
ANIMATION_SUBMITTED_TEXT_DELAY: 500,
ANIMATION_SUBMITTED_HEIGHT_DURATION: 1000,
ANIMATION_SUBMITTED_TEXT_DURATION: 2000,
BACKGROUND_IMAGE_TRANSITION_DURATION: 1000,
SCREEN_TRANSITION_END_TIMEOUT: 1000,
ARROW_HIDE_DELAY: 3000,
Expand Down
30 changes: 20 additions & 10 deletions src/components/ReportActionItem/ReportPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import Animated, {useAnimatedStyle, useSharedValue, withDelay, withSpring, withTiming} from 'react-native-reanimated';
import Button from '@components/Button';
import {getButtonRole, getButtonStyle} from '@components/Button/utils';
import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import Icon from '@components/Icon';
Expand Down Expand Up @@ -82,8 +81,9 @@ import {
} from '@libs/TransactionUtils';
import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu';
import variables from '@styles/variables';
import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidIOUActions, canSubmitReport, payInvoice, payMoneyRequest, submitReport} from '@userActions/IOU';
import {approveMoneyRequest, canApproveIOU, canIOUBePaid as canIOUBePaidIOUActions, canSubmitReport, payInvoice, payMoneyRequest} from '@userActions/IOU';
import Timing from '@userActions/Timing';
import SubmitButtonWithAnimation from '@src/components/SubmitButtonWithAnimation';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -245,10 +245,17 @@ function ReportPreview({
formattedMerchant = null;
}

const [isSubmitAnimationRunning, setIsSubmitAnimationRunning] = useState(false);

const startSubmitAnimation = useCallback(() => {
setIsSubmitAnimationRunning(true);
HapticFeedback.longPress();
}, []);

const isArchived = isArchivedReportWithID(iouReport?.reportID);
const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const filteredTransactions = transactions?.filter((transaction) => transaction) ?? [];
const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, violations);
const shouldShowSubmitButton = canSubmitReport(iouReport, policy, filteredTransactions, violations) || isSubmitAnimationRunning;
const shouldDisableSubmitButton = shouldShowSubmitButton && !isAllowedToSubmitDraftExpenseReport(iouReport);

// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
Expand All @@ -263,6 +270,7 @@ function ReportPreview({
const stopAnimation = useCallback(() => {
setIsPaidAnimationRunning(false);
setIsApprovedAnimationRunning(false);
setIsSubmitAnimationRunning(false);
}, []);
const startAnimation = useCallback(() => {
setIsPaidAnimationRunning(true);
Expand All @@ -272,7 +280,6 @@ function ReportPreview({
setIsApprovedAnimationRunning(true);
HapticFeedback.longPress();
}, []);

const confirmPayment = useCallback(
(type: PaymentMethodType | undefined, payAsBusiness?: boolean) => {
if (!type) {
Expand Down Expand Up @@ -416,7 +423,8 @@ function ReportPreview({

const bankAccountRoute = getBankAccountRoute(chatReport);

const shouldShowSettlementButton = !shouldShowSubmitButton && (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation;
const shouldShowSettlementButton =
!shouldShowSubmitButton && (shouldShowPayButton || shouldShowApproveButton) && !showRTERViolationMessage && !shouldShowBrokenConnectionViolation && !isSubmitAnimationRunning;

const shouldPromptUserToAddBankAccount = (hasMissingPaymentMethod(userWallet, iouReportID) || hasMissingInvoiceBankAccount(iouReportID)) && !isSettled(iouReportID);
const shouldShowRBR = hasErrors && !iouSettled;
Expand Down Expand Up @@ -664,11 +672,13 @@ function ReportPreview({
/>
)}
{shouldShowSubmitButton && (
<Button
success={isWaitingForSubmissionFromCurrentUser}
text={translate('common.submit')}
onPress={() => iouReport && submitReport(iouReport)}
isDisabled={shouldDisableSubmitButton}
<SubmitButtonWithAnimation
isWaitingForSubmissionFromCurrentUser={isWaitingForSubmissionFromCurrentUser}
shouldDisableSubmitButton={shouldDisableSubmitButton}
iouReport={iouReport}
startSubmitAnimation={startSubmitAnimation}
isSubmitAnimationRunning={isSubmitAnimationRunning}
onAnimationFinish={stopAnimation}
/>
)}
</View>
Expand Down
81 changes: 81 additions & 0 deletions src/components/SubmitButtonWithAnimation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, {useEffect, useState} from 'react';
import type {OnyxEntry} from 'react-native-onyx';
import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import useLocalize from '@hooks/useLocalize';
import {submitReport} from '@libs/actions/IOU';
import CONST from '@src/CONST';
import type * as OnyxTypes from '@src/types/onyx';
import Button from './Button';
import {Send} from './Icon/Expensicons';

type SubmitButtonWithAnimationProps = {
isWaitingForSubmissionFromCurrentUser?: boolean;
iouReport: OnyxEntry<OnyxTypes.Report>;
shouldDisableSubmitButton: boolean;
startSubmitAnimation: () => void;
isSubmitAnimationRunning: boolean;
onAnimationFinish: () => void;
};

function SubmitButtonWithAnimation({
isWaitingForSubmissionFromCurrentUser,
iouReport,
shouldDisableSubmitButton,
startSubmitAnimation,
isSubmitAnimationRunning,
onAnimationFinish,
}: SubmitButtonWithAnimationProps) {
const {translate} = useLocalize();
const height = useSharedValue(40);
const opacity = useSharedValue(1);

const [showLoadingIndicator, setShowLoadingIndicator] = useState(false);
const [showSubmittedText, setShowSubmittedText] = useState(false);

const containerStyles = useAnimatedStyle(() => ({
height: height.get(),
opacity: opacity.get(),
overflow: 'hidden',
}));

useEffect(() => {
if (!isSubmitAnimationRunning) {
height.set(40);
opacity.set(1);
return;
}

// Step 1: Show loading indicator for 1 second
setShowLoadingIndicator(true);
setTimeout(() => {
setShowLoadingIndicator(false);
setShowSubmittedText(true);
}, 1000);

// Step 2: Show "Submitted" text for 1 second
setTimeout(() => {
opacity.set(withTiming(0, {duration: CONST.ANIMATION_SUBMITTED_TEXT_DELAY}));
height.set(withTiming(0, {duration: CONST.ANIMATION_SUBMITTED_HEIGHT_DURATION}, () => runOnJS(onAnimationFinish)()));
}, CONST.ANIMATION_SUBMITTED_TEXT_DURATION);
}, [isSubmitAnimationRunning, onAnimationFinish, height, opacity]);

return (
<Animated.View style={containerStyles}>
<Button
success={isWaitingForSubmissionFromCurrentUser}
icon={showSubmittedText ? Send : undefined}
text={showSubmittedText ? translate('common.submitted') : translate('common.submit')}
isLoading={showLoadingIndicator}
onPress={() => {
startSubmitAnimation();
iouReport && submitReport(iouReport);
}}
isDisabled={shouldDisableSubmitButton}
/>
</Animated.View>
);
}

SubmitButtonWithAnimation.displayName = 'SubmitButtonWithAnimation';

export default SubmitButtonWithAnimation;
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ const translations = {
selectMultiple: 'Select multiple',
saveChanges: 'Save changes',
submit: 'Submit',
submitted: 'Submitted',
rotate: 'Rotate',
zoom: 'Zoom',
password: 'Password',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ const translations = {
save: 'Guardar',
saveChanges: 'Guardar cambios',
submit: 'Enviar',
submitted: 'Enviado',
rotate: 'Rotar',
zoom: 'Zoom',
password: 'Contraseña',
Expand Down