From eaf39ea2d93cc4198e13cf7af0b053b98a73e645 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 5 Sep 2024 12:19:25 +0800 Subject: [PATCH 01/12] animate settlement button when pay and trigger a haptic feedback --- .../ReportActionItem/ReportPreview.tsx | 60 ++++++----- .../AnimatedSettlementButton.tsx | 90 ++++++++++++++++ .../index.tsx} | 100 +++--------------- src/components/SettlementButton/types.ts | 86 +++++++++++++++ 4 files changed, 219 insertions(+), 117 deletions(-) create mode 100644 src/components/SettlementButton/AnimatedSettlementButton.tsx rename src/components/{SettlementButton.tsx => SettlementButton/index.tsx} (78%) create mode 100644 src/components/SettlementButton/types.ts diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 792ebb1769001..d76c9123bb922 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -11,7 +11,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ProcessMoneyReportHoldMenu from '@components/ProcessMoneyReportHoldMenu'; import type {ActionHandledType} from '@components/ProcessMoneyReportHoldMenu'; -import SettlementButton from '@components/SettlementButton'; +import AnimatedSettlementButton from '@components/SettlementButton/AnimatedSettlementButton'; import {showContextMenuForReport} from '@components/ShowContextMenuContext'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -21,6 +21,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; 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 * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; @@ -200,11 +201,13 @@ function ReportPreview({ if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) { setIsHoldMenuVisible(true); } else if (chatReport && iouReport) { + HapticFeedback.longPress(); if (ReportUtils.isInvoiceReport(iouReport)) { IOU.payInvoice(type, chatReport, iouReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, iouReport); } + return true; } }; @@ -463,34 +466,33 @@ function ReportPreview({ )} - {shouldShowSettlementButton && ( - - )} + {shouldShowExportIntegrationButton && !shouldShowSettlementButton && ( (variables.componentSizeNormal); + const buttonStyles = useAnimatedStyle(() => ({ + transform: [{scale: buttonScale.value}], + opacity: buttonOpacity.value, + })); + const paymentCompleteTextStyles = useAnimatedStyle(() => ({ + transform: [{scale: paymentCompleteTextScale.value}], + opacity: paymentCompleteTextOpacity.value, + position: 'absolute', + alignSelf: 'center', + })); + + const containerStyles = useAnimatedStyle(() => ({ + height: height.value, + justifyContent: 'center', + overflow: 'hidden', + })); + + const [isAnimationRunning, setIsAnimationRunning] = useState(false); + + const resetAnimation = () => { + buttonScale.value = 1; + buttonOpacity.value = 1; + paymentCompleteTextScale.value = 0; + paymentCompleteTextOpacity.value = 1; + height.value = variables.componentSizeNormal; + } + + useEffect(() => { + if (!isAnimationRunning) { + resetAnimation(); + return; + } + buttonScale.value = withTiming(0, {duration: 200}); + buttonOpacity.value = withTiming(0, {duration: 200}); + paymentCompleteTextScale.value = withTiming(1, {duration: 200}); + + // Wait for the above animation + 1s delay before hiding the component + height.value = withDelay( + 1200, + withTiming(0, {duration: 200}, () => runOnJS(setIsAnimationRunning)(false)), + ); + paymentCompleteTextOpacity.value = withDelay(1200, withTiming(0, {duration: 200})); + }, [isAnimationRunning]); + + if (!isVisible && !isAnimationRunning) { + return null; + } + + return ( + + {isAnimationRunning && ( + + Payment complete + + )} + + { + const isPaid = !!settlementButtonProps.onPress(paymentType, payAsBusiness); + setIsAnimationRunning(isPaid); + }} + /> + + + ); +} + +AnimatedSettlementButton.displayName = 'AnimatedSettlementButton'; + +export default AnimatedSettlementButton; diff --git a/src/components/SettlementButton.tsx b/src/components/SettlementButton/index.tsx similarity index 78% rename from src/components/SettlementButton.tsx rename to src/components/SettlementButton/index.tsx index fc72f2fe7418e..9a59ba5371b60 100644 --- a/src/components/SettlementButton.tsx +++ b/src/components/SettlementButton/index.tsx @@ -1,5 +1,5 @@ import React, {useMemo} from 'react'; -import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx, withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; @@ -14,25 +14,21 @@ import * as IOU from '@userActions/IOU'; import * as PolicyActions from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; -import type {ButtonSizeValue} from '@src/styles/utils/types'; -import type {LastPaymentMethod, Policy, Report} from '@src/types/onyx'; +import type {LastPaymentMethod, Policy} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; -import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; -import type {PaymentType} from './ButtonWithDropdownMenu/types'; -import * as Expensicons from './Icon/Expensicons'; -import KYCWall from './KYCWall'; -import {useSession} from './OnyxProvider'; +import ButtonWithDropdownMenu from '../ButtonWithDropdownMenu'; +import type {PaymentType} from '../ButtonWithDropdownMenu/types'; +import * as Expensicons from '../Icon/Expensicons'; +import KYCWall from '../KYCWall'; +import {useSession} from '../OnyxProvider'; +import {SettlementButtonProps} from './types'; type KYCFlowEvent = GestureResponderEvent | KeyboardEvent | undefined; type TriggerKYCFlow = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType) => void; -type EnablePaymentsRoute = typeof ROUTES.ENABLE_PAYMENTS | typeof ROUTES.IOU_SEND_ENABLE_PAYMENTS | typeof ROUTES.SETTINGS_ENABLE_PAYMENTS; - type SettlementButtonOnyxProps = { /** The last payment method used per policy */ nvpLastPaymentMethod?: OnyxEntry; @@ -41,79 +37,7 @@ type SettlementButtonOnyxProps = { policy: OnyxEntry; }; -type SettlementButtonProps = SettlementButtonOnyxProps & { - /** Callback to execute when this button is pressed. Receives a single payment type argument. */ - onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean) => void; - - /** Callback when the payment options popover is shown */ - onPaymentOptionsShow?: () => void; - - /** Callback when the payment options popover is closed */ - onPaymentOptionsHide?: () => void; - - /** The route to redirect if user does not have a payment method setup */ - enablePaymentsRoute: EnablePaymentsRoute; - - /** Call the onPress function on main button when Enter key is pressed */ - pressOnEnter?: boolean; - - /** Settlement currency type */ - currency?: string; - - /** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */ - chatReportID?: string; - - /** The IOU/Expense report we are paying */ - iouReport?: OnyxEntry; - - /** Should we show the payment options? */ - shouldHidePaymentOptions?: boolean; - - /** Should we show the payment options? */ - shouldShowApproveButton?: boolean; - - /** Should approve button be disabled? */ - shouldDisableApproveButton?: boolean; - - /** The policyID of the report we are paying */ - policyID?: string; - - /** Additional styles to add to the component */ - style?: StyleProp; - - /** Total money amount in form */ - formattedAmount?: string; - - /** The size of button size */ - buttonSize?: ButtonSizeValue; - - /** Route for the Add Bank Account screen for a given navigation stack */ - addBankAccountRoute?: Route; - - /** Route for the Add Debit Card screen for a given navigation stack */ - addDebitCardRoute?: Route; - - /** Whether the button should be disabled */ - isDisabled?: boolean; - - /** Whether we should show a loading state for the main button */ - isLoading?: boolean; - - /** The anchor alignment of the popover menu for payment method dropdown */ - paymentMethodDropdownAnchorAlignment?: AnchorAlignment; - - /** The anchor alignment of the popover menu for KYC wall popover */ - kycWallAnchorAlignment?: AnchorAlignment; - - /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ - enterKeyEventListenerPriority?: number; - - /** Callback to open confirmation modal if any of the transactions is on HOLD */ - confirmApproval?: () => void; - - /** Whether to use keyboard shortcuts for confirmation or not */ - useKeyboardShortcuts?: boolean; -}; +type SettlementButtonWithOnyxProps = SettlementButtonProps & SettlementButtonOnyxProps; function SettlementButton({ addDebitCardRoute = ROUTES.IOU_SEND_ADD_DEBIT_CARD, @@ -150,7 +74,7 @@ function SettlementButton({ useKeyboardShortcuts = false, onPaymentOptionsShow, onPaymentOptionsHide, -}: SettlementButtonProps) { +}: SettlementButtonWithOnyxProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID); @@ -295,7 +219,7 @@ function SettlementButton({ return; } - playSound(SOUNDS.DONE); + playSound(SOUNDS.SUCCESS); onPress(iouPaymentType); }; @@ -341,7 +265,7 @@ function SettlementButton({ SettlementButton.displayName = 'SettlementButton'; -export default withOnyx({ +export default withOnyx({ nvpLastPaymentMethod: { key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, selector: (paymentMethod) => paymentMethod ?? {}, diff --git a/src/components/SettlementButton/types.ts b/src/components/SettlementButton/types.ts new file mode 100644 index 0000000000000..dd464082d13e7 --- /dev/null +++ b/src/components/SettlementButton/types.ts @@ -0,0 +1,86 @@ +import type {StyleProp, ViewStyle} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import ROUTES from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; +import type {ButtonSizeValue} from '@src/styles/utils/types'; +import type {Report} from '@src/types/onyx'; +import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type AnchorAlignment from '@src/types/utils/AnchorAlignment'; + +type EnablePaymentsRoute = typeof ROUTES.ENABLE_PAYMENTS | typeof ROUTES.IOU_SEND_ENABLE_PAYMENTS | typeof ROUTES.SETTINGS_ENABLE_PAYMENTS; + +type SettlementButtonProps = { + /** Callback to execute when this button is pressed. Receives a single payment type argument. */ + onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean) => void | boolean; + + /** Callback when the payment options popover is shown */ + onPaymentOptionsShow?: () => void; + + /** Callback when the payment options popover is closed */ + onPaymentOptionsHide?: () => void; + + /** The route to redirect if user does not have a payment method setup */ + enablePaymentsRoute: EnablePaymentsRoute; + + /** Call the onPress function on main button when Enter key is pressed */ + pressOnEnter?: boolean; + + /** Settlement currency type */ + currency?: string; + + /** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */ + chatReportID?: string; + + /** The IOU/Expense report we are paying */ + iouReport?: OnyxEntry; + + /** Should we show the payment options? */ + shouldHidePaymentOptions?: boolean; + + /** Should we show the payment options? */ + shouldShowApproveButton?: boolean; + + /** Should approve button be disabled? */ + shouldDisableApproveButton?: boolean; + + /** The policyID of the report we are paying */ + policyID?: string; + + /** Additional styles to add to the component */ + style?: StyleProp; + + /** Total money amount in form */ + formattedAmount?: string; + + /** The size of button size */ + buttonSize?: ButtonSizeValue; + + /** Route for the Add Bank Account screen for a given navigation stack */ + addBankAccountRoute?: Route; + + /** Route for the Add Debit Card screen for a given navigation stack */ + addDebitCardRoute?: Route; + + /** Whether the button should be disabled */ + isDisabled?: boolean; + + /** Whether we should show a loading state for the main button */ + isLoading?: boolean; + + /** The anchor alignment of the popover menu for payment method dropdown */ + paymentMethodDropdownAnchorAlignment?: AnchorAlignment; + + /** The anchor alignment of the popover menu for KYC wall popover */ + kycWallAnchorAlignment?: AnchorAlignment; + + /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ + enterKeyEventListenerPriority?: number; + + /** Callback to open confirmation modal if any of the transactions is on HOLD */ + confirmApproval?: () => void; + + /** Whether to use keyboard shortcuts for confirmation or not */ + useKeyboardShortcuts?: boolean; +}; + +export type {SettlementButtonProps}; From f3e6b99e2a14bd16af051bfab6203fcf7fb6f893 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 5 Sep 2024 13:51:23 +0800 Subject: [PATCH 02/12] lint --- .../SettlementButton/AnimatedSettlementButton.tsx | 4 ++-- src/components/SettlementButton/index.tsx | 12 ++++++------ src/components/SettlementButton/types.ts | 4 ++-- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 1525feb792711..5b995ceb65e96 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -4,7 +4,7 @@ import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import SettlementButton from '.'; -import {SettlementButtonProps} from './types'; +import type SettlementButtonProps from './types'; type AnimatedSettlementButtonProps = SettlementButtonProps & { isVisible: boolean; @@ -42,7 +42,7 @@ function AnimatedSettlementButton({isVisible, ...settlementButtonProps}: Animate paymentCompleteTextScale.value = 0; paymentCompleteTextOpacity.value = 1; height.value = variables.componentSizeNormal; - } + }; useEffect(() => { if (!isAnimationRunning) { diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 9a59ba5371b60..6010eb104b40a 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -2,6 +2,11 @@ import React, {useMemo} from 'react'; import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx, withOnyx} from 'react-native-onyx'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {PaymentType} from '@components/ButtonWithDropdownMenu/types'; +import * as Expensicons from '@components/Icon/Expensicons'; +import KYCWall from '@components/KYCWall'; +import {useSession} from '@components/OnyxProvider'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import Navigation from '@libs/Navigation/Navigation'; @@ -18,12 +23,7 @@ import ROUTES from '@src/ROUTES'; import type {LastPaymentMethod, Policy} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import ButtonWithDropdownMenu from '../ButtonWithDropdownMenu'; -import type {PaymentType} from '../ButtonWithDropdownMenu/types'; -import * as Expensicons from '../Icon/Expensicons'; -import KYCWall from '../KYCWall'; -import {useSession} from '../OnyxProvider'; -import {SettlementButtonProps} from './types'; +import type SettlementButtonProps from './types'; type KYCFlowEvent = GestureResponderEvent | KeyboardEvent | undefined; diff --git a/src/components/SettlementButton/types.ts b/src/components/SettlementButton/types.ts index dd464082d13e7..c886ff0580ad0 100644 --- a/src/components/SettlementButton/types.ts +++ b/src/components/SettlementButton/types.ts @@ -1,6 +1,6 @@ import type {StyleProp, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; -import ROUTES from '@src/ROUTES'; +import type ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type {ButtonSizeValue} from '@src/styles/utils/types'; import type {Report} from '@src/types/onyx'; @@ -83,4 +83,4 @@ type SettlementButtonProps = { useKeyboardShortcuts?: boolean; }; -export type {SettlementButtonProps}; +export default SettlementButtonProps; From f5352348588940abbb6187868f3599f65e836c43 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 5 Sep 2024 23:07:53 +0700 Subject: [PATCH 03/12] lint --- .../SettlementButton/AnimatedSettlementButton.tsx | 11 +++++++---- src/libs/ReportUtils.ts | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 5b995ceb65e96..3336d012815f2 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from 'react'; +import React, {useCallback, useEffect, useState} from 'react'; import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTiming} from 'react-native-reanimated'; import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -36,19 +36,21 @@ function AnimatedSettlementButton({isVisible, ...settlementButtonProps}: Animate const [isAnimationRunning, setIsAnimationRunning] = useState(false); - const resetAnimation = () => { + const resetAnimation = useCallback(() => { + // eslint-disable-next-line react-compiler/react-compiler buttonScale.value = 1; buttonOpacity.value = 1; paymentCompleteTextScale.value = 0; paymentCompleteTextOpacity.value = 1; height.value = variables.componentSizeNormal; - }; + }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height]); useEffect(() => { if (!isAnimationRunning) { resetAnimation(); return; } + // eslint-disable-next-line react-compiler/react-compiler buttonScale.value = withTiming(0, {duration: 200}); buttonOpacity.value = withTiming(0, {duration: 200}); paymentCompleteTextScale.value = withTiming(1, {duration: 200}); @@ -59,7 +61,7 @@ function AnimatedSettlementButton({isVisible, ...settlementButtonProps}: Animate withTiming(0, {duration: 200}, () => runOnJS(setIsAnimationRunning)(false)), ); paymentCompleteTextOpacity.value = withDelay(1200, withTiming(0, {duration: 200})); - }, [isAnimationRunning]); + }, [isAnimationRunning, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, resetAnimation]); if (!isVisible && !isAnimationRunning) { return null; @@ -74,6 +76,7 @@ function AnimatedSettlementButton({isVisible, ...settlementButtonProps}: Animate )} { const isPaid = !!settlementButtonProps.onPress(paymentType, payAsBusiness); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7b226b2e5c8ef..e59616de58bfb 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5757,6 +5757,10 @@ function isUnread(report: OnyxEntry): boolean { if (isEmptyReport(report) && !isSelfDM(report)) { return false; } + if (!report.lastReadTime) { + return false; + } + // lastVisibleActionCreated and lastReadTime are both datetime strings and can be compared directly const lastVisibleActionCreated = report.lastVisibleActionCreated ?? ''; const lastReadTime = report.lastReadTime ?? ''; From bf5317eb64c793980c6221c8173b437fc00cd8cf Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 5 Sep 2024 23:10:52 +0700 Subject: [PATCH 04/12] missing type from conflict --- src/components/SettlementButton/types.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/SettlementButton/types.ts b/src/components/SettlementButton/types.ts index c886ff0580ad0..3eb7e63937d9c 100644 --- a/src/components/SettlementButton/types.ts +++ b/src/components/SettlementButton/types.ts @@ -73,6 +73,9 @@ type SettlementButtonProps = { /** The anchor alignment of the popover menu for KYC wall popover */ kycWallAnchorAlignment?: AnchorAlignment; + /** Whether the personal bank account option should be shown */ + shouldShowPersonalBankAccountOption?: boolean; + /** The priority to assign the enter key event listener to buttons. 0 is the highest priority. */ enterKeyEventListenerPriority?: number; From a6061bd3a062f80910a1e02d18d6424ba91a898e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 5 Sep 2024 23:16:36 +0700 Subject: [PATCH 05/12] undo unintentional change --- src/libs/ReportUtils.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e59616de58bfb..63a4f7312b45d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5757,9 +5757,6 @@ function isUnread(report: OnyxEntry): boolean { if (isEmptyReport(report) && !isSelfDM(report)) { return false; } - if (!report.lastReadTime) { - return false; - } // lastVisibleActionCreated and lastReadTime are both datetime strings and can be compared directly const lastVisibleActionCreated = report.lastVisibleActionCreated ?? ''; From a37a6e42a52b636fc0e4f8fd6d1bbe2e70d78f9e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Thu, 5 Sep 2024 23:17:01 +0700 Subject: [PATCH 06/12] undo unintentional change --- src/libs/ReportUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 63a4f7312b45d..7b226b2e5c8ef 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -5757,7 +5757,6 @@ function isUnread(report: OnyxEntry): boolean { if (isEmptyReport(report) && !isSelfDM(report)) { return false; } - // lastVisibleActionCreated and lastReadTime are both datetime strings and can be compared directly const lastVisibleActionCreated = report.lastVisibleActionCreated ?? ''; const lastReadTime = report.lastReadTime ?? ''; From e9e48ab53b2ea8d342f80cbbe5759d084b5c3846 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 9 Sep 2024 18:08:44 +0700 Subject: [PATCH 07/12] move the state to start the animation to report preview --- .../ReportActionItem/ReportPreview.tsx | 68 +++++++++++-------- .../AnimatedSettlementButton.tsx | 25 +++---- src/components/SettlementButton/types.ts | 2 +- 3 files changed, 47 insertions(+), 48 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 2b93880c98c1c..8c3dda515bcc6 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -1,5 +1,5 @@ import truncate from 'lodash/truncate'; -import React, {useMemo, useState} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -137,6 +137,7 @@ function ReportPreview({ [transactions, iouReportID, action], ); + const [shouldStartPaidAnimation, setShouldStartPaidAnimation] = useState(false); const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [requestType, setRequestType] = useState(); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy); @@ -197,6 +198,7 @@ function ReportPreview({ const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); + const stopAnimation = useCallback(() => setShouldStartPaidAnimation(false), []); const confirmPayment = (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type) { return; @@ -208,13 +210,13 @@ function ReportPreview({ } else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) { setIsHoldMenuVisible(true); } else if (chatReport && iouReport) { + setShouldStartPaidAnimation(true); HapticFeedback.longPress(); if (ReportUtils.isInvoiceReport(iouReport)) { IOU.payInvoice(type, chatReport, iouReport, payAsBusiness); } else { IOU.payMoneyRequest(type, chatReport, iouReport); } - return true; } }; @@ -309,7 +311,10 @@ function ReportPreview({ const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); - const shouldShowPayButton = useMemo(() => IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions), [iouReport, chatReport, policy, allTransactions]); + const shouldShowPayButton = useMemo( + () => shouldStartPaidAnimation || IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions), + [shouldStartPaidAnimation, iouReport, chatReport, policy, allTransactions], + ); const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]); @@ -475,33 +480,36 @@ function ReportPreview({ )} - + {shouldShowSettlementButton && ( + + )} {shouldShowExportIntegrationButton && !shouldShowSettlementButton && ( void; }; -function AnimatedSettlementButton({isVisible, ...settlementButtonProps}: AnimatedSettlementButtonProps) { +function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, ...settlementButtonProps}: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); const buttonScale = useSharedValue(1); const buttonOpacity = useSharedValue(1); @@ -34,8 +35,6 @@ function AnimatedSettlementButton({isVisible, ...settlementButtonProps}: Animate overflow: 'hidden', })); - const [isAnimationRunning, setIsAnimationRunning] = useState(false); - const resetAnimation = useCallback(() => { // eslint-disable-next-line react-compiler/react-compiler buttonScale.value = 1; @@ -46,7 +45,7 @@ function AnimatedSettlementButton({isVisible, ...settlementButtonProps}: Animate }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height]); useEffect(() => { - if (!isAnimationRunning) { + if (!shouldStartPaidAnimation) { resetAnimation(); return; } @@ -58,18 +57,14 @@ function AnimatedSettlementButton({isVisible, ...settlementButtonProps}: Animate // Wait for the above animation + 1s delay before hiding the component height.value = withDelay( 1200, - withTiming(0, {duration: 200}, () => runOnJS(setIsAnimationRunning)(false)), + withTiming(0, {duration: 200}, () => runOnJS(onAnimationFinish)()), ); paymentCompleteTextOpacity.value = withDelay(1200, withTiming(0, {duration: 200})); - }, [isAnimationRunning, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, resetAnimation]); - - if (!isVisible && !isAnimationRunning) { - return null; - } + }, [shouldStartPaidAnimation, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, resetAnimation]); return ( - {isAnimationRunning && ( + {shouldStartPaidAnimation && ( Payment complete @@ -78,10 +73,6 @@ function AnimatedSettlementButton({isVisible, ...settlementButtonProps}: Animate { - const isPaid = !!settlementButtonProps.onPress(paymentType, payAsBusiness); - setIsAnimationRunning(isPaid); - }} /> diff --git a/src/components/SettlementButton/types.ts b/src/components/SettlementButton/types.ts index 3eb7e63937d9c..8db094100aea6 100644 --- a/src/components/SettlementButton/types.ts +++ b/src/components/SettlementButton/types.ts @@ -11,7 +11,7 @@ type EnablePaymentsRoute = typeof ROUTES.ENABLE_PAYMENTS | typeof ROUTES.IOU_SEN type SettlementButtonProps = { /** Callback to execute when this button is pressed. Receives a single payment type argument. */ - onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean) => void | boolean; + onPress: (paymentType?: PaymentMethodType, payAsBusiness?: boolean) => void; /** Callback when the payment options popover is shown */ onPaymentOptionsShow?: () => void; From 1a8b84771af945332519c73e9f8814be522abc9c Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 9 Sep 2024 18:38:19 +0700 Subject: [PATCH 08/12] disable the button while animating --- src/components/Button/index.tsx | 5 +++++ src/components/ButtonWithDropdownMenu/index.tsx | 2 ++ src/components/ButtonWithDropdownMenu/types.ts | 3 +++ .../SettlementButton/AnimatedSettlementButton.tsx | 11 ++++++++++- src/components/SettlementButton/index.tsx | 2 ++ src/components/SettlementButton/types.ts | 3 +++ 6 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index 698591d22bfd8..2716ae0ca6260 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -77,6 +77,9 @@ type ButtonProps = Partial & { /** Additional styles to add after local styles. Applied to Pressable portion of button */ style?: StyleProp; + /** Additional styles to add to the component when it's disabled */ + disabledStyle?: StyleProp; + /** Additional button styles. Specific to the OpacityView of the button */ innerStyles?: StyleProp; @@ -202,6 +205,7 @@ function Button( enterKeyEventListenerPriority = 0, style = [], + disabledStyle, innerStyles = [], textStyles = [], textHoverStyles = [], @@ -376,6 +380,7 @@ function Button( danger && !isDisabled ? styles.buttonDangerHovered : undefined, hoverStyles, ]} + disabledStyle={disabledStyle} id={id} accessibilityLabel={accessibilityLabel} role={CONST.ROLE.BUTTON} diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index ddab08bdc1d3e..8ee390f908bfd 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -25,6 +25,7 @@ function ButtonWithDropdownMenu({ menuHeaderText = '', customText, style, + disabledStyle, buttonSize = CONST.DROPDOWN_BUTTON_SIZE.MEDIUM, anchorAlignment = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, @@ -156,6 +157,7 @@ function ButtonWithDropdownMenu({ pressOnEnter={pressOnEnter} isDisabled={isDisabled || !!options[0].disabled} style={[styles.w100, style]} + disabledStyle={disabledStyle} isLoading={isLoading} text={selectedItem.text} onPress={(event) => onPress(event, options[0].value)} diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index 58ad58ce4e68b..6b168a7ff566c 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -66,6 +66,9 @@ type ButtonWithDropdownMenuProps = { /** Additional styles to add to the component */ style?: StyleProp; + /** Additional styles to add to the component when it's disabled */ + disabledStyle?: StyleProp; + /** Menu options to display */ /** e.g. [{text: 'Pay with Expensify', icon: Wallet}] */ options: Array>; diff --git a/src/components/SettlementButton/AnimatedSettlementButton.tsx b/src/components/SettlementButton/AnimatedSettlementButton.tsx index 91ef7429e81e5..2d7478b492586 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -11,7 +11,7 @@ type AnimatedSettlementButtonProps = SettlementButtonProps & { onAnimationFinish: () => void; }; -function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, ...settlementButtonProps}: AnimatedSettlementButtonProps) { +function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); const buttonScale = useSharedValue(1); const buttonOpacity = useSharedValue(1); @@ -35,6 +35,13 @@ function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, overflow: 'hidden', })); + const buttonDisabledStyle = shouldStartPaidAnimation + ? { + opacity: 1, + ...styles.cursorDefault, + } + : undefined; + const resetAnimation = useCallback(() => { // eslint-disable-next-line react-compiler/react-compiler buttonScale.value = 1; @@ -73,6 +80,8 @@ function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 69ae02387203d..6af61d761cd58 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -68,6 +68,7 @@ function SettlementButton({ shouldShowApproveButton = false, shouldDisableApproveButton = false, style, + disabledStyle, shouldShowPersonalBankAccountOption = false, enterKeyEventListenerPriority = 0, confirmApproval, @@ -245,6 +246,7 @@ function SettlementButton({ options={paymentButtonOptions} onOptionSelected={(option) => savePreferredPaymentMethod(policyID, option.value)} style={style} + disabledStyle={disabledStyle} buttonSize={buttonSize} anchorAlignment={paymentMethodDropdownAnchorAlignment} enterKeyEventListenerPriority={enterKeyEventListenerPriority} diff --git a/src/components/SettlementButton/types.ts b/src/components/SettlementButton/types.ts index 8db094100aea6..0a26aec914e05 100644 --- a/src/components/SettlementButton/types.ts +++ b/src/components/SettlementButton/types.ts @@ -49,6 +49,9 @@ type SettlementButtonProps = { /** Additional styles to add to the component */ style?: StyleProp; + /** Additional styles to add to the component when it's disabled */ + disabledStyle?: StyleProp; + /** Total money amount in form */ formattedAmount?: string; From 77f814fbeebae2cc048a95f672a40274600c8b68 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 10 Sep 2024 10:22:23 +0700 Subject: [PATCH 09/12] move value to const --- src/CONST.ts | 2 ++ .../AnimatedSettlementButton.tsx | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index cf3facb0d1d82..3c218063dd509 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -187,6 +187,8 @@ 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, 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 2d7478b492586..51d029b8e4b2a 100644 --- a/src/components/SettlementButton/AnimatedSettlementButton.tsx +++ b/src/components/SettlementButton/AnimatedSettlementButton.tsx @@ -3,6 +3,7 @@ import Animated, {runOnJS, useAnimatedStyle, useSharedValue, withDelay, withTimi import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import SettlementButton from '.'; import type SettlementButtonProps from './types'; @@ -28,13 +29,11 @@ function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, position: 'absolute', alignSelf: 'center', })); - const containerStyles = useAnimatedStyle(() => ({ height: height.value, justifyContent: 'center', overflow: 'hidden', })); - const buttonDisabledStyle = shouldStartPaidAnimation ? { opacity: 1, @@ -57,16 +56,17 @@ function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, return; } // eslint-disable-next-line react-compiler/react-compiler - buttonScale.value = withTiming(0, {duration: 200}); - buttonOpacity.value = withTiming(0, {duration: 200}); - paymentCompleteTextScale.value = withTiming(1, {duration: 200}); + 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}); // Wait for the above animation + 1s delay before hiding the component + const totalDelay = CONST.ANIMATION_PAY_BUTTON_DURATION + CONST.ANIMATION_PAY_BUTTON_HIDE_DELAY; height.value = withDelay( - 1200, - withTiming(0, {duration: 200}, () => runOnJS(onAnimationFinish)()), + totalDelay, + withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION}, () => runOnJS(onAnimationFinish)()), ); - paymentCompleteTextOpacity.value = withDelay(1200, withTiming(0, {duration: 200})); + paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION})); }, [shouldStartPaidAnimation, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, resetAnimation]); return ( From 45798dd1910d16dd67296bbc26a939522e02084e Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 13 Sep 2024 11:31:13 +0700 Subject: [PATCH 10/12] renaming --- src/components/ReportActionItem/ReportPreview.tsx | 12 ++++++------ .../SettlementButton/AnimatedSettlementButton.tsx | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 535786fcf063c..367defc4ff43b 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -137,7 +137,7 @@ function ReportPreview({ [transactions, iouReportID, action], ); - const [shouldStartPaidAnimation, setShouldStartPaidAnimation] = useState(false); + const [isPaidAnimationRunning, setIsPaidAnimationRunning] = useState(false); const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); const [requestType, setRequestType] = useState(); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(iouReport, policy); @@ -198,7 +198,7 @@ function ReportPreview({ const {isDelegateAccessRestricted, delegatorEmail} = useDelegateUserDetails(); const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false); - const stopAnimation = useCallback(() => setShouldStartPaidAnimation(false), []); + const stopAnimation = useCallback(() => setIsPaidAnimationRunning(false), []); const confirmPayment = (type: PaymentMethodType | undefined, payAsBusiness?: boolean) => { if (!type) { return; @@ -210,7 +210,7 @@ function ReportPreview({ } else if (ReportUtils.hasHeldExpenses(iouReport?.reportID)) { setIsHoldMenuVisible(true); } else if (chatReport && iouReport) { - setShouldStartPaidAnimation(true); + setIsPaidAnimationRunning(true); HapticFeedback.longPress(); if (ReportUtils.isInvoiceReport(iouReport)) { IOU.payInvoice(type, chatReport, iouReport, payAsBusiness); @@ -312,8 +312,8 @@ function ReportPreview({ const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const shouldShowPayButton = useMemo( - () => shouldStartPaidAnimation || IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions), - [shouldStartPaidAnimation, iouReport, chatReport, policy, allTransactions], + () => isPaidAnimationRunning || IOU.canIOUBePaid(iouReport, chatReport, policy, allTransactions), + [isPaidAnimationRunning, iouReport, chatReport, policy, allTransactions], ); const shouldShowApproveButton = useMemo(() => IOU.canApproveIOU(iouReport, policy), [iouReport, policy]); @@ -482,7 +482,7 @@ function ReportPreview({ {shouldShowSettlementButton && ( void; }; -function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { +function AnimatedSettlementButton({isPaidAnimationRunning, onAnimationFinish, isDisabled, ...settlementButtonProps}: AnimatedSettlementButtonProps) { const styles = useThemeStyles(); const buttonScale = useSharedValue(1); const buttonOpacity = useSharedValue(1); @@ -34,7 +34,7 @@ function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, justifyContent: 'center', overflow: 'hidden', })); - const buttonDisabledStyle = shouldStartPaidAnimation + const buttonDisabledStyle = isPaidAnimationRunning ? { opacity: 1, ...styles.cursorDefault, @@ -51,7 +51,7 @@ function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, }, [buttonScale, buttonOpacity, paymentCompleteTextScale, paymentCompleteTextOpacity, height]); useEffect(() => { - if (!shouldStartPaidAnimation) { + if (!isPaidAnimationRunning) { resetAnimation(); return; } @@ -67,11 +67,11 @@ function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION}, () => runOnJS(onAnimationFinish)()), ); paymentCompleteTextOpacity.value = withDelay(totalDelay, withTiming(0, {duration: CONST.ANIMATION_PAY_BUTTON_DURATION})); - }, [shouldStartPaidAnimation, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, resetAnimation]); + }, [isPaidAnimationRunning, onAnimationFinish, buttonOpacity, buttonScale, height, paymentCompleteTextOpacity, paymentCompleteTextScale, resetAnimation]); return ( - {shouldStartPaidAnimation && ( + {isPaidAnimationRunning && ( Payment complete @@ -80,7 +80,7 @@ function AnimatedSettlementButton({shouldStartPaidAnimation, onAnimationFinish, From 327da6f941f8b9906e6e6f726e59df1f434a8418 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 13 Sep 2024 11:31:25 +0700 Subject: [PATCH 11/12] replace withOnyx with useOnyx --- src/components/SettlementButton/index.tsx | 35 ++++++----------------- 1 file changed, 8 insertions(+), 27 deletions(-) diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index 3a15f7a12d0c6..cb63436badc28 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -1,7 +1,6 @@ import React, {useMemo} from 'react'; import type {GestureResponderEvent} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; -import {useOnyx, withOnyx} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {PaymentType} from '@components/ButtonWithDropdownMenu/types'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -18,7 +17,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {LastPaymentMethod, Policy} from '@src/types/onyx'; +import type {LastPaymentMethod} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type SettlementButtonProps from './types'; @@ -27,16 +26,6 @@ type KYCFlowEvent = GestureResponderEvent | KeyboardEvent | undefined; type TriggerKYCFlow = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType) => void; -type SettlementButtonOnyxProps = { - /** The last payment method used per policy */ - nvpLastPaymentMethod?: OnyxEntry; - - /** The policy of the report */ - policy: OnyxEntry; -}; - -type SettlementButtonWithOnyxProps = SettlementButtonProps & SettlementButtonOnyxProps; - function SettlementButton({ addDebitCardRoute = ROUTES.IOU_SEND_ADD_DEBIT_CARD, addBankAccountRoute = '', @@ -53,9 +42,6 @@ function SettlementButton({ currency = CONST.CURRENCY.USD, enablePaymentsRoute, iouReport, - // The "nvpLastPaymentMethod" object needs to be stable to prevent the "useMemo" - // hook from being recreated unnecessarily, hence the use of CONST.EMPTY_OBJECT - nvpLastPaymentMethod = CONST.EMPTY_OBJECT, isDisabled = false, isLoading = false, formattedAmount = '', @@ -70,16 +56,19 @@ function SettlementButton({ shouldShowPersonalBankAccountOption = false, enterKeyEventListenerPriority = 0, confirmApproval, - policy, useKeyboardShortcuts = false, onPaymentOptionsShow, onPaymentOptionsHide, -}: SettlementButtonWithOnyxProps) { +}: SettlementButtonProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const session = useSession(); // The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here. const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || -1}`); + // The "nvpLastPaymentMethod" object needs to be stable to prevent the "useMemo" + // hook from being recreated unnecessarily, hence the use of CONST.EMPTY_OBJECT + const [nvpLastPaymentMethod = CONST.EMPTY_OBJECT as LastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {selector: (paymentMethod) => paymentMethod ?? {}}); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const isInvoiceReport = (!isEmptyObject(iouReport) && ReportUtils.isInvoiceReport(iouReport)) || false; const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicyExpenseChat(chatReport); const shouldShowPaywithExpensifyOption = @@ -252,12 +241,4 @@ function SettlementButton({ SettlementButton.displayName = 'SettlementButton'; -export default withOnyx({ - nvpLastPaymentMethod: { - key: ONYXKEYS.NVP_LAST_PAYMENT_METHOD, - selector: (paymentMethod) => paymentMethod ?? {}, - }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, -})(SettlementButton); +export default SettlementButton; From eb0027296bb714a4f9d45b83a2129fb208676742 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 13 Sep 2024 19:00:47 +0700 Subject: [PATCH 12/12] simplify the code and update useEffect deps --- src/components/SettlementButton/index.tsx | 38 +++++++++++++++-------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/components/SettlementButton/index.tsx b/src/components/SettlementButton/index.tsx index cb63436badc28..75499b48305be 100644 --- a/src/components/SettlementButton/index.tsx +++ b/src/components/SettlementButton/index.tsx @@ -17,7 +17,6 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {LastPaymentMethod} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type SettlementButtonProps from './types'; @@ -47,7 +46,7 @@ function SettlementButton({ formattedAmount = '', onPress, pressOnEnter = false, - policyID = '', + policyID = '-1', shouldHidePaymentOptions = false, shouldShowApproveButton = false, shouldDisableApproveButton = false, @@ -65,9 +64,7 @@ function SettlementButton({ const session = useSession(); // The app would crash due to subscribing to the entire report collection if chatReportID is an empty string. So we should have a fallback ID here. const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${chatReportID || -1}`); - // The "nvpLastPaymentMethod" object needs to be stable to prevent the "useMemo" - // hook from being recreated unnecessarily, hence the use of CONST.EMPTY_OBJECT - const [nvpLastPaymentMethod = CONST.EMPTY_OBJECT as LastPaymentMethod] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {selector: (paymentMethod) => paymentMethod ?? {}}); + const [lastPaymentMethod = '-1'] = useOnyx(ONYXKEYS.NVP_LAST_PAYMENT_METHOD, {selector: (paymentMethod) => paymentMethod?.[policyID]}); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const isInvoiceReport = (!isEmptyObject(iouReport) && ReportUtils.isInvoiceReport(iouReport)) || false; const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicyExpenseChat(chatReport); @@ -109,9 +106,6 @@ function SettlementButton({ } // To achieve the one tap pay experience we need to choose the correct payment type as default. - // If the user has previously chosen a specific payment option or paid for some expense, - // let's use the last payment method or use default. - const paymentMethod = nvpLastPaymentMethod?.[policyID] ?? '-1'; if (canUseWallet) { buttonOptions.push(paymentMethods[CONST.IOU.PAYMENT_TYPE.EXPENSIFY]); } @@ -160,14 +154,27 @@ function SettlementButton({ buttonOptions.push(approveButtonOption); } - // Put the preferred payment method to the front of the array, so it's shown as default - if (paymentMethod) { - return buttonOptions.sort((method) => (method.value === paymentMethod ? -1 : 0)); + // Put the preferred payment method to the front of the array, so it's shown as default. We assume their last payment method is their preferred. + if (lastPaymentMethod) { + return buttonOptions.sort((method) => (method.value === lastPaymentMethod ? -1 : 0)); } return buttonOptions; // We don't want to reorder the options when the preferred payment method changes while the button is still visible // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps - }, [currency, formattedAmount, iouReport, chatReport, policyID, translate, shouldHidePaymentOptions, shouldShowApproveButton, shouldDisableApproveButton]); + }, [ + iouReport, + translate, + formattedAmount, + shouldDisableApproveButton, + isInvoiceReport, + currency, + shouldHidePaymentOptions, + shouldShowApproveButton, + shouldShowPaywithExpensifyOption, + shouldShowPayElsewhereOption, + chatReport, + onPress, + ]); const selectPaymentType = (event: KYCFlowEvent, iouPaymentType: PaymentMethodType, triggerKYCFlow: TriggerKYCFlow) => { if (policy && SubscriptionUtils.shouldRestrictUserBillableActions(policy.id)) { @@ -226,7 +233,12 @@ function SettlementButton({ onPress={(event, iouPaymentType) => selectPaymentType(event, iouPaymentType, triggerKYCFlow)} pressOnEnter={pressOnEnter} options={paymentButtonOptions} - onOptionSelected={(option) => savePreferredPaymentMethod(policyID, option.value)} + onOptionSelected={(option) => { + if (policyID === '-1') { + return; + } + savePreferredPaymentMethod(policyID, option.value); + }} style={style} disabledStyle={disabledStyle} buttonSize={buttonSize}