From e8f948f68656e68853e5522a8b4df732e05e6e85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 19 Oct 2023 14:24:41 +0200 Subject: [PATCH 001/635] install native-stack --- package-lock.json | 45 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + 2 files changed, 46 insertions(+) diff --git a/package-lock.json b/package-lock.json index 95c25f602e30..75d3e9fe0ed0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,6 +37,7 @@ "@react-native-picker/picker": "^2.4.3", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.6", + "@react-navigation/native-stack": "^6.9.14", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.0.11", @@ -8961,6 +8962,33 @@ "react-native": "*" } }, + "node_modules/@react-navigation/native-stack": { + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.9.14.tgz", + "integrity": "sha512-7RiZkvMFN6f0kmANc63B/0m9ttQ2JpDIPWQwPU93FP698s19KTOyu7uxgl7Oi3bvsqHFO5JfiR7B+4h8lh9dxw==", + "dependencies": { + "@react-navigation/elements": "^1.3.19", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "@react-navigation/native": "^6.0.0", + "react": "*", + "react-native": "*", + "react-native-safe-area-context": ">= 3.0.0", + "react-native-screens": ">= 3.0.0" + } + }, + "node_modules/@react-navigation/native-stack/node_modules/@react-navigation/elements": { + "version": "1.3.19", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.19.tgz", + "integrity": "sha512-7hLvSYKPuDS070pza5gd43WDX7QgfuEmuTWNbCJhKdWlLudYmq3qzxGCBwCfO2dEI6+p8tla5wruaWiGKAbTYw==", + "peerDependencies": { + "@react-navigation/native": "^6.0.0", + "react": "*", + "react-native": "*", + "react-native-safe-area-context": ">= 3.0.0" + } + }, "node_modules/@react-navigation/routers": { "version": "6.1.8", "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.8.tgz", @@ -59320,6 +59348,23 @@ "nanoid": "^3.1.23" } }, + "@react-navigation/native-stack": { + "version": "6.9.14", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.9.14.tgz", + "integrity": "sha512-7RiZkvMFN6f0kmANc63B/0m9ttQ2JpDIPWQwPU93FP698s19KTOyu7uxgl7Oi3bvsqHFO5JfiR7B+4h8lh9dxw==", + "requires": { + "@react-navigation/elements": "^1.3.19", + "warn-once": "^0.1.0" + }, + "dependencies": { + "@react-navigation/elements": { + "version": "1.3.19", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.19.tgz", + "integrity": "sha512-7hLvSYKPuDS070pza5gd43WDX7QgfuEmuTWNbCJhKdWlLudYmq3qzxGCBwCfO2dEI6+p8tla5wruaWiGKAbTYw==", + "requires": {} + } + } + }, "@react-navigation/routers": { "version": "6.1.8", "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.8.tgz", diff --git a/package.json b/package.json index 1db859827c41..665951328bd1 100644 --- a/package.json +++ b/package.json @@ -82,6 +82,7 @@ "@react-native-picker/picker": "^2.4.3", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.6", + "@react-navigation/native-stack": "^6.9.14", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.0.11", From b180872b639ceb3e4aa399645c9c85ec87b0b0eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 19 Oct 2023 15:14:37 +0200 Subject: [PATCH 002/635] create PlatformStackNavigator with different impls for web & native --- src/libs/Navigation/AppNavigator/ModalStackNavigators.js | 7 +++++-- .../AppNavigator/Navigators/CentralPaneNavigator.js | 4 ++-- src/libs/Navigation/AppNavigator/Navigators/Overlay.js | 1 + .../AppNavigator/Navigators/RightModalNavigator.js | 4 ++-- src/libs/Navigation/AppNavigator/PublicScreens.js | 4 ++-- .../AppNavigator/createCustomStackNavigator/index.js | 4 ++-- src/libs/Navigation/PlatformStackNavigator/index.native.ts | 7 +++++++ src/libs/Navigation/PlatformStackNavigator/index.ts | 7 +++++++ 8 files changed, 28 insertions(+), 10 deletions(-) create mode 100644 src/libs/Navigation/PlatformStackNavigator/index.native.ts create mode 100644 src/libs/Navigation/PlatformStackNavigator/index.ts diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index cfc8f815e4fe..2e5950e1b094 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -1,9 +1,12 @@ import _ from 'underscore'; import React from 'react'; -import {createStackNavigator, CardStyleInterpolators} from '@react-navigation/stack'; +import {CardStyleInterpolators} from '@react-navigation/stack'; +import * as PlatformStackNavigator from '../PlatformStackNavigator'; + import styles from '../../../styles/styles'; import SCREENS from '../../../SCREENS'; +// TODO: migrate options const defaultSubRouteOptions = { cardStyle: styles.navigationScreenCardStyle, headerShown: false, @@ -17,7 +20,7 @@ const defaultSubRouteOptions = { * @returns {Function} */ function createModalStackNavigator(screens) { - const ModalStackNavigator = createStackNavigator(); + const ModalStackNavigator = PlatformStackNavigator.createPlatformStackNavigator(); return () => ( {_.map(screens, (getComponent, name) => ( diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js index 64eadcbe06c3..b5fa76a48b90 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js @@ -1,12 +1,12 @@ import React from 'react'; -import {createStackNavigator} from '@react-navigation/stack'; +import * as PlatformStackNavigator from '../../PlatformStackNavigator'; import SCREENS from '../../../../SCREENS'; import ReportScreenWrapper from '../ReportScreenWrapper'; import getCurrentUrl from '../../currentUrl'; import styles from '../../../../styles/styles'; import FreezeWrapper from '../../FreezeWrapper'; -const Stack = createStackNavigator(); +const Stack = PlatformStackNavigator.createPlatformStackNavigator(); const url = getCurrentUrl(); const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : undefined; diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js b/src/libs/Navigation/AppNavigator/Navigators/Overlay.js index 1b2faff8c5e3..ed3c27aed673 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js +++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.js @@ -1,5 +1,6 @@ import React from 'react'; import {Animated, View} from 'react-native'; +// TODO: migrate import {useCardAnimation} from '@react-navigation/stack'; import PropTypes from 'prop-types'; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js index 5f24ec159828..822a1c6ead55 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js @@ -1,6 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import {createStackNavigator} from '@react-navigation/stack'; +import * as PlatformStackNavigator from '../../PlatformStackNavigator'; import * as ModalStackNavigators from '../ModalStackNavigators'; import RHPScreenOptions from '../RHPScreenOptions'; @@ -10,7 +10,7 @@ import styles from '../../../../styles/styles'; import Overlay from './Overlay'; import NoDropZone from '../../../../components/DragAndDrop/NoDropZone'; -const Stack = createStackNavigator(); +const Stack = PlatformStackNavigator.createPlatformStackNavigator(); const propTypes = { ...withNavigationPropTypes, diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.js b/src/libs/Navigation/AppNavigator/PublicScreens.js index 7b0afb787278..6a2ad51cab6b 100644 --- a/src/libs/Navigation/AppNavigator/PublicScreens.js +++ b/src/libs/Navigation/AppNavigator/PublicScreens.js @@ -1,5 +1,5 @@ import React from 'react'; -import {createStackNavigator} from '@react-navigation/stack'; +import * as PlatformStackNavigator from '../PlatformStackNavigator'; import SignInPage from '../../../pages/signin/SignInPage'; import ValidateLoginPage from '../../../pages/ValidateLoginPage'; import LogInWithShortLivedAuthTokenPage from '../../../pages/LogInWithShortLivedAuthTokenPage'; @@ -10,7 +10,7 @@ import AppleSignInDesktopPage from '../../../pages/signin/AppleSignInDesktopPage import GoogleSignInDesktopPage from '../../../pages/signin/GoogleSignInDesktopPage'; import SAMLSignInPage from '../../../pages/signin/SAMLSignInPage'; -const RootStack = createStackNavigator(); +const RootStack = PlatformStackNavigator.createPlatformStackNavigator(); function PublicScreens() { return ( diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js index 58be3d2af3da..5b9679426a4f 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js @@ -1,7 +1,7 @@ import React, {useRef} from 'react'; import PropTypes from 'prop-types'; import {useNavigationBuilder, createNavigatorFactory} from '@react-navigation/native'; -import {StackView} from '@react-navigation/stack'; +import * as PlatformStackNavigator from '../../PlatformStackNavigator'; import CustomRouter from './CustomRouter'; import useWindowDimensions from '../../../../hooks/useWindowDimensions'; @@ -42,7 +42,7 @@ function ResponsiveStackNavigator(props) { return ( - Date: Thu, 19 Oct 2023 15:22:14 +0200 Subject: [PATCH 003/635] create separate functions --- src/libs/Navigation/AppNavigator/ModalStackNavigators.js | 4 ++-- .../AppNavigator/Navigators/CentralPaneNavigator.js | 4 ++-- .../AppNavigator/Navigators/RightModalNavigator.js | 4 ++-- src/libs/Navigation/AppNavigator/PublicScreens.js | 4 ++-- .../AppNavigator/createCustomStackNavigator/index.js | 4 ++-- .../Navigation/PlatformStackNavigation/StackView.native.ts | 3 +++ src/libs/Navigation/PlatformStackNavigation/StackView.ts | 3 +++ .../createPlatformStackNavigator.native.ts | 7 +++++++ .../createPlatformStackNavigator.ts | 7 +++++++ src/libs/Navigation/PlatformStackNavigator/index.native.ts | 7 ------- src/libs/Navigation/PlatformStackNavigator/index.ts | 7 ------- 11 files changed, 30 insertions(+), 24 deletions(-) create mode 100644 src/libs/Navigation/PlatformStackNavigation/StackView.native.ts create mode 100644 src/libs/Navigation/PlatformStackNavigation/StackView.ts create mode 100644 src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.native.ts create mode 100644 src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.ts delete mode 100644 src/libs/Navigation/PlatformStackNavigator/index.native.ts delete mode 100644 src/libs/Navigation/PlatformStackNavigator/index.ts diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 2e5950e1b094..985aacc9da96 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import React from 'react'; import {CardStyleInterpolators} from '@react-navigation/stack'; -import * as PlatformStackNavigator from '../PlatformStackNavigator'; +import createPlatformStackNavigator from '../PlatformStackNavigation/createPlatformStackNavigator'; import styles from '../../../styles/styles'; import SCREENS from '../../../SCREENS'; @@ -20,7 +20,7 @@ const defaultSubRouteOptions = { * @returns {Function} */ function createModalStackNavigator(screens) { - const ModalStackNavigator = PlatformStackNavigator.createPlatformStackNavigator(); + const ModalStackNavigator = createPlatformStackNavigator(); return () => ( {_.map(screens, (getComponent, name) => ( diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js index b5fa76a48b90..754a25694785 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator.js @@ -1,12 +1,12 @@ import React from 'react'; -import * as PlatformStackNavigator from '../../PlatformStackNavigator'; +import createPlatformStackNavigator from '../../PlatformStackNavigation/createPlatformStackNavigator'; import SCREENS from '../../../../SCREENS'; import ReportScreenWrapper from '../ReportScreenWrapper'; import getCurrentUrl from '../../currentUrl'; import styles from '../../../../styles/styles'; import FreezeWrapper from '../../FreezeWrapper'; -const Stack = PlatformStackNavigator.createPlatformStackNavigator(); +const Stack = createPlatformStackNavigator(); const url = getCurrentUrl(); const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : undefined; diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js index 822a1c6ead55..2086490c597f 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.js @@ -1,6 +1,6 @@ import React from 'react'; import {View} from 'react-native'; -import * as PlatformStackNavigator from '../../PlatformStackNavigator'; +import createPlatformStackNavigator from '../../PlatformStackNavigation/createPlatformStackNavigator'; import * as ModalStackNavigators from '../ModalStackNavigators'; import RHPScreenOptions from '../RHPScreenOptions'; @@ -10,7 +10,7 @@ import styles from '../../../../styles/styles'; import Overlay from './Overlay'; import NoDropZone from '../../../../components/DragAndDrop/NoDropZone'; -const Stack = PlatformStackNavigator.createPlatformStackNavigator(); +const Stack = createPlatformStackNavigator(); const propTypes = { ...withNavigationPropTypes, diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.js b/src/libs/Navigation/AppNavigator/PublicScreens.js index 6a2ad51cab6b..720784f97ec1 100644 --- a/src/libs/Navigation/AppNavigator/PublicScreens.js +++ b/src/libs/Navigation/AppNavigator/PublicScreens.js @@ -1,5 +1,5 @@ import React from 'react'; -import * as PlatformStackNavigator from '../PlatformStackNavigator'; +import createPlatformStackNavigator from '../PlatformStackNavigation/createPlatformStackNavigator'; import SignInPage from '../../../pages/signin/SignInPage'; import ValidateLoginPage from '../../../pages/ValidateLoginPage'; import LogInWithShortLivedAuthTokenPage from '../../../pages/LogInWithShortLivedAuthTokenPage'; @@ -10,7 +10,7 @@ import AppleSignInDesktopPage from '../../../pages/signin/AppleSignInDesktopPage import GoogleSignInDesktopPage from '../../../pages/signin/GoogleSignInDesktopPage'; import SAMLSignInPage from '../../../pages/signin/SAMLSignInPage'; -const RootStack = PlatformStackNavigator.createPlatformStackNavigator(); +const RootStack = createPlatformStackNavigator(); function PublicScreens() { return ( diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js index 5b9679426a4f..60894018ffd3 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js @@ -1,7 +1,7 @@ import React, {useRef} from 'react'; import PropTypes from 'prop-types'; import {useNavigationBuilder, createNavigatorFactory} from '@react-navigation/native'; -import * as PlatformStackNavigator from '../../PlatformStackNavigator'; +import StackView from '../../PlatformStackNavigation/StackView'; import CustomRouter from './CustomRouter'; import useWindowDimensions from '../../../../hooks/useWindowDimensions'; @@ -42,7 +42,7 @@ function ResponsiveStackNavigator(props) { return ( - Date: Thu, 19 Oct 2023 17:06:52 +0200 Subject: [PATCH 004/635] fix right modal options --- .../defaultScreenOptions/index.native.ts | 10 +++++++++ .../index.ts} | 0 .../index.native.ts | 7 +++++++ .../getRightModalNavigatorOptions/index.ts | 21 +++++++++++++++++++ .../getRootNavigatorScreenOptions.js | 15 ++----------- .../getNavigationModalCardStyles/types.ts | 4 +--- 6 files changed, 41 insertions(+), 16 deletions(-) create mode 100644 src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts rename src/libs/Navigation/AppNavigator/{defaultScreenOptions.js => defaultScreenOptions/index.ts} (100%) create mode 100644 src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts create mode 100644 src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts diff --git a/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts new file mode 100644 index 000000000000..12ba580c41d8 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts @@ -0,0 +1,10 @@ +const defaultScreenOptions = { + contentStyle: { + overflow: 'visible', + flex: 1, + }, + headerShown: false, + animationTypeForReplace: 'push', +}; + +export default defaultScreenOptions; diff --git a/src/libs/Navigation/AppNavigator/defaultScreenOptions.js b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.ts similarity index 100% rename from src/libs/Navigation/AppNavigator/defaultScreenOptions.js rename to src/libs/Navigation/AppNavigator/defaultScreenOptions/index.ts diff --git a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts new file mode 100644 index 000000000000..35d4605a2e5c --- /dev/null +++ b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts @@ -0,0 +1,7 @@ +import {NativeStackNavigationOptions} from '@react-navigation/native-stack'; + +const rightModalNavigatorOptions = (): NativeStackNavigationOptions => ({ + presentation: 'card', +}); + +export default rightModalNavigatorOptions; diff --git a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts new file mode 100644 index 000000000000..ccab8ecd9fa5 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts @@ -0,0 +1,21 @@ +import {StackNavigationOptions} from '@react-navigation/stack'; +import getNavigationModalCardStyle from '../../../../styles/getNavigationModalCardStyles'; +import modalCardStyleInterpolator from '../modalCardStyleInterpolator'; + +const rightModalNavigatorOptions = (isSmallScreenWidth: boolean): StackNavigationOptions => ({ + cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), + presentation: 'transparentModal', + + // We want pop in RHP since there are some flows that would work weird otherwise + animationTypeForReplace: 'pop', + cardStyle: { + ...getNavigationModalCardStyle(), + + // This is necessary to cover translated sidebar with overlay. + width: isSmallScreenWidth ? '100%' : '200%', + // Excess space should be on the left so we need to position from right. + right: 0, + }, +}); + +export default rightModalNavigatorOptions; diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js index a7456fb071b4..ef879e269242 100644 --- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js +++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js @@ -3,6 +3,7 @@ import styles from '../../../styles/styles'; import variables from '../../../styles/variables'; import getNavigationModalCardStyle from '../../../styles/getNavigationModalCardStyles'; import CONFIG from '../../../CONFIG'; +import getRightModalNavigatorOptions from './getRightModalNavigatorOptions'; const commonScreenOptions = { headerShown: false, @@ -15,19 +16,7 @@ const commonScreenOptions = { export default (isSmallScreenWidth) => ({ rightModalNavigator: { ...commonScreenOptions, - cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), - presentation: 'transparentModal', - - // We want pop in RHP since there are some flows that would work weird otherwise - animationTypeForReplace: 'pop', - cardStyle: { - ...getNavigationModalCardStyle(), - - // This is necessary to cover translated sidebar with overlay. - width: isSmallScreenWidth ? '100%' : '200%', - // Excess space should be on the left so we need to position from right. - right: 0, - }, + ...getRightModalNavigatorOptions(isSmallScreenWidth), }, homeScreen: { diff --git a/src/styles/getNavigationModalCardStyles/types.ts b/src/styles/getNavigationModalCardStyles/types.ts index 877981dd4dd2..e0dba07dc908 100644 --- a/src/styles/getNavigationModalCardStyles/types.ts +++ b/src/styles/getNavigationModalCardStyles/types.ts @@ -1,7 +1,5 @@ import {ViewStyle} from 'react-native'; -type GetNavigationModalCardStylesParams = {isSmallScreenWidth: number}; - -type GetNavigationModalCardStyles = (params: GetNavigationModalCardStylesParams) => ViewStyle; +type GetNavigationModalCardStyles = () => ViewStyle; export default GetNavigationModalCardStyles; From 27b2a131ccc8d7a9652adc127f67752f6911b447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 19 Oct 2023 17:09:36 +0200 Subject: [PATCH 005/635] add note about card* property usage --- .../Navigation/AppNavigator/getRootNavigatorScreenOptions.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js index ef879e269242..578ed1881b97 100644 --- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js +++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js @@ -22,8 +22,8 @@ export default (isSmallScreenWidth) => ({ homeScreen: { title: CONFIG.SITE_TITLE, ...commonScreenOptions, + // Note: The card* properties won't be applied on mobile platforms, as they use the native defaults. cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), - cardStyle: { ...getNavigationModalCardStyle(), width: isSmallScreenWidth ? '100%' : variables.sideBarWidth, @@ -36,6 +36,7 @@ export default (isSmallScreenWidth) => ({ // eslint-disable-next-line rulesdir/no-negated-variables fullScreen: { ...commonScreenOptions, + cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, true, props), cardStyle: { ...getNavigationModalCardStyle(), @@ -49,8 +50,8 @@ export default (isSmallScreenWidth) => ({ title: CONFIG.SITE_TITLE, ...commonScreenOptions, animationEnabled: isSmallScreenWidth, - cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, true, props), + cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, true, props), cardStyle: { ...getNavigationModalCardStyle(), paddingRight: isSmallScreenWidth ? 0 : variables.sideBarWidth, From 5e5deb49b113133448114eff68fb05c8547b822d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Thu, 19 Oct 2023 17:40:19 +0200 Subject: [PATCH 006/635] migrate defaultSubRouteOptions --- .../Navigation/AppNavigator/ModalStackNavigators.js | 11 +---------- .../modalStackNavigatorOptions/index.native.ts | 9 +++++++++ .../AppNavigator/modalStackNavigatorOptions/index.ts | 10 ++++++++++ 3 files changed, 20 insertions(+), 10 deletions(-) create mode 100644 src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts create mode 100644 src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 985aacc9da96..7f5e8fbc3579 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -1,17 +1,8 @@ import _ from 'underscore'; import React from 'react'; -import {CardStyleInterpolators} from '@react-navigation/stack'; import createPlatformStackNavigator from '../PlatformStackNavigation/createPlatformStackNavigator'; - -import styles from '../../../styles/styles'; import SCREENS from '../../../SCREENS'; - -// TODO: migrate options -const defaultSubRouteOptions = { - cardStyle: styles.navigationScreenCardStyle, - headerShown: false, - cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, -}; +import defaultSubRouteOptions from './modalStackNavigatorOptions'; /** * Create a modal stack navigator with an array of sub-screens. diff --git a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts new file mode 100644 index 000000000000..c54b1d653aca --- /dev/null +++ b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts @@ -0,0 +1,9 @@ +import {NativeStackNavigationOptions} from '@react-navigation/native-stack'; +import styles from '../../../../styles/styles'; + +const defaultSubRouteOptions: NativeStackNavigationOptions = { + contentStyle: styles.navigationScreenCardStyle, + headerShown: false, +}; + +export default defaultSubRouteOptions; diff --git a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts new file mode 100644 index 000000000000..ea241e6f441b --- /dev/null +++ b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts @@ -0,0 +1,10 @@ +import {CardStyleInterpolators, StackNavigationOptions} from '@react-navigation/stack'; +import styles from '../../../../styles/styles'; + +const defaultSubRouteOptions: StackNavigationOptions = { + cardStyle: styles.navigationScreenCardStyle, + headerShown: false, + cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, +}; + +export default defaultSubRouteOptions; From 6e49ccf73e6aa9907a24debc676696bdb9c83c1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 20 Oct 2023 08:08:20 +0200 Subject: [PATCH 007/635] change default animation for android --- .../Navigation/AppNavigator/defaultScreenOptions/index.native.ts | 1 + .../AppNavigator/getRightModalNavigatorOptions/index.native.ts | 1 + .../Navigation/AppNavigator/getRootNavigatorScreenOptions.js | 1 + .../AppNavigator/modalStackNavigatorOptions/index.native.ts | 1 + 4 files changed, 4 insertions(+) diff --git a/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts index 12ba580c41d8..17100bc71bda 100644 --- a/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts +++ b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts @@ -5,6 +5,7 @@ const defaultScreenOptions = { }, headerShown: false, animationTypeForReplace: 'push', + animation: 'slide_from_right', }; export default defaultScreenOptions; diff --git a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts index 35d4605a2e5c..9c0d1fe3abff 100644 --- a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts +++ b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts @@ -2,6 +2,7 @@ import {NativeStackNavigationOptions} from '@react-navigation/native-stack'; const rightModalNavigatorOptions = (): NativeStackNavigationOptions => ({ presentation: 'card', + animation: 'slide_from_right', }); export default rightModalNavigatorOptions; diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js index 578ed1881b97..9d485f239fe6 100644 --- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js +++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js @@ -11,6 +11,7 @@ const commonScreenOptions = { animationEnabled: true, cardOverlayEnabled: true, animationTypeForReplace: 'push', + animation: 'slide_from_right', }; export default (isSmallScreenWidth) => ({ diff --git a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts index c54b1d653aca..96ee4c4b26d2 100644 --- a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts +++ b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts @@ -4,6 +4,7 @@ import styles from '../../../../styles/styles'; const defaultSubRouteOptions: NativeStackNavigationOptions = { contentStyle: styles.navigationScreenCardStyle, headerShown: false, + animation: 'slide_from_right', }; export default defaultSubRouteOptions; From 08c6181183ac2f092e739985a75fed309d8b65c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hanno=20J=2E=20G=C3=B6decke?= Date: Fri, 20 Oct 2023 08:59:45 +0200 Subject: [PATCH 008/635] migrate overlay --- .../Navigators/{Overlay.js => Overlay/index.js} | 9 ++++----- .../AppNavigator/Navigators/Overlay/index.native.js | 7 +++++++ 2 files changed, 11 insertions(+), 5 deletions(-) rename src/libs/Navigation/AppNavigator/Navigators/{Overlay.js => Overlay/index.js} (86%) create mode 100644 src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.js diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.js similarity index 86% rename from src/libs/Navigation/AppNavigator/Navigators/Overlay.js rename to src/libs/Navigation/AppNavigator/Navigators/Overlay/index.js index ed3c27aed673..ad35484d21d8 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js +++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.js @@ -1,14 +1,13 @@ import React from 'react'; import {Animated, View} from 'react-native'; -// TODO: migrate import {useCardAnimation} from '@react-navigation/stack'; import PropTypes from 'prop-types'; -import styles from '../../../../styles/styles'; +import styles from '../../../../../styles/styles'; -import PressableWithoutFeedback from '../../../../components/Pressable/PressableWithoutFeedback'; -import useLocalize from '../../../../hooks/useLocalize'; -import CONST from '../../../../CONST'; +import PressableWithoutFeedback from '../../../../../components/Pressable/PressableWithoutFeedback'; +import useLocalize from '../../../../../hooks/useLocalize'; +import CONST from '../../../../../CONST'; const propTypes = { /* Callback to close the modal */ diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.js b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.js new file mode 100644 index 000000000000..30651e32cbd6 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.js @@ -0,0 +1,7 @@ +function Overlay() { + return null; +} + +Overlay.displayName = 'Overlay'; + +export default Overlay; From 18d46518d7626e8f0966bc51c408aaa6392fbe09 Mon Sep 17 00:00:00 2001 From: s-alves10 Date: Wed, 22 Nov 2023 00:55:01 -0600 Subject: [PATCH 009/635] fix: check if the parent report is the archived room --- src/libs/actions/Task.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index c91f6d1a2eec..e59debd08a1d 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -877,6 +877,11 @@ function canModifyTask(taskReport, sessionAccountID) { return false; } + const parentReport = ReportUtils.getParentReport(taskReport); + if (ReportUtils.isArchivedRoom(parentReport)) { + return false; + } + if (sessionAccountID === getTaskOwnerAccountID(taskReport) || sessionAccountID === getTaskAssigneeAccountID(taskReport)) { return true; } @@ -884,7 +889,6 @@ function canModifyTask(taskReport, sessionAccountID) { // If you don't have access to the task report (maybe haven't opened it yet), check if you can access the parent report // - If the parent report is an #admins only room // - If you are a policy admin - const parentReport = ReportUtils.getParentReport(taskReport); return ReportUtils.isAllowedToComment(parentReport); } From 6fb66b91cafb483f4739f739816828fcaa01d455 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 1 Dec 2023 13:13:03 +0700 Subject: [PATCH 010/635] go back to correct page in referral page --- src/pages/ReferralDetailsPage.js | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/pages/ReferralDetailsPage.js b/src/pages/ReferralDetailsPage.js index 60b5d23b39da..2f773d16a8ff 100644 --- a/src/pages/ReferralDetailsPage.js +++ b/src/pages/ReferralDetailsPage.js @@ -17,7 +17,6 @@ import Navigation from '@libs/Navigation/Navigation'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -58,27 +57,13 @@ function ReferralDetailsPage({route, account}) { return `${CONST.REFERRAL_PROGRAM.LINK}/?thanks=${encodeURIComponent(email)}`; } - function getFallbackRoute() { - const fallbackRoutes = { - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]: ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(CONST.IOU.TYPE.REQUEST), - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]: ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(CONST.IOU.TYPE.SEND), - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]: ROUTES.NEW_CHAT, - [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]: ROUTES.SEARCH, - }; - - return fallbackRoutes[contentType]; - } - return ( - Navigation.goBack(getFallbackRoute())} - /> + Navigation.goBack(getFallbackRoute())} + onPress={() => Navigation.goBack()} pressOnEnter enterKeyEventListenerPriority={1} /> From 1f22bec902d8e96699377a9d10168752e4f8fc61 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 4 Dec 2023 13:46:19 +0700 Subject: [PATCH 011/635] add fallback route for confirm button --- src/pages/ReferralDetailsPage.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/pages/ReferralDetailsPage.js b/src/pages/ReferralDetailsPage.js index 2f773d16a8ff..2b6bc9249bc6 100644 --- a/src/pages/ReferralDetailsPage.js +++ b/src/pages/ReferralDetailsPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -17,6 +17,7 @@ import Navigation from '@libs/Navigation/Navigation'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -53,6 +54,17 @@ function ReferralDetailsPage({route, account}) { const shouldShowBody2 = isShareCode; const shouldShowClipboard = contentType === CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND || isShareCode; + const fallBackRoute = useMemo(() => { + const fallbackRoutes = { + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]: ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(CONST.IOU.TYPE.REQUEST), + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]: ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(CONST.IOU.TYPE.SEND), + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]: ROUTES.NEW_CHAT, + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]: ROUTES.SEARCH, + }; + + return fallbackRoutes[contentType]; + }, [contentType]); + function generateReferralURL(email) { return `${CONST.REFERRAL_PROGRAM.LINK}/?thanks=${encodeURIComponent(email)}`; } @@ -93,7 +105,7 @@ function ReferralDetailsPage({route, account}) { success style={[styles.w100]} text={translate('common.buttonConfirm')} - onPress={() => Navigation.goBack()} + onPress={() => Navigation.goBack(fallBackRoute)} pressOnEnter enterKeyEventListenerPriority={1} /> From dded6349ee50d371ed5c920a4c3334700ad1d537 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 19 Dec 2023 21:55:13 +0530 Subject: [PATCH 012/635] test map when image is not loaded in request view --- .../ReportActionItem/MoneyRequestPreview.js | 10 +++++++++- src/components/ReportActionItem/MoneyRequestView.js | 13 ++++++++++++- src/libs/actions/IOU.js | 3 +++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 12c6d0629370..1d798130dfd5 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -38,6 +38,7 @@ import CONST from '@src/CONST'; import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; import ReportActionItemImages from './ReportActionItemImages'; +import ConfirmedRoute from '@components/ConfirmedRoute'; const propTypes = { /** The active IOUReport, used for Onyx subscription */ @@ -169,6 +170,8 @@ function MoneyRequestPreview(props) { const hasPendingWaypoints = lodashGet(props.transaction, 'pendingFields.waypoints', null); + const showMapAsImage = isDistanceRequest && hasPendingWaypoints; + const getSettledMessage = () => { if (isExpensifyCardTransaction) { return translate('common.done'); @@ -257,7 +260,12 @@ function MoneyRequestPreview(props) { !props.onPreviewPressed ? [styles.moneyRequestPreviewBox, ...props.containerStyles] : {}, ]} > - {hasReceipt && ( + {showMapAsImage && ( + + + + )} + {!showMapAsImage && hasReceipt && ( - {hasReceipt && ( + {showMapAsImage && ( + + + + + + )} + {!showMapAsImage && hasReceipt && ( Date: Wed, 20 Dec 2023 04:54:23 -0600 Subject: [PATCH 013/635] fix: remove redundant code --- src/libs/actions/Task.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index 03b94d5f82de..50172158efdb 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -882,8 +882,6 @@ function canModifyTask(taskReport, sessionAccountID, policyRole = '') { return true; } - const parentReport = ReportUtils.getParentReport(taskReport); - if (policyRole && (ReportUtils.isChatRoom(parentReport) || ReportUtils.isPolicyExpenseChat(parentReport)) && policyRole !== CONST.POLICY.ROLE.ADMIN) { return false; } From 2c2b068bf13be2ebef31382f42e2c830679c8814 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 25 Dec 2023 16:20:11 +0700 Subject: [PATCH 014/635] refactor referral route --- src/ROUTES.ts | 20 ++++++++++++++++ src/SCREENS.ts | 5 ++++ .../OptionsSelector/BaseOptionsSelector.js | 6 +++-- .../AppNavigator/ModalStackNavigators.tsx | 5 ++++ src/libs/Navigation/linkingConfig.ts | 5 ++++ src/pages/NewChatPage.js | 2 ++ src/pages/ReferralDetailsPage.js | 23 +++++++++++++++++-- src/pages/SearchPage.js | 2 ++ src/pages/ShareCodePage.js | 2 +- ...yForRefactorRequestParticipantsSelector.js | 14 +++++++++++ .../step/IOURequestStepParticipants.js | 2 ++ 11 files changed, 81 insertions(+), 5 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index ca1fe9f0e81a..64de8064ba81 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -463,6 +463,26 @@ const ROUTES = { route: 'referral/:contentType', getRoute: (contentType: string) => `referral/${contentType}` as const, }, + REFERRAL_DETAILS_MODAL_REQUEST: { + route: 'create/request/participants/:transactionID/:reportID/referral/:contentType', + getRoute: (transactionID: string, reportID: string, contentType: string) => `create/request/participants/${transactionID}/${reportID}/referral/${contentType}` as const, + }, + REFERRAL_DETAILS_MODAL_START_CHAT: { + route: 'new/referral/:contentType', + getRoute: (contentType: string) => `new/referral/${contentType}` as const, + }, + REFERRAL_DETAILS_MODAL_SEND_MONEY: { + route: 'send/new/participants/referral/:contentType', + getRoute: (contentType: string) => `send/new/participants/referral/${contentType}` as const, + }, + REFERRAL_DETAILS_MODAL_REFER_FRIEND: { + route: 'search/referral/:contentType', + getRoute: (contentType: string) => `search/referral/${contentType}` as const, + }, + REFERRAL_DETAILS_MODAL_SHARE_CODE: { + route: 'settings/shareCode/referral/:contentType', + getRoute: (contentType: string) => `settings/shareCode/referral/${contentType}` as const, + }, // These are some one-off routes that will be removed once they're no longer needed (see GH issues for details) SAASTR: 'saastr', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 2cd263237866..b8282ce2d8b7 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -235,6 +235,11 @@ const SCREENS = { REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount', GET_ASSISTANCE: 'GetAssistance', REFERRAL_DETAILS: 'Referral_Details', + REFERRAL_DETAILS_MONEY_REQUEST: 'Referral_Details_Money_Request', + REFERRAL_DETAILS_START_CHAT: 'Referral_Details_Start_Chat', + REFERRAL_DETAILS_SEND_MONEY: 'Referral_Details_Send_Money', + REFERRAL_DETAILS_REFER_FRIEND: 'Referral_Details_Refer_Friend', + REFERRAL_DETAILS_SHARE_CODE: 'Referral_Details_Share_Code', KEYBOARD_SHORTCUTS: 'KeyboardShortcuts', } as const; diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 3c40b3cf1144..725d55cf5925 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -24,7 +24,6 @@ import KeyboardShortcut from '@libs/KeyboardShortcut'; import Navigation from '@libs/Navigation/Navigation'; import setSelection from '@libs/setSelection'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import {defaultProps as optionsSelectorDefaultProps, propTypes as optionsSelectorPropTypes} from './optionsSelectorPropTypes'; const propTypes = { @@ -49,6 +48,9 @@ const propTypes = { /** Referral content type */ referralContentType: PropTypes.string, + /** Referral route */ + referralRoute: PropTypes.string, + ...optionsSelectorPropTypes, ...withLocalizePropTypes, ...withThemeStylesPropTypes, @@ -621,7 +623,7 @@ class BaseOptionsSelector extends Component { { - Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(this.props.referralContentType)); + Navigation.navigate(this.props.referralRoute); }} style={[ this.props.themeStyles.p5, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 256ea6d4eceb..37c751235086 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -277,6 +277,11 @@ const SignInModalStackNavigator = createModalStackNavigator({ [SCREENS.REFERRAL_DETAILS]: () => require('../../../pages/ReferralDetailsPage').default as React.ComponentType, + [SCREENS.REFERRAL_DETAILS_MONEY_REQUEST]: () => require('../../../pages/ReferralDetailsPage').default as React.ComponentType, + [SCREENS.REFERRAL_DETAILS_START_CHAT]: () => require('../../../pages/ReferralDetailsPage').default as React.ComponentType, + [SCREENS.REFERRAL_DETAILS_SEND_MONEY]: () => require('../../../pages/ReferralDetailsPage').default as React.ComponentType, + [SCREENS.REFERRAL_DETAILS_REFER_FRIEND]: () => require('../../../pages/ReferralDetailsPage').default as React.ComponentType, + [SCREENS.REFERRAL_DETAILS_SHARE_CODE]: () => require('../../../pages/ReferralDetailsPage').default as React.ComponentType, }); export { diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index 0383455a5946..e5979bb78075 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -487,6 +487,11 @@ const linkingConfig: LinkingOptions = { [SCREENS.RIGHT_MODAL.REFERRAL]: { screens: { [SCREENS.REFERRAL_DETAILS]: ROUTES.REFERRAL_DETAILS_MODAL.route, + [SCREENS.REFERRAL_DETAILS_MONEY_REQUEST]: ROUTES.REFERRAL_DETAILS_MODAL_REQUEST.route, + [SCREENS.REFERRAL_DETAILS_START_CHAT]: ROUTES.REFERRAL_DETAILS_MODAL_START_CHAT.route, + [SCREENS.REFERRAL_DETAILS_SEND_MONEY]: ROUTES.REFERRAL_DETAILS_MODAL_SEND_MONEY.route, + [SCREENS.REFERRAL_DETAILS_REFER_FRIEND]: ROUTES.REFERRAL_DETAILS_MODAL_REFER_FRIEND.route, + [SCREENS.REFERRAL_DETAILS_SHARE_CODE]: ROUTES.REFERRAL_DETAILS_MODAL_SHARE_CODE.route, }, }, }, diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index d7abbab6e93f..60f4d7c1334a 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -22,6 +22,7 @@ import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; @@ -253,6 +254,7 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, i shouldShowConfirmButton shouldShowReferralCTA referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT} + referralRoute={ROUTES.REFERRAL_DETAILS_MODAL_START_CHAT.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT)} confirmButtonText={selectedOptions.length > 1 ? translate('newChatPage.createGroup') : translate('newChatPage.createChat')} textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} onConfirmSelection={createGroup} diff --git a/src/pages/ReferralDetailsPage.js b/src/pages/ReferralDetailsPage.js index fb02778db72d..49671a59a204 100644 --- a/src/pages/ReferralDetailsPage.js +++ b/src/pages/ReferralDetailsPage.js @@ -1,6 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useMemo, useRef} from 'react'; -import {View} from 'react-native'; +import React, {useRef} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import ContextMenuItem from '@components/ContextMenuItem'; @@ -15,9 +14,11 @@ import useSingleExecution from '@hooks/useSingleExecution'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Clipboard from '@libs/Clipboard'; +import Navigation from '@libs/Navigation/Navigation'; import * as Link from '@userActions/Link'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import * as ReportActionContextMenu from './home/report/ContextMenu/ReportActionContextMenu'; @@ -25,6 +26,10 @@ const propTypes = { /** Navigation route context info provided by react navigation */ route: PropTypes.shape({ params: PropTypes.shape({ + /** The ID of the transaction being configured */ + transactionID: PropTypes.string, + /** The report ID of the IOU */ + reportID: PropTypes.string, /** The type of the content from where CTA was called */ contentType: PropTypes.string, }), @@ -48,6 +53,7 @@ function ReferralDetailsPage({route, account}) { const popoverAnchor = useRef(null); const {isExecuting, singleExecution} = useSingleExecution(); let {contentType} = route.params; + const {transactionID, reportID} = route.params; if (!_.includes(_.values(CONST.REFERRAL_PROGRAM.CONTENT_TYPES), contentType)) { contentType = CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND; @@ -59,6 +65,18 @@ function ReferralDetailsPage({route, account}) { const shouldShowClipboard = contentType === CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND || isShareCode; const referralLink = `${CONST.REFERRAL_PROGRAM.LINK}/?thanks=${encodeURIComponent(account.primaryLogin)}`; + function getFallbackRoute() { + const fallbackRoutes = { + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST]: ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.getRoute(CONST.IOU.TYPE.REQUEST, transactionID, reportID), + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY]: ROUTES.MONEY_REQUEST_PARTICIPANTS.getRoute(CONST.IOU.TYPE.SEND), + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT]: ROUTES.NEW_CHAT, + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND]: ROUTES.SEARCH, + [CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE]: ROUTES.SETTINGS_SHARE_CODE, + }; + + return fallbackRoutes[contentType]; + } + return ( Navigation.navigate(getFallbackRoute())} > {contentHeader} {contentBody} diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 061f43e73de8..da04fe70fba6 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -16,6 +16,7 @@ import * as Report from '@userActions/Report'; import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import personalDetailsPropType from './personalDetailsPropType'; import reportPropTypes from './reportPropTypes'; @@ -181,6 +182,7 @@ function SearchPage({betas, personalDetails, reports, isSearchingForReports}) { textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} shouldShowReferralCTA referralContentType={CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND} + referralRoute={ROUTES.REFERRAL_DETAILS_MODAL_REFER_FRIEND.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND)} textInputAlert={isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''} onLayout={searchRendered} safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} diff --git a/src/pages/ShareCodePage.js b/src/pages/ShareCodePage.js index 1f062a42f8bf..65c952016b5a 100644 --- a/src/pages/ShareCodePage.js +++ b/src/pages/ShareCodePage.js @@ -121,7 +121,7 @@ class ShareCodePage extends React.Component { Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE))} + onPress={() => Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL_SHARE_CODE.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE))} /> diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 4db9c4ce3fb7..c8d94d247278 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -20,6 +20,7 @@ import personalDetailsPropType from '@pages/personalDetailsPropType'; import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; const propTypes = { /** Beta features list */ @@ -63,6 +64,12 @@ const propTypes = { /** Whether we are searching for reports in the server */ isSearchingForReports: PropTypes.bool, + /** The report ID of the IOU */ + reportID: PropTypes.string.isRequired, + + /** The ID of the transaction being configured */ + transactionID: PropTypes.string.isRequired, + ...withLocalizePropTypes, }; @@ -89,6 +96,8 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ iouType, iouRequestType, isSearchingForReports, + reportID, + transactionID, }) { const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); @@ -321,6 +330,11 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ shouldShowOptions={isOptionsDataReady} shouldShowReferralCTA referralContentType={iouType === 'send' ? CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY : CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST} + referralRoute={ + iouType === 'send' + ? ROUTES.REFERRAL_DETAILS_MODAL_SEND_MONEY.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY) + : ROUTES.REFERRAL_DETAILS_MODAL_REQUEST.getRoute(transactionID, reportID, CONST.REFERRAL_PROGRAM.CONTENT_TYPES.MONEY_REQUEST) + } shouldPreventDefaultFocusOnSelectRow={!Browser.isMobile()} shouldDelayFocus footerContent={isAllowedToSplit && footerContent} diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index ec670b828146..4bb676cab7af 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -95,6 +95,8 @@ function IOURequestStepParticipants({ onParticipantsAdded={addParticipant} onFinish={goToNextStep} iouType={iouType} + reportID={reportID} + transactionID={transactionID} iouRequestType={iouRequestType} /> From b14c40dfd3a5161b4d1828a040ae6c5e57888e45 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Tue, 2 Jan 2024 21:31:37 +0100 Subject: [PATCH 015/635] fix: adding backTo param to ReportParticipantsPage.js --- src/pages/ReportParticipantsPage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index e04ffbb352fc..1e84bd3dcbbb 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -121,7 +121,12 @@ function ReportParticipantsPage(props) { }, ]} onSelectRow={(option) => { - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID)); + Navigation.navigate( + ROUTES.PROFILE.getRoute( + option.accountID, + ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID) + ) + ); }} hideSectionHeaders showTitleTooltip From eb332d633e2af1d0d83a5cde92ae9c4c1f343f20 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 3 Jan 2024 20:05:50 +0100 Subject: [PATCH 016/635] feat: add hold implementation --- assets/images/stopwatch.svg | 3 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/components/Header.tsx | 9 +- src/components/HoldBanner.tsx | 22 +++ src/components/Icon/Expensicons.ts | 2 + src/components/MoneyRequestHeader.js | 29 +++- src/components/TextPill.tsx | 22 +++ src/languages/en.ts | 13 ++ src/languages/es.ts | 13 ++ .../AppNavigator/ModalStackNavigators.tsx | 1 + src/libs/Navigation/linkingConfig.ts | 1 + src/libs/ReportUtils.ts | 135 +++++++++++++++++- src/libs/TransactionUtils.ts | 8 ++ src/libs/actions/IOU.js | 109 ++++++++++++++ src/pages/iou/HoldReasonPage.js | 104 ++++++++++++++ src/types/onyx/Transaction.ts | 1 + 17 files changed, 472 insertions(+), 5 deletions(-) create mode 100644 assets/images/stopwatch.svg create mode 100644 src/components/HoldBanner.tsx create mode 100644 src/components/TextPill.tsx create mode 100644 src/pages/iou/HoldReasonPage.js diff --git a/assets/images/stopwatch.svg b/assets/images/stopwatch.svg new file mode 100644 index 000000000000..d27d6b0b7c36 --- /dev/null +++ b/assets/images/stopwatch.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 49067d1c7b8f..27346d2ca690 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -278,6 +278,10 @@ const ROUTES = { route: ':iouType/new/category/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}` as const, }, + MONEY_REQUEST_HOLD_REASON: { + route: ':iouType/edit/reason/:transactionID?', + getRoute: (iouType: string, transactionID: string, reportID: string, backTo: string) => `${iouType}/edit/reason/${transactionID}?backTo=${backTo}&reportID=${reportID}` as const, + }, MONEY_REQUEST_TAG: { route: ':iouType/new/tag/:reportID?', getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 26a23e7efadc..d945c752a4cc 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -119,6 +119,7 @@ const SCREENS = { SCAN_TAB: 'scan', DISTANCE_TAB: 'distance', CREATE: 'Money_Request_Create', + HOLD: 'Money_Request_Hold_Reason', STEP_CONFIRMATION: 'Money_Request_Step_Confirmation', START: 'Money_Request_Start', STEP_AMOUNT: 'Money_Request_Step_Amount', diff --git a/src/components/Header.tsx b/src/components/Header.tsx index 4eac2c7a6994..8d21659e8fab 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,5 +1,5 @@ import React, {ReactNode} from 'react'; -import {StyleProp, TextStyle, View} from 'react-native'; +import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import EnvironmentBadge from './EnvironmentBadge'; import Text from './Text'; @@ -16,12 +16,15 @@ type HeaderProps = { /** Additional text styles */ textStyles?: StyleProp; + + /** Additional text styles */ + containerStyles?: StyleProp; }; -function Header({title = '', subtitle = '', textStyles = [], shouldShowEnvironmentBadge = false}: HeaderProps) { +function Header({title = '', subtitle = '', textStyles = [], containerStyles = [], shouldShowEnvironmentBadge = false}: HeaderProps) { const styles = useThemeStyles(); return ( - + {typeof title === 'string' ? Boolean(title) && ( diff --git a/src/components/HoldBanner.tsx b/src/components/HoldBanner.tsx new file mode 100644 index 000000000000..af77d9076629 --- /dev/null +++ b/src/components/HoldBanner.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Text from './Text'; +import TextPill from './TextPill'; + +function HoldBanner() { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + + {translate('iou.hold')} + {translate('iou.requestOnHold')} + + ); +} + +HoldBanner.displayName = 'HoldBanner'; + +export default HoldBanner; diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 2ddee8b2939b..d11998ad1111 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -115,6 +115,7 @@ import Linkedin from '@assets/images/social-linkedin.svg'; import Podcast from '@assets/images/social-podcast.svg'; import Twitter from '@assets/images/social-twitter.svg'; import Youtube from '@assets/images/social-youtube.svg'; +import Stopwatch from '@assets/images/stopwatch.svg'; import Sync from '@assets/images/sync.svg'; import Task from '@assets/images/task.svg'; import ThreeDots from '@assets/images/three-dots.svg'; @@ -243,6 +244,7 @@ export { RotateLeft, Send, Shield, + Stopwatch, Sync, Task, ThumbsUp, diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 488630dd0590..a29c43ab4eaa 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -20,6 +20,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; +import HoldBanner from './HoldBanner'; import * as Expensicons from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import participantPropTypes from './participantPropTypes'; @@ -72,10 +73,12 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const moneyRequestReport = parentReport; const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); const isApproved = ReportUtils.isReportApproved(moneyRequestReport); + const isOnHold = TransactionUtils.isOnHold(transaction); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); // Only the requestor can take delete the request, admins can only edit it. const isActionOwner = lodashGet(parentReportAction, 'actorAccountID') === lodashGet(session, 'accountID', null); + const isPolicyAdmin = lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; const deleteTransaction = useCallback(() => { IOU.deleteMoneyRequest(lodashGet(parentReportAction, 'originalMessage.IOUTransactionID'), parentReportAction, true); @@ -87,6 +90,22 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); + const changeMoneyRequestStatus = () => { + if (!isOnHold) { + const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); + Navigation.navigate( + ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute( + lodashGet(policy, 'type'), + lodashGet(parentReportAction, 'originalMessage.IOUTransactionID'), + lodashGet(report, 'reportID'), + activeRoute, + ), + ); + } else { + IOU.unholdRequest(lodashGet(parentReportAction, 'originalMessage.IOUTransactionID'), lodashGet(report, 'reportID')); + } + }; + useEffect(() => { if (canModifyRequest) { return; @@ -95,6 +114,13 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, setIsDeleteModalVisible(false); }, [canModifyRequest]); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)]; + if ((isPolicyAdmin || isActionOwner) && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction)) { + threeDotsMenuItems.push({ + icon: Expensicons.Stopwatch, + text: !isOnHold ? translate('iou.holdRequest') : translate('iou.unholdRequest'), + onSelected: () => changeMoneyRequestStatus(), + }); + } if (canModifyRequest) { if (!TransactionUtils.hasReceipt(transaction)) { threeDotsMenuItems.push({ @@ -114,7 +140,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, <> )} + {isOnHold && } {children}; +} + +TextPill.displayName = 'TextPill'; + +export default TextPill; diff --git a/src/languages/en.ts b/src/languages/en.ts index e223dd0a9aaf..13f31392aa05 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -627,6 +627,19 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Started settling up, payment is held until ${submitterDisplayName} enables their Wallet`, enableWallet: 'Enable Wallet', + hold: 'Hold', + holdRequest: 'Hold Request', + unholdRequest: 'Unhold Request', + explainHold: "Explain why you're holding this request.", + reason: 'Reason', + holdReasonRequired: 'A reason is required when holding.', + requestOnHold: 'This request was put on hold. Review the comments for next steps.', + confirmApprove: 'Confirm what to approve', + confirmApprovalAmount: 'Approve the entire report total or only the amount not on hold.', + confirmPay: 'Confirm what to pay', + confirmPayAmount: 'Pay all out-of-pocket spend or only the amount not on hold.', + payOnly: 'Pay only', + approveOnly: 'Approve only', set: 'set', changed: 'changed', removed: 'removed', diff --git a/src/languages/es.ts b/src/languages/es.ts index 42743f43a098..f04973010145 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -622,6 +622,19 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', + hold: 'En espera', + holdRequest: 'Solicitud de retención', + unholdRequest: 'Solicitud de cancelación de retención', + explainHold: 'Explique por qué mantiene esta solicitud.', + reason: 'Razón', + holdReasonRequired: 'Se requiere una razón al sostener.', + requestOnHold: 'Esta solicitud quedó en suspenso. Revise los comentarios para los próximos pasos.', + confirmApprove: 'Confirmar qué aprobar', + confirmApprovalAmount: 'Aprobar el total del informe completo o solo el monto no retenido.', + confirmPay: 'Confirmar que pagar', + confirmPayAmount: 'Pague todos los gastos de bolsillo o solo el monto no retenido.', + payOnly: 'Paga solo', + approveOnly: 'Aprobar sólo', set: 'estableció', changed: 'cambió', removed: 'eliminó', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 151a795a7e36..231cf2385399 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -95,6 +95,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType, [SCREENS.MONEY_REQUEST.DATE]: () => require('../../../pages/iou/MoneyRequestDatePage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.HOLD]: () => require('../../../pages/iou/HoldReasonPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.DESCRIPTION]: () => require('../../../pages/iou/MoneyRequestDescriptionPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.CATEGORY]: () => require('../../../pages/iou/MoneyRequestCategoryPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.TAG]: () => require('../../../pages/iou/MoneyRequestTagPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index 5f7ecd10cc8c..21743a701f48 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -399,6 +399,7 @@ const linkingConfig: LinkingOptions = { [SCREENS.MONEY_REQUEST.STEP_DATE]: ROUTES.MONEY_REQUEST_STEP_DATE.route, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.route, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: ROUTES.MONEY_REQUEST_STEP_DISTANCE.route, + [SCREENS.MONEY_REQUEST.HOLD]: ROUTES.MONEY_REQUEST_HOLD_REASON.route, [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: ROUTES.MONEY_REQUEST_STEP_MERCHANT.route, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.route, [SCREENS.MONEY_REQUEST.STEP_SCAN]: ROUTES.MONEY_REQUEST_STEP_SCAN.route, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 470d9f3392d3..42920e63e004 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2295,7 +2295,6 @@ function getParentNavigationSubtitle(report: OnyxEntry): ParentNavigatio /** * Navigate to the details page of a given report - * */ function navigateToDetailsPage(report: OnyxEntry) { const participantAccountIDs = report?.participantAccountIDs ?? []; @@ -3094,6 +3093,99 @@ function buildOptimisticCreatedReportAction(emailCreatingAction: string, created }; } +/** + * Returns the necessary reportAction onyx data to indicate that the transaction has been put on hold optimistically + * @param [created] - Action created time + */ +function buildOptimisticHoldReportAction(created = DateUtils.getDBTime()): OptimisticSubmittedReportAction { + return { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + actorAccountID: currentUserAccountID, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'normal', + text: `held this request`, + }, + ], + person: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'normal', + text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, + }, + ], + automatic: true, + avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + created, + shouldShow: true, + }; +} + +/** + * Returns the necessary reportAction onyx data to indicate that the transaction has been removed from hold optimistically + * @param [created] - Action created time + */ +function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): OptimisticSubmittedReportAction { + return { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + actorAccountID: currentUserAccountID, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'normal', + text: `unheld this request`, + }, + ], + person: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'normal', + text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, + }, + ], + automatic: true, + avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + created, + shouldShow: true, + }; +} + +/** + * Returns the necessary reportAction user comment user provided to put on hold optimistically + * @param [created] - Action created time + */ +function buildOptimisticHoldReportActionComment(comment: string, created = DateUtils.getDBTime()): OptimisticSubmittedReportAction { + return { + reportActionID: NumberUtils.rand64(), + actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + actorAccountID: currentUserAccountID, + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'normal', + text: `${comment}`, + }, + ], + person: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'normal', + text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, + }, + ], + automatic: true, + avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), + created, + shouldShow: true, + }; +} + /** * Returns the necessary reportAction onyx data to indicate that a task report has been edited */ @@ -4241,6 +4333,41 @@ function navigateToPrivateNotes(report: Report, session: Session) { Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); } +/** + * Check if Report has any held expenses + */ +function hasHeldExpenses(iouReportID: string): boolean { + const transactions = TransactionUtils.getAllReportTransactions(iouReportID); + return transactions.some((transaction) => TransactionUtils.isOnHold(transaction)); +} + +/** + * Check if all expenses in the Report are on hold + */ +function hasOnlyHeldExpenses(iouReportID: string): boolean { + const transactions = TransactionUtils.getAllReportTransactions(iouReportID); + return !transactions.some((transaction) => !TransactionUtils.isOnHold(transaction)); +} + +/** + * Return held and full amount formatted with used currency + */ +function getNonHeldAndFullAmount(iouReportID: string): string[] { + const transactions = TransactionUtils.getAllReportTransactions(iouReportID); + const usedCurrency = transactions[0].currency; + + let fullAmount = 0; + const nonheldAmount = transactions.reduce((previousValue, transaction) => { + fullAmount += transaction.amount * -1; + if (!TransactionUtils.isOnHold(transaction)) { + return previousValue + transaction.amount * -1; + } + return previousValue; + }, 0); + + return [CurrencyUtils.convertToDisplayString(nonheldAmount, usedCurrency), CurrencyUtils.convertToDisplayString(fullAmount, usedCurrency)]; +} + /** * Disable reply in thread action if: * @@ -4436,8 +4563,14 @@ export { shouldDisableWelcomeMessage, navigateToPrivateNotes, canEditWriteCapability, + hasHeldExpenses, + hasOnlyHeldExpenses, + getNonHeldAndFullAmount, hasSmartscanError, shouldAutoFocusOnKeyPress, + buildOptimisticHoldReportAction, + buildOptimisticHoldReportActionComment, + buildOptimisticUnHoldReportAction, shouldDisableThread, }; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 615bea7ff18d..df53823dba38 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -518,6 +518,13 @@ function getRecentTransactions(transactions: Record, size = 2): .slice(0, size); } +/** + * Check if transaction is on hold + */ +function isOnHold(transaction: Transaction): boolean { + return !!transaction.comment?.hold; +} + /** * this is the formulae to calculate tax */ @@ -568,6 +575,7 @@ export { isCardTransaction, isPending, isPosted, + isOnHold, getWaypoints, isAmountMissing, isMerchantMissing, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 511c299dda54..ef9627600d7a 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -3449,6 +3449,113 @@ function getIOUReportID(iou, route) { return lodashGet(route, 'params.reportID') || lodashGet(iou, 'participants.0.reportID', ''); } +/** + * Put money request on HOLD + * @param {string} transactionID + * @param {string} comment + * @param {string} reportID + */ +function putOnHold(transactionID, comment, reportID) { + const createdDate = new Date(); + const createdReportAction = ReportUtils.buildOptimisticHoldReportAction(createdDate); + const createdCommentReportAction = ReportUtils.buildOptimisticHoldReportActionComment(comment, new Date(createdDate.getTime() + 1)); + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const transactionDetails = ReportUtils.getTransactionDetails(transaction); + + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [createdReportAction.reportActionID]: createdReportAction, + [createdCommentReportAction.reportActionID]: createdCommentReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + comment: { + hold: createdReportAction.reportActionID, + }, + }, + }, + ]; + + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + comment: { + hold: null, + }, + }, + }, + ]; + + API.write( + 'HoldRequest', + { + ...transactionDetails, + transactionID, + comment, + }, + {optimisticData, successData: [], failureData}, + ); +} + +/** + * Remove money request from HOLD + * @param {string} transactionID + * @param {string} reportID + */ +function unholdRequest(transactionID, reportID) { + const createdReportAction = ReportUtils.buildOptimisticUnHoldReportAction(); + const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; + const transactionDetails = ReportUtils.getTransactionDetails(transaction); + + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [createdReportAction.reportActionID]: createdReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + comment: { + hold: null, + }, + }, + }, + ]; + + const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, + value: { + comment: { + hold: null, + }, + }, + }, + ]; + + API.write( + 'UnHoldRequest', + { + ...transactionDetails, + transactionID, + }, + {optimisticData, successData, failureData: []}, + ); +} + export { setMoneyRequestParticipants, createDistanceRequest, @@ -3503,5 +3610,7 @@ export { detachReceipt, getIOUReportID, editMoneyRequest, + putOnHold, + unholdRequest, resetMoneyRequestAmount_temporaryForRefactor, }; diff --git a/src/pages/iou/HoldReasonPage.js b/src/pages/iou/HoldReasonPage.js new file mode 100644 index 000000000000..777d35ca2002 --- /dev/null +++ b/src/pages/iou/HoldReasonPage.js @@ -0,0 +1,104 @@ +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; +import React, {useCallback, useRef} from 'react'; +import {View} from 'react-native'; +import _ from 'underscore'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import * as IOU from '@userActions/IOU'; +import CONST from '@src/CONST'; + +const propTypes = { + /** Navigation route context info provided by react navigation */ + route: PropTypes.shape({ + /** Route specific parameters used on this screen via route :iouType/new/category/:reportID? */ + params: PropTypes.shape({ + /** ID of the transaction the page was opened for */ + transactionID: PropTypes.string, + + /** ID of the report that user is providing hold reason to */ + reportID: PropTypes.string, + + /** Link to previous page */ + backTo: PropTypes.string, + }), + }).isRequired, +}; + +function HoldReasonPage({route}) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const reasonRef = useRef(); + + const transactionID = lodashGet(route, 'params.transactionID', ''); + const reportID = lodashGet(route, 'params.reportID', ''); + const backTo = lodashGet(route, 'params.backTo', ''); + + const navigateBack = () => { + Navigation.navigate(backTo); + }; + + const onSubmit = (values) => { + IOU.putOnHold(transactionID, values.comment, reportID); + navigateBack(); + }; + + const validate = useCallback((value) => { + const errors = {}; + + if (_.isEmpty(value.comment)) { + errors.comment = 'common.error.fieldRequired'; + } + + return errors; + }, []); + + return ( + + + + {translate('iou.explainHold')} + + (reasonRef.current = e)} + autoFocus + /> + + + + ); +} + +HoldReasonPage.displayName = 'MoneyRequestHoldReasonPage'; +HoldReasonPage.propTypes = propTypes; + +export default HoldReasonPage; diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 53bfc36a4e47..dee799499113 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -20,6 +20,7 @@ type Waypoint = { type WaypointCollection = Record; type Comment = { comment?: string; + hold?: string; waypoints?: WaypointCollection; isLoading?: boolean; type?: string; From c3268eff35a8c6af361ae2e1d0b6795431427219 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Wed, 3 Jan 2024 14:18:38 -0500 Subject: [PATCH 017/635] feat(Violations): add transaction violation message to MoneyRequestPreview. --- .../ReportActionItem/MoneyRequestPreview.js | 17 ++++++++++++++++- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/TransactionUtils.ts | 5 +++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index f0e818ddff4d..71b306043ecc 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -28,6 +28,7 @@ import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import {transactionViolationsPropType} from '@libs/Violations/propTypes'; import walletTermsPropTypes from '@pages/EnablePayments/walletTermsPropTypes'; import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import iouReportPropTypes from '@pages/iouReportPropTypes'; @@ -104,6 +105,9 @@ const propTypes = { /** Whether a message is a whisper */ isWhisper: PropTypes.bool, + + /** All transactionViolations */ + transactionViolations: transactionViolationsPropType, }; const defaultProps = { @@ -123,6 +127,7 @@ const defaultProps = { transaction: {}, shouldShowPendingConversionMessage: false, isWhisper: false, + transactionViolations: {}, }; function MoneyRequestPreview(props) { @@ -155,7 +160,8 @@ function MoneyRequestPreview(props) { const description = requestComment; const hasReceipt = TransactionUtils.hasReceipt(props.transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(props.transaction); - const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(props.transaction); + const hasViolations = TransactionUtils.hasViolation(props.transaction.transactionId, props.transactionViolations); + const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(props.transaction) || hasViolations; const isDistanceRequest = TransactionUtils.isDistanceRequest(props.transaction); const isExpensifyCardTransaction = TransactionUtils.isExpensifyCardTransaction(props.transaction); const isSettled = ReportUtils.isSettled(props.iouReport.reportID); @@ -208,6 +214,12 @@ function MoneyRequestPreview(props) { } let message = translate('iou.cash'); + if (hasViolations) { + const violations = TransactionUtils.getTransactionViolations(props.transaction.transactionId, props.transactionViolations); + const firstViolationName = translate(`violations.${violations[0].name}`); + const isTooLong = violations.length > 1 || firstViolationName.length > 15; + message += ` • ${isTooLong ? translate('violations.reviewRequired') : firstViolationName}`; + } if (ReportUtils.isGroupPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) { message += ` • ${translate('iou.approved')}`; } else if (props.iouReport.isWaitingOnBankAccount) { @@ -390,4 +402,7 @@ export default withOnyx({ walletTerms: { key: ONYXKEYS.WALLET_TERMS, }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, })(MoneyRequestPreview); diff --git a/src/languages/en.ts b/src/languages/en.ts index 6e177c1df141..654454939a8b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2074,6 +2074,7 @@ export default { perDayLimit: ({limit}: ViolationsPerDayLimitParams) => `Amount over daily ${limit}/person category limit`, receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.', receiptRequired: ({amount, category}: ViolationsReceiptRequiredParams) => `Receipt required over ${amount} ${category ? ' category limit' : ''}`, + reviewRequired: 'dummy.violations.reviewRequired.EN', rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin diff --git a/src/languages/es.ts b/src/languages/es.ts index 990554b0b502..cce9b6a98eca 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2561,6 +2561,7 @@ export default { perDayLimit: ({limit}: ViolationsPerDayLimitParams) => `Importe supera el límite diario de la categoría de ${limit}/persona`, receiptNotSmartScanned: 'Recibo no verificado. Por favor, confirma su exactitud', receiptRequired: ({amount, category}: ViolationsReceiptRequiredParams) => `Recibo obligatorio para importes sobre ${category ? 'el limite de la categoría de ' : ''}${amount}`, + reviewRequired: 'dummy.violations.reviewRequired>ES', rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 5cb962b27cdc..d669cc0d917d 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -526,6 +526,10 @@ function hasViolation(transactionID: string, transactionViolations: TransactionV return Boolean(transactionViolations[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'violation')); } +function getTransactionViolations(transactionID: string, transactionViolations: TransactionViolations): TransactionViolation[] | null { + return (transactionViolations[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] as TransactionViolation[]) ?? null; +} + /** * this is the formulae to calculate tax */ @@ -564,6 +568,7 @@ export { getCategory, getBillable, getTag, + getTransactionViolations, getLinkedTransaction, getAllReportTransactions, hasReceipt, From 54006d9ef2e37eb2cbfbf1c1eb4cfbcaf61714f1 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Wed, 3 Jan 2024 16:59:57 -0500 Subject: [PATCH 018/635] feat(Violations): fix broken import --- src/components/ViolationMessages.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ViolationMessages.tsx b/src/components/ViolationMessages.tsx index 310c2deafee5..41ad44a54381 100644 --- a/src/components/ViolationMessages.tsx +++ b/src/components/ViolationMessages.tsx @@ -2,7 +2,7 @@ import React, {useMemo} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import ViolationsUtils from '@libs/ViolationsUtils'; +import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import {TransactionViolation} from '@src/types/onyx'; import Text from './Text'; From 92f340a97955f06f3b73a7e55e059566215ad794 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Wed, 3 Jan 2024 17:00:54 -0500 Subject: [PATCH 019/635] feat(Violations): refactor for consistency and readability --- src/components/ReportActionItem/MoneyRequestPreview.js | 10 +++++----- src/libs/TransactionUtils.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 71b306043ecc..4a79fc5f6232 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -160,7 +160,7 @@ function MoneyRequestPreview(props) { const description = requestComment; const hasReceipt = TransactionUtils.hasReceipt(props.transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(props.transaction); - const hasViolations = TransactionUtils.hasViolation(props.transaction.transactionId, props.transactionViolations); + const hasViolations = TransactionUtils.hasViolation(props.transaction, props.transactionViolations); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(props.transaction) || hasViolations; const isDistanceRequest = TransactionUtils.isDistanceRequest(props.transaction); const isExpensifyCardTransaction = TransactionUtils.isExpensifyCardTransaction(props.transaction); @@ -215,10 +215,10 @@ function MoneyRequestPreview(props) { let message = translate('iou.cash'); if (hasViolations) { - const violations = TransactionUtils.getTransactionViolations(props.transaction.transactionId, props.transactionViolations); - const firstViolationName = translate(`violations.${violations[0].name}`); - const isTooLong = violations.length > 1 || firstViolationName.length > 15; - message += ` • ${isTooLong ? translate('violations.reviewRequired') : firstViolationName}`; + const violations = TransactionUtils.getTransactionViolations(props.transaction, props.transactionViolations); + const violation = translate(`violations.${violations[0]?.name}`, violations[0]?.data); + const isTooLong = violations?.length > 1 || violation.length > 15; + message += ` • ${isTooLong ? translate('violations.reviewRequired') : violation}`; } if (ReportUtils.isGroupPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) { message += ` • ${translate('iou.approved')}`; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index d669cc0d917d..63f831b81706 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -522,11 +522,11 @@ function getRecentTransactions(transactions: Record, size = 2): /** * Checks if any violations for the provided transaction are of type 'violation' */ -function hasViolation(transactionID: string, transactionViolations: TransactionViolations): boolean { - return Boolean(transactionViolations[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'violation')); +function hasViolation(transaction: Transaction, transactionViolations: TransactionViolations): boolean { + return Boolean(transactionViolations[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transaction.transactionID]?.some((violation: TransactionViolation) => violation.type === 'violation')); } -function getTransactionViolations(transactionID: string, transactionViolations: TransactionViolations): TransactionViolation[] | null { +function getTransactionViolations({transactionID}: Transaction, transactionViolations: TransactionViolations): TransactionViolation[] | null { return (transactionViolations[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] as TransactionViolation[]) ?? null; } From 48c8083a6cf09c95e2e45f9667fb8a76a58bae8b Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Wed, 3 Jan 2024 17:03:48 -0500 Subject: [PATCH 020/635] feat(Violations): make dummy string < 15 chars> --- src/languages/en.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 654454939a8b..08f38fb58e3b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2074,7 +2074,7 @@ export default { perDayLimit: ({limit}: ViolationsPerDayLimitParams) => `Amount over daily ${limit}/person category limit`, receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.', receiptRequired: ({amount, category}: ViolationsReceiptRequiredParams) => `Receipt required over ${amount} ${category ? ' category limit' : ''}`, - reviewRequired: 'dummy.violations.reviewRequired.EN', + reviewRequired: 'XreviewRequired', rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin From 2b51f6a68b802890c463b140b2ec3e5aaf99bcd4 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Wed, 3 Jan 2024 17:09:26 -0500 Subject: [PATCH 021/635] fix lint --- src/components/ReportActionItem/MoneyRequestPreview.js | 4 ++-- src/libs/Violations/ViolationsUtils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 4a79fc5f6232..5456f29f9109 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -216,8 +216,8 @@ function MoneyRequestPreview(props) { let message = translate('iou.cash'); if (hasViolations) { const violations = TransactionUtils.getTransactionViolations(props.transaction, props.transactionViolations); - const violation = translate(`violations.${violations[0]?.name}`, violations[0]?.data); - const isTooLong = violations?.length > 1 || violation.length > 15; + const violation = translate(`violations.${violations[0].name}`, violations[0].data); + const isTooLong = violations.length > 1 || violation.length > 15; message += ` • ${isTooLong ? translate('violations.reviewRequired') : violation}`; } if (ReportUtils.isGroupPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) { diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index 09c97ff1133c..0dd467220352 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -1,9 +1,9 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; +import {Phrase, PhraseParameters} from '@libs/Localize'; import {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import {PolicyCategories, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx'; -import {Phrase, PhraseParameters} from './Localize'; const ViolationsUtils = { /** From 062a878ea38a88e8151c83cf8f74aede1459243e Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Thu, 4 Jan 2024 17:09:42 -0500 Subject: [PATCH 022/635] feat(Violations): add translation --- src/languages/en.ts | 2 +- src/languages/es.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 08f38fb58e3b..2e1dbacb5bc3 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2074,7 +2074,7 @@ export default { perDayLimit: ({limit}: ViolationsPerDayLimitParams) => `Amount over daily ${limit}/person category limit`, receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.', receiptRequired: ({amount, category}: ViolationsReceiptRequiredParams) => `Receipt required over ${amount} ${category ? ' category limit' : ''}`, - reviewRequired: 'XreviewRequired', + reviewRequired: 'Review required', rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin diff --git a/src/languages/es.ts b/src/languages/es.ts index cce9b6a98eca..e264595ad978 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2561,7 +2561,7 @@ export default { perDayLimit: ({limit}: ViolationsPerDayLimitParams) => `Importe supera el límite diario de la categoría de ${limit}/persona`, receiptNotSmartScanned: 'Recibo no verificado. Por favor, confirma su exactitud', receiptRequired: ({amount, category}: ViolationsReceiptRequiredParams) => `Recibo obligatorio para importes sobre ${category ? 'el limite de la categoría de ' : ''}${amount}`, - reviewRequired: 'dummy.violations.reviewRequired>ES', + reviewRequired: 'Revisión requerida', rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin From 62ee8237a09da6db21568b37cb26883f2036d7ce Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 8 Jan 2024 21:45:34 +0100 Subject: [PATCH 023/635] feat: add proper permissions & style system messages --- src/CONST.ts | 2 + src/components/MoneyRequestHeader.js | 33 +++++++--- src/components/TextPill.tsx | 2 +- src/libs/ReportUtils.ts | 61 +++++++------------ src/libs/actions/IOU.js | 23 +++++-- .../home/report/ReportActionItemFragment.js | 35 ++++++++--- .../home/report/ReportActionItemMessage.tsx | 3 + .../home/report/ReportActionItemSingle.tsx | 1 + .../report/comment/TextCommentFragment.js | 2 +- src/types/onyx/OriginalMessage.ts | 14 +++++ 10 files changed, 115 insertions(+), 61 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 0c98645511d4..70899bc1eaa9 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -511,6 +511,7 @@ const CONST = { CHRONOSOOOLIST: 'CHRONOSOOOLIST', CLOSED: 'CLOSED', CREATED: 'CREATED', + HOLD: 'HOLD', IOU: 'IOU', MARKEDREIMBURSED: 'MARKEDREIMBURSED', MODIFIEDEXPENSE: 'MODIFIEDEXPENSE', @@ -594,6 +595,7 @@ const CONST = { REMOVE_FROM_ROOM: 'REMOVEFROMROOM', JOIN_ROOM: 'JOINROOM', }, + UNHOLD: 'UNHOLD', }, THREAD_DISABLED: ['CREATED'], }, diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index a29c43ab4eaa..048364596e86 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -76,6 +76,13 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isOnHold = TransactionUtils.isOnHold(transaction); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); + console.log('report: ', report); + console.log('parentReport: ', parentReport); + console.log('parentReportAction: ', parentReportAction); + console.log('transaction: ', transaction); + console.log("lodashGet(policy, 'role'): ", lodashGet(policy, 'role')); + console.log("lodashGet(policy, 'type'): ", lodashGet(policy, 'type')); + // Only the requestor can take delete the request, admins can only edit it. const isActionOwner = lodashGet(parentReportAction, 'actorAccountID') === lodashGet(session, 'accountID', null); const isPolicyAdmin = lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; @@ -88,7 +95,8 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); const isPending = TransactionUtils.isExpensifyCardTransaction(transaction) && TransactionUtils.isPending(transaction); - const canModifyRequest = isActionOwner && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); + const isRequestModifiable = !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction); + const canModifyRequest = isActionOwner && isRequestModifiable; const changeMoneyRequestStatus = () => { if (!isOnHold) { @@ -113,14 +121,25 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, setIsDeleteModalVisible(false); }, [canModifyRequest]); + const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)]; - if ((isPolicyAdmin || isActionOwner) && !isSettled && !isApproved && !ReportActionsUtils.isDeletedAction(parentReportAction)) { - threeDotsMenuItems.push({ - icon: Expensicons.Stopwatch, - text: !isOnHold ? translate('iou.holdRequest') : translate('iou.unholdRequest'), - onSelected: () => changeMoneyRequestStatus(), - }); + if (isRequestModifiable) { + if (isOnHold && (ReportUtils.isHoldCreator(transaction, lodashGet(report, 'reportID')) || isPolicyAdmin)) { + threeDotsMenuItems.push({ + icon: Expensicons.Stopwatch, + text: translate('iou.unholdRequest'), + onSelected: () => changeMoneyRequestStatus(), + }); + } + if (!isOnHold && (lodashGet(parentReport, 'type') === 'iou' || isPolicyAdmin || isActionOwner)) { + threeDotsMenuItems.push({ + icon: Expensicons.Stopwatch, + text: translate('iou.holdRequest'), + onSelected: () => changeMoneyRequestStatus(), + }); + } } + if (canModifyRequest) { if (!TransactionUtils.hasReceipt(transaction)) { threeDotsMenuItems.push({ diff --git a/src/components/TextPill.tsx b/src/components/TextPill.tsx index 1104b5829d95..67dd8bf56ddc 100644 --- a/src/components/TextPill.tsx +++ b/src/components/TextPill.tsx @@ -14,7 +14,7 @@ type TextPillProps = { function TextPill({color, children}: TextPillProps) { const styles = useThemeStyles(); - return {children}; + return {children}; } TextPill.displayName = 'TextPill'; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 42920e63e004..96ff9a71d1c0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3097,58 +3097,31 @@ function buildOptimisticCreatedReportAction(emailCreatingAction: string, created * Returns the necessary reportAction onyx data to indicate that the transaction has been put on hold optimistically * @param [created] - Action created time */ -function buildOptimisticHoldReportAction(created = DateUtils.getDBTime()): OptimisticSubmittedReportAction { +function buildOptimisticHoldReportAction(comment: string, created = DateUtils.getDBTime()): OptimisticSubmittedReportAction { return { reportActionID: NumberUtils.rand64(), - actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + actionName: CONST.REPORT.ACTIONS.TYPE.HOLD, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, actorAccountID: currentUserAccountID, message: [ { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: `held this request`, + text: `held this money request with the comment: ${comment}`, }, - ], - person: [ { - type: CONST.REPORT.MESSAGE.TYPE.TEXT, - style: 'normal', - text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, - }, - ], - automatic: true, - avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), - created, - shouldShow: true, - }; -} - -/** - * Returns the necessary reportAction onyx data to indicate that the transaction has been removed from hold optimistically - * @param [created] - Action created time - */ -function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): OptimisticSubmittedReportAction { - return { - reportActionID: NumberUtils.rand64(), - actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - actorAccountID: currentUserAccountID, - message: [ - { - type: CONST.REPORT.MESSAGE.TYPE.TEXT, - style: 'normal', - text: `unheld this request`, + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + text: comment, }, ], person: [ { type: CONST.REPORT.MESSAGE.TYPE.TEXT, - style: 'normal', + style: 'strong', text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, }, ], - automatic: true, + automatic: false, avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), created, shouldShow: true, @@ -3156,20 +3129,20 @@ function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): Opt } /** - * Returns the necessary reportAction user comment user provided to put on hold optimistically + * Returns the necessary reportAction onyx data to indicate that the transaction has been removed from hold optimistically * @param [created] - Action created time */ -function buildOptimisticHoldReportActionComment(comment: string, created = DateUtils.getDBTime()): OptimisticSubmittedReportAction { +function buildOptimisticUnHoldReportAction(created = DateUtils.getDBTime()): OptimisticSubmittedReportAction { return { reportActionID: NumberUtils.rand64(), - actionName: CONST.REPORT.ACTIONS.TYPE.SUBMITTED, + actionName: CONST.REPORT.ACTIONS.TYPE.UNHOLD, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, actorAccountID: currentUserAccountID, message: [ { type: CONST.REPORT.MESSAGE.TYPE.TEXT, style: 'normal', - text: `${comment}`, + text: `unheld this money request`, }, ], person: [ @@ -3179,7 +3152,7 @@ function buildOptimisticHoldReportActionComment(comment: string, created = DateU text: allPersonalDetails?.[currentUserAccountID ?? '']?.displayName ?? currentUserEmail, }, ], - automatic: true, + automatic: false, avatar: allPersonalDetails?.[currentUserAccountID ?? '']?.avatar ?? UserUtils.getDefaultAvatarURL(currentUserAccountID), created, shouldShow: true, @@ -4333,6 +4306,14 @@ function navigateToPrivateNotes(report: Report, session: Session) { Navigation.navigate(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); } +/** + * Check if Report has any held expenses + */ +function isHoldCreator(transaction: Transaction, reportID: string): boolean { + const holdReportAction = ReportActionsUtils.getReportAction(reportID, `${transaction.comment?.hold}`); + return isActionCreator(holdReportAction); +} + /** * Check if Report has any held expenses */ @@ -4563,13 +4544,13 @@ export { shouldDisableWelcomeMessage, navigateToPrivateNotes, canEditWriteCapability, + isHoldCreator, hasHeldExpenses, hasOnlyHeldExpenses, getNonHeldAndFullAmount, hasSmartscanError, shouldAutoFocusOnKeyPress, buildOptimisticHoldReportAction, - buildOptimisticHoldReportActionComment, buildOptimisticUnHoldReportAction, shouldDisableThread, }; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ef9627600d7a..f2e2553615ef 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -3457,8 +3457,7 @@ function getIOUReportID(iou, route) { */ function putOnHold(transactionID, comment, reportID) { const createdDate = new Date(); - const createdReportAction = ReportUtils.buildOptimisticHoldReportAction(createdDate); - const createdCommentReportAction = ReportUtils.buildOptimisticHoldReportActionComment(comment, new Date(createdDate.getTime() + 1)); + const createdReportAction = ReportUtils.buildOptimisticHoldReportAction(comment, DateUtils.getDBTime(createdDate)); const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`]; const transactionDetails = ReportUtils.getTransactionDetails(transaction); @@ -3468,7 +3467,6 @@ function putOnHold(transactionID, comment, reportID) { key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, value: { [createdReportAction.reportActionID]: createdReportAction, - [createdCommentReportAction.reportActionID]: createdCommentReportAction, }, }, { @@ -3482,6 +3480,16 @@ function putOnHold(transactionID, comment, reportID) { }, ]; + const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [createdReportAction.reportActionID]: null, + }, + }, + ]; + const failureData = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -3501,7 +3509,7 @@ function putOnHold(transactionID, comment, reportID) { transactionID, comment, }, - {optimisticData, successData: [], failureData}, + {optimisticData, successData, failureData}, ); } @@ -3535,6 +3543,13 @@ function unholdRequest(transactionID, reportID) { ]; const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [createdReportAction.reportActionID]: null, + }, + }, { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js index f05b3decc6d7..92ed637cbe62 100644 --- a/src/pages/home/report/ReportActionItemFragment.js +++ b/src/pages/home/report/ReportActionItemFragment.js @@ -65,6 +65,9 @@ const propTypes = { /** Whether the report action type is 'APPROVED' or 'SUBMITTED'. Used to style system messages from Old Dot */ isApprovedOrSubmittedReportAction: PropTypes.bool, + /** Whether the report action type is 'UNHOLD' or 'HOLD'. Used to style messages related to hold requests */ + isHoldReportAction: PropTypes.bool, + /** Used to format RTL display names in Old Dot system messages e.g. Arabic */ isFragmentContainingDisplayName: PropTypes.bool, @@ -89,6 +92,7 @@ const defaultProps = { actorIcon: {}, isThreadParentMessage: false, isApprovedOrSubmittedReportAction: false, + isHoldReportAction: false, isFragmentContainingDisplayName: false, displayAsGroup: false, }; @@ -131,14 +135,29 @@ function ReportActionItemFragment(props) { ); } case 'TEXT': { - return props.isApprovedOrSubmittedReportAction ? ( - - {props.isFragmentContainingDisplayName ? convertToLTR(props.fragment.text) : props.fragment.text} - - ) : ( + if (props.isApprovedOrSubmittedReportAction) { + return ( + + {props.isFragmentContainingDisplayName ? convertToLTR(props.fragment.text) : props.fragment.text} + + ); + } + + if (props.isHoldReportAction) { + return ( + + {props.isFragmentContainingDisplayName ? convertToLTR(props.fragment.text) : props.fragment.text} + + ); + } + + return ( type === action.actionName); + const isHoldReportAction = [CONST.REPORT.ACTIONS.TYPE.HOLD, CONST.REPORT.ACTIONS.TYPE.UNHOLD].some((type) => type === action.actionName); + /** * Get the ReportActionItemFragments * @param shouldWrapInText determines whether the fragments are wrapped in a Text component @@ -79,6 +81,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid style={style} displayAsGroup={displayAsGroup} isApprovedOrSubmittedReportAction={isApprovedOrSubmittedReportAction} + isHoldReportAction={isHoldReportAction} // Since system messages from Old Dot begin with the person who performed the action, // the first fragment will contain the person's display name and their email. We'll use this // to decide if the fragment should be from left to right for RTL display names e.g. Arabic for proper diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 43b5630b2685..68a6ef96ae82 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -78,6 +78,7 @@ function ReportActionItemSingle({ const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID; let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID); + console.log('actionTest', action, displayName); const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); diff --git a/src/pages/home/report/comment/TextCommentFragment.js b/src/pages/home/report/comment/TextCommentFragment.js index 3d6482344450..2c05be903dcb 100644 --- a/src/pages/home/report/comment/TextCommentFragment.js +++ b/src/pages/home/report/comment/TextCommentFragment.js @@ -50,7 +50,7 @@ function TextCommentFragment(props) { const theme = useTheme(); const styles = useThemeStyles(); const {fragment, styleAsDeleted} = props; - const {html, text} = fragment; + const {html = '', text} = fragment; // If the only difference between fragment.text and fragment.html is
tags // we render it as text, not as html. diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 767f724dd571..6c9a1ad8280d 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -9,6 +9,8 @@ type OriginalMessageActionName = | 'CHRONOSOOOLIST' | 'CLOSED' | 'CREATED' + | 'HOLD' + | 'UNHOLD' | 'IOU' | 'MODIFIEDEXPENSE' | 'REIMBURSEMENTQUEUED' @@ -26,6 +28,16 @@ type OriginalMessageApproved = { }; type OriginalMessageSource = 'Chronos' | 'email' | 'ios' | 'android' | 'web' | ''; +type OriginalMessageHold = { + actionName: typeof CONST.REPORT.ACTIONS.TYPE.HOLD; + originalMessage: unknown; +}; + +type OriginalMessageUnHold = { + actionName: typeof CONST.REPORT.ACTIONS.TYPE.UNHOLD; + originalMessage: unknown; +}; + type IOUDetails = { amount: number; comment?: string; @@ -242,6 +254,8 @@ type OriginalMessage = | OriginalMessageSubmitted | OriginalMessageClosed | OriginalMessageCreated + | OriginalMessageHold + | OriginalMessageUnHold | OriginalMessageRenamed | OriginalMessageChronosOOOList | OriginalMessageReportPreview From 98c83dacb4cf632f62fb3387c611f6d228445c17 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 8 Jan 2024 21:47:14 +0100 Subject: [PATCH 024/635] fix: remove console log --- src/components/MoneyRequestHeader.js | 7 ------- src/pages/home/report/ReportActionItemSingle.tsx | 1 - 2 files changed, 8 deletions(-) diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 048364596e86..7c64eb54fcf7 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -76,13 +76,6 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const isOnHold = TransactionUtils.isOnHold(transaction); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); - console.log('report: ', report); - console.log('parentReport: ', parentReport); - console.log('parentReportAction: ', parentReportAction); - console.log('transaction: ', transaction); - console.log("lodashGet(policy, 'role'): ", lodashGet(policy, 'role')); - console.log("lodashGet(policy, 'type'): ", lodashGet(policy, 'type')); - // Only the requestor can take delete the request, admins can only edit it. const isActionOwner = lodashGet(parentReportAction, 'actorAccountID') === lodashGet(session, 'accountID', null); const isPolicyAdmin = lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; diff --git a/src/pages/home/report/ReportActionItemSingle.tsx b/src/pages/home/report/ReportActionItemSingle.tsx index 68a6ef96ae82..43b5630b2685 100644 --- a/src/pages/home/report/ReportActionItemSingle.tsx +++ b/src/pages/home/report/ReportActionItemSingle.tsx @@ -78,7 +78,6 @@ function ReportActionItemSingle({ const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const actorAccountID = action.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && iouReport ? iouReport.managerID : action.actorAccountID; let displayName = ReportUtils.getDisplayNameForParticipant(actorAccountID); - console.log('actionTest', action, displayName); const {avatar, login, pendingFields, status, fallbackIcon} = personalDetails[actorAccountID ?? -1] ?? {}; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing let actorHint = (login || (displayName ?? '')).replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); From ba91d6e7ed66d225d3ffa2880956386d50dd9dff Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Mon, 8 Jan 2024 21:59:26 +0100 Subject: [PATCH 025/635] fix: typecheck --- src/components/TextPill.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/TextPill.tsx b/src/components/TextPill.tsx index 7f3ad73fa0b5..73a376772ee3 100644 --- a/src/components/TextPill.tsx +++ b/src/components/TextPill.tsx @@ -10,7 +10,7 @@ type TextPillProps = { color?: string; /** Styles to apply to the text */ - textStyles: StyleProp; + textStyles?: StyleProp; children: React.ReactNode; }; From ef26aaa1d61d8b25ae43ed2deb9cbc0c589d74b5 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 9 Jan 2024 17:49:16 +0530 Subject: [PATCH 026/635] test changes --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- src/components/ReportActionItem/MoneyRequestView.js | 2 +- src/libs/TransactionUtils.ts | 7 +++++-- src/libs/actions/IOU.js | 4 +++- src/types/onyx/Transaction.ts | 6 ++++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 0663441c3834..9bba3aed205a 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -38,8 +38,8 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; -import ReportActionItemImages from './ReportActionItemImages'; import ConfirmedRoute from '@components/ConfirmedRoute'; +import ReportActionItemImages from './ReportActionItemImages'; const propTypes = { /** The active IOUReport, used for Onyx subscription */ diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index eb658a7f8a1b..fc70b47d7101 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -42,8 +42,8 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import ReportActionItemImage from './ReportActionItemImage'; import ConfirmedRoute from '@components/ConfirmedRoute'; +import ReportActionItemImage from './ReportActionItemImage'; const violationNames = lodashValues(CONST.VIOLATIONS); diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index c34a6753c1d5..b8fad803d957 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -1,5 +1,6 @@ import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; @@ -7,8 +8,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types/onyx'; import type {PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type PolicyTaxRate from '@src/types/onyx/PolicyTaxRates'; -import type {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import type {Comment, PendingFieldsCollection, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; +import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; @@ -98,6 +99,7 @@ function buildOptimisticTransaction( category = '', tag = '', billable = false, + pendingFields: PendingFieldsCollection | null = null, ): Transaction { // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) @@ -112,6 +114,7 @@ function buildOptimisticTransaction( } return { + ...(isNotEmptyObject(pendingFields) ? {pendingFields} : {}), transactionID, amount, currency, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 4e1a45101732..f4ff83e995c8 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -721,6 +721,8 @@ function getMoneyRequestInformation( receiptObject.state = receipt.state || CONST.IOU.RECEIPT_STATE.SCANREADY; filename = receipt.name; } + const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( ReportUtils.isExpenseReport(iouReport) ? -amount : amount, currency, @@ -736,6 +738,7 @@ function getMoneyRequestInformation( category, tag, billable, + isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : null, ); const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); @@ -747,7 +750,6 @@ function getMoneyRequestInformation( // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 // to remind me to do this. - const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; if (existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { optimisticTransaction = OnyxUtils.fastMerge(existingTransaction, optimisticTransaction); // pendingFields: { diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 8b7e26280305..a6b65f7cba38 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -49,6 +49,8 @@ type Route = { type Routes = Record; +type PendingFieldsCollection = Partial<{[K in keyof Transaction | keyof Comment]: ValueOf}>; + type Transaction = { amount: number; billable: boolean; @@ -76,7 +78,7 @@ type Transaction = { routes?: Routes; transactionID: string; tag: string; - pendingFields?: Partial<{[K in keyof Transaction | keyof Comment]: ValueOf}>; + pendingFields?: PendingFieldsCollection; /** Card Transactions */ @@ -97,4 +99,4 @@ type Transaction = { }; export default Transaction; -export type {WaypointCollection, Comment, Receipt, Waypoint}; +export type {WaypointCollection, Comment, Receipt, Waypoint, PendingFieldsCollection}; From 54a63d643b9ad602ace915090c3c18cae28af5b0 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 9 Jan 2024 18:07:40 +0530 Subject: [PATCH 027/635] fix lint --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- src/components/ReportActionItem/MoneyRequestView.js | 2 +- src/libs/TransactionUtils.ts | 2 +- src/libs/actions/IOU.js | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 9bba3aed205a..84ed317be294 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -5,6 +5,7 @@ import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import ConfirmedRoute from '@components/ConfirmedRoute'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestSkeletonView from '@components/MoneyRequestSkeletonView'; @@ -38,7 +39,6 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; -import ConfirmedRoute from '@components/ConfirmedRoute'; import ReportActionItemImages from './ReportActionItemImages'; const propTypes = { diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index fc70b47d7101..04b8d35e3448 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -5,6 +5,7 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import categoryPropTypes from '@components/categoryPropTypes'; +import ConfirmedRoute from '@components/ConfirmedRoute'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -42,7 +43,6 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import ConfirmedRoute from '@components/ConfirmedRoute'; import ReportActionItemImage from './ReportActionItemImage'; const violationNames = lodashValues(CONST.VIOLATIONS); diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index b8fad803d957..4155fe426f5a 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -1,6 +1,5 @@ import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; @@ -9,6 +8,7 @@ import type {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types import type {PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type PolicyTaxRate from '@src/types/onyx/PolicyTaxRates'; import type {Comment, PendingFieldsCollection, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f4ff83e995c8..f004b44feca2 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -722,7 +722,7 @@ function getMoneyRequestInformation( filename = receipt.name; } const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; - const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( ReportUtils.isExpenseReport(iouReport) ? -amount : amount, currency, @@ -752,9 +752,9 @@ function getMoneyRequestInformation( // to remind me to do this. if (existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { optimisticTransaction = OnyxUtils.fastMerge(existingTransaction, optimisticTransaction); -// pendingFields: { -// waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, -// } + // pendingFields: { + // waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + // } } // STEP 4: Build optimistic reportActions. We need: From 46141e379c495127f8287f3ca15a771c4eba63df Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 9 Jan 2024 18:09:37 +0530 Subject: [PATCH 028/635] fix lint --- src/libs/actions/IOU.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f004b44feca2..d68b33e6b5ef 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -750,11 +750,8 @@ function getMoneyRequestInformation( // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 // to remind me to do this. - if (existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { + if (isDistanceRequest) { optimisticTransaction = OnyxUtils.fastMerge(existingTransaction, optimisticTransaction); - // pendingFields: { - // waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - // } } // STEP 4: Build optimistic reportActions. We need: From 3ea41321b126be3b4005a58d539ef79c3bad2da7 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Tue, 9 Jan 2024 15:04:03 -0500 Subject: [PATCH 029/635] feat(Violations): fix import --- src/libs/Violations/ViolationsUtils.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libs/Violations/ViolationsUtils.ts b/src/libs/Violations/ViolationsUtils.ts index f9971e713649..86f60ed04aa5 100644 --- a/src/libs/Violations/ViolationsUtils.ts +++ b/src/libs/Violations/ViolationsUtils.ts @@ -1,10 +1,9 @@ import reject from 'lodash/reject'; import Onyx from 'react-native-onyx'; +import type {Phrase, PhraseParameters} from '@libs/Localize'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PolicyCategories, PolicyTags, Transaction, TransactionViolation} from '@src/types/onyx'; -import type {Phrase, PhraseParameters} from '../Localize'; - const ViolationsUtils = { /** @@ -135,7 +134,7 @@ const ViolationsUtils = { case 'nonExpensiworksExpense': return translate('violations.nonExpensiworksExpense'); case 'overAutoApprovalLimit': - return translate('violations.overAutoApprovalLimit', {formattedLimitAmount: violation.data?.formattedLimitAmount ?? ''}); + return translate('violations.overAutoApprovalLimit', {formattedLimitAmount: violation.data?.formattedLimit ?? ''}); case 'overCategoryLimit': return translate('violations.overCategoryLimit', {categoryLimit: violation.data?.categoryLimit ?? ''}); case 'overLimit': @@ -148,7 +147,7 @@ const ViolationsUtils = { return translate('violations.receiptNotSmartScanned'); case 'receiptRequired': return translate('violations.receiptRequired', { - amount: violation.data?.amount ?? '0', + formattedLimit: violation.data?.formattedLimit ?? '0', category: violation.data?.category ?? '', }); case 'rter': From dd39fd141627e40c0e3c7647d1511551b9ebec5f Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Tue, 9 Jan 2024 15:05:28 -0500 Subject: [PATCH 030/635] feat(Violations): move condition outside of component body to allow memoization --- .../ReportActionItem/MoneyRequestPreview.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 6b145d86d80a..c14175689934 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -131,6 +131,13 @@ const defaultProps = { transactionViolations: {}, }; +// We should not render the component if there is no iouReport and it's not a split. +// Moved outside of the component scope to allow memoization of values later. +function MoneyRequestPreviewWrapper(props) { + // eslint-disable-next-line react/jsx-props-no-spreading + return _.isEmpty(props.iouReport) && !props.isBillSplit ? null : ; +} + function MoneyRequestPreview(props) { const theme = useTheme(); const styles = useThemeStyles(); @@ -138,10 +145,6 @@ function MoneyRequestPreview(props) { const {translate} = useLocalize(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); - if (_.isEmpty(props.iouReport) && !props.isBillSplit) { - return null; - } - const sessionAccountID = lodashGet(props.session, 'accountID', null); const managerID = props.iouReport.managerID || ''; const ownerAccountID = props.iouReport.ownerAccountID || ''; From d037f7d7a27e347524b1bfd2f648307daf205a9a Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Tue, 9 Jan 2024 15:07:47 -0500 Subject: [PATCH 031/635] feat(Violations): update translations to handle null cases. correct data prop names. --- src/languages/en.ts | 4 ++-- src/languages/es.ts | 5 +++-- src/languages/types.ts | 4 ++-- src/types/onyx/TransactionViolation.ts | 2 +- 4 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 8e94aecc1b79..1e723ea54cfe 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2078,7 +2078,7 @@ export default { maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Date older than ${maxAge} days`, missingCategory: 'Missing category', missingComment: 'Description required for selected category', - missingTag: ({tagName}: ViolationsMissingTagParams) => `Missing ${tagName ?? 'tag'}`, + missingTag: (params: ViolationsMissingTagParams) => `Missing ${params?.tagName ?? 'tag'}`, modifiedAmount: 'Amount greater than scanned receipt', modifiedDate: 'Date differs from scanned receipt', nonExpensiworksExpense: 'Non-Expensiworks expense', @@ -2088,7 +2088,7 @@ export default { overLimitAttendee: ({amount}: ViolationsOverLimitParams) => `Amount over ${amount}/person limit`, perDayLimit: ({limit}: ViolationsPerDayLimitParams) => `Amount over daily ${limit}/person category limit`, receiptNotSmartScanned: 'Receipt not verified. Please confirm accuracy.', - receiptRequired: ({amount, category}: ViolationsReceiptRequiredParams) => `Receipt required over ${amount} ${category ? ' category limit' : ''}`, + receiptRequired: (params: ViolationsReceiptRequiredParams) => `Receipt required${params ? ` over ${params.formattedLimit}${params.category ? ` category limit` : ''}` : ''}`, reviewRequired: 'Review required', rter: ({brokenBankConnection, email, isAdmin, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { diff --git a/src/languages/es.ts b/src/languages/es.ts index 8760ef652d1d..cead5beb9c2a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2565,7 +2565,7 @@ export default { maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Fecha de más de ${maxAge} días`, missingCategory: 'Falta categoría', missingComment: 'Descripción obligatoria para categoría seleccionada', - missingTag: ({tagName}: ViolationsMissingTagParams) => `Falta ${tagName}`, + missingTag: (params: ViolationsMissingTagParams) => `Falta ${params?.tagName ?? 'etiqueta'}`, modifiedAmount: 'Importe superior al del recibo escaneado', modifiedDate: 'Fecha difiere del recibo escaneado', nonExpensiworksExpense: 'Gasto no es de Expensiworks', @@ -2575,7 +2575,8 @@ export default { overLimitAttendee: ({amount}: ViolationsOverLimitParams) => `Importe supera el límite de ${amount}/persona`, perDayLimit: ({limit}: ViolationsPerDayLimitParams) => `Importe supera el límite diario de la categoría de ${limit}/persona`, receiptNotSmartScanned: 'Recibo no verificado. Por favor, confirma su exactitud', - receiptRequired: ({amount, category}: ViolationsReceiptRequiredParams) => `Recibo obligatorio para importes sobre ${category ? 'el limite de la categoría de ' : ''}${amount}`, + receiptRequired: (params: ViolationsReceiptRequiredParams) => + `Recibo obligatorio${params ? ` para importes sobre${params.category ? ` el limite de la categoría de` : ''} ${params.formattedLimit}` : ''}`, reviewRequired: 'Revisión requerida', rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { diff --git a/src/languages/types.ts b/src/languages/types.ts index 3185b7a8f6f1..e69b4bd49686 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -223,7 +223,7 @@ type ViolationsInvoiceMarkupParams = {invoiceMarkup?: number}; type ViolationsMaxAgeParams = {maxAge: number}; -type ViolationsMissingTagParams = {tagName?: string}; +type ViolationsMissingTagParams = {tagName?: string} | null; type ViolationsOverAutoApprovalLimitParams = {formattedLimitAmount: string}; @@ -233,7 +233,7 @@ type ViolationsOverLimitParams = {amount: string}; type ViolationsPerDayLimitParams = {limit: string}; -type ViolationsReceiptRequiredParams = {amount: string; category?: string}; +type ViolationsReceiptRequiredParams = {formattedLimit: string; category?: string} | null; type ViolationsRterParams = { brokenBankConnection: boolean; diff --git a/src/types/onyx/TransactionViolation.ts b/src/types/onyx/TransactionViolation.ts index 253e2e8e2a29..b01a5fe38a62 100644 --- a/src/types/onyx/TransactionViolation.ts +++ b/src/types/onyx/TransactionViolation.ts @@ -18,7 +18,7 @@ type TransactionViolation = { invoiceMarkup?: number; maxAge?: number; tagName?: string; - formattedLimitAmount?: string; + formattedLimit?: string; categoryLimit?: string; limit?: string; category?: string; From 3b87f1343846608b7be3d312250bf8b4fc692b6c Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Tue, 9 Jan 2024 15:36:39 -0500 Subject: [PATCH 032/635] feat(Violations): filter violations to remove 'notification' type messages --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index c14175689934..dcc342e795d6 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -222,7 +222,7 @@ function MoneyRequestPreview(props) { if (hasViolations) { const violations = TransactionUtils.getTransactionViolations(props.transaction, props.transactionViolations); const violation = translate(`violations.${violations[0].name}`, violations[0].data); - const isTooLong = violations.length > 1 || violation.length > 15; + const isTooLong = _.filter(violations, (v) => v.type === 'violation').length > 1 || violation.length > 15; message += ` • ${isTooLong ? translate('violations.reviewRequired') : violation}`; } if (ReportUtils.isPaidGroupPolicyExpenseReport(props.iouReport) && ReportUtils.isReportApproved(props.iouReport) && !ReportUtils.isSettled(props.iouReport)) { From ebcff3806996d04dcc8fb5b4dbf78e4f8d3b02d4 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Tue, 9 Jan 2024 15:37:09 -0500 Subject: [PATCH 033/635] feat(Violations): memoize preview header text --- .../ReportActionItem/MoneyRequestPreview.js | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index dcc342e795d6..4cbb9330f962 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -1,7 +1,7 @@ import {truncate} from 'lodash'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -197,7 +197,7 @@ function MoneyRequestPreview(props) { showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive); }; - const getPreviewHeaderText = () => { + const previewHeaderText = useMemo(() => { if (isDistanceRequest) { return translate('common.distance'); } @@ -232,8 +232,20 @@ function MoneyRequestPreview(props) { } else if (props.iouReport.isCancelledIOU) { message += ` • ${translate('iou.canceled')}`; } - return message; - }; + return message + (isSettled && !props.iouReport.isCancelledIOU ? ` • ${getSettledMessage()}` : ''); + }, [ + getSettledMessage, + hasViolations, + isDistanceRequest, + isExpensifyCardTransaction, + isScanning, + isSettled, + props.iouReport, + props.isBillSplit, + props.transaction, + props.transactionViolations, + translate, + ]); const getDisplayAmountText = () => { if (isDistanceRequest) { @@ -294,9 +306,7 @@ function MoneyRequestPreview(props) { ) : ( - - {getPreviewHeaderText() + (isSettled && !props.iouReport.isCancelledIOU ? ` • ${getSettledMessage()}` : '')} - + {previewHeaderText} {!isSettled && hasFieldErrors && ( Date: Tue, 9 Jan 2024 15:38:04 -0500 Subject: [PATCH 034/635] feat(Violations): add proptypes and call wrapper component --- src/components/ReportActionItem/MoneyRequestPreview.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 4cbb9330f962..5c7d89f535b3 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -397,6 +397,9 @@ function MoneyRequestPreview(props) { MoneyRequestPreview.propTypes = propTypes; MoneyRequestPreview.defaultProps = defaultProps; MoneyRequestPreview.displayName = 'MoneyRequestPreview'; +MoneyRequestPreviewWrapper.propTypes = propTypes; +MoneyRequestPreviewWrapper.defaultProps = defaultProps; +MoneyRequestPreviewWrapper.displayName = 'MoneyRequestPreviewWrapper'; export default withOnyx({ personalDetails: { @@ -420,4 +423,4 @@ export default withOnyx({ transactionViolations: { key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, }, -})(MoneyRequestPreview); +})(MoneyRequestPreviewWrapper); From cd0f3a81fd156581b1d3b2dc80f83d2ffaa32899 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Tue, 9 Jan 2024 15:38:10 -0500 Subject: [PATCH 035/635] feat(Violations): fix import --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f2584cb8accd..6d20c3badac5 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -22,7 +22,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import * as UserUtils from '@libs/UserUtils'; -import ViolationsUtils from '@libs/ViolationsUtils'; +import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; From 96470a37550bba0ff0bdb4634112d7294e3e6637 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Tue, 9 Jan 2024 16:59:53 -0500 Subject: [PATCH 036/635] feat(Violations): memoize strings --- .../ReportActionItem/MoneyRequestPreview.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 5c7d89f535b3..738ba6c40f59 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -186,12 +186,12 @@ function MoneyRequestPreview(props) { const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction)] : []; - const getSettledMessage = () => { + const getSettledMessage = useMemo(() => { if (isExpensifyCardTransaction) { return translate('common.done'); } return translate('iou.settledExpensify'); - }; + }, [isExpensifyCardTransaction, translate]); const showContextMenu = (event) => { showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive); @@ -232,7 +232,7 @@ function MoneyRequestPreview(props) { } else if (props.iouReport.isCancelledIOU) { message += ` • ${translate('iou.canceled')}`; } - return message + (isSettled && !props.iouReport.isCancelledIOU ? ` • ${getSettledMessage()}` : ''); + return message + (isSettled && !props.iouReport.isCancelledIOU ? ` • ${getSettledMessage}` : ''); }, [ getSettledMessage, hasViolations, @@ -247,7 +247,7 @@ function MoneyRequestPreview(props) { translate, ]); - const getDisplayAmountText = () => { + const displayAmountText = useMemo(() => { if (isDistanceRequest) { return requestAmount && !hasPendingWaypoints ? CurrencyUtils.convertToDisplayString(requestAmount, props.transaction.currency) : translate('common.tbd'); } @@ -261,9 +261,9 @@ function MoneyRequestPreview(props) { } return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); - }; + }, [hasPendingWaypoints, isDistanceRequest, isScanning, props.transaction, requestAmount, requestCurrency, translate]); - const getDisplayDeleteAmountText = () => { + const displayDeleteAmountText = useMemo(() => { const {amount, currency} = ReportUtils.getTransactionDetails(props.action.originalMessage); if (isDistanceRequest) { @@ -271,9 +271,9 @@ function MoneyRequestPreview(props) { } return CurrencyUtils.convertToDisplayString(amount, currency); - }; + }, [isDistanceRequest, props.action.originalMessage]); - const displayAmount = isDeleted ? getDisplayDeleteAmountText() : getDisplayAmountText(); + const displayAmount = isDeleted ? displayDeleteAmountText : displayAmountText; const childContainer = ( From 1387e34a837991d6497580ad6428d96992cfacbf Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 10 Jan 2024 01:05:48 +0100 Subject: [PATCH 037/635] Permissions check --- src/components/MoneyRequestHeader.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 7c64eb54fcf7..5f889d78a5ef 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -79,6 +79,7 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, // Only the requestor can take delete the request, admins can only edit it. const isActionOwner = lodashGet(parentReportAction, 'actorAccountID') === lodashGet(session, 'accountID', null); const isPolicyAdmin = lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN; + const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === moneyRequestReport.managerID; const deleteTransaction = useCallback(() => { IOU.deleteMoneyRequest(lodashGet(parentReportAction, 'originalMessage.IOUTransactionID'), parentReportAction, true); @@ -117,14 +118,16 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(report)]; if (isRequestModifiable) { - if (isOnHold && (ReportUtils.isHoldCreator(transaction, lodashGet(report, 'reportID')) || isPolicyAdmin)) { + const isHoldCreator = ReportUtils.isHoldCreator(transaction, lodashGet(report, 'reportID')); + const isRequestIOU = lodashGet(parentReport, 'type') === 'iou'; + if (isOnHold && ((isRequestIOU && isHoldCreator) || (!isRequestIOU && (isPolicyAdmin || isActionOwner || isApprover)))) { threeDotsMenuItems.push({ icon: Expensicons.Stopwatch, text: translate('iou.unholdRequest'), onSelected: () => changeMoneyRequestStatus(), }); } - if (!isOnHold && (lodashGet(parentReport, 'type') === 'iou' || isPolicyAdmin || isActionOwner)) { + if (!isOnHold && (isRequestIOU || isPolicyAdmin || isActionOwner || isApprover)) { threeDotsMenuItems.push({ icon: Expensicons.Stopwatch, text: translate('iou.holdRequest'), From 7c01d82bbc3e5b749cb45758b915af0254d0100b Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 10 Jan 2024 23:10:50 +0700 Subject: [PATCH 038/635] migrate to ts --- src/CONST.ts | 4 +- src/components/AddPaymentMethodMenu.js | 13 +- src/components/KYCWall/index.js | 3 +- src/libs/CardUtils.ts | 10 +- src/libs/PaymentUtils.ts | 2 +- src/libs/actions/BankAccounts.ts | 2 +- src/libs/actions/PaymentMethods.ts | 14 +- ...entMethodList.js => PaymentMethodList.tsx} | 214 +++++++----------- ...lletEmptyState.js => WalletEmptyState.tsx} | 10 +- .../{CardDetails.js => CardDetails.tsx} | 67 +++--- .../{WalletPage.js => WalletPage.tsx} | 163 +++++++------ .../{index.native.js => index.native.tsx} | 0 .../Wallet/WalletPage/{index.js => index.tsx} | 0 src/pages/settings/Wallet/WalletPage/types.ts | 31 +++ .../Wallet/WalletPage/walletPagePropTypes.js | 52 ----- src/types/onyx/AccountData.ts | 2 + src/types/onyx/Card.ts | 4 +- src/types/onyx/index.ts | 2 + 18 files changed, 271 insertions(+), 322 deletions(-) rename src/pages/settings/Wallet/{PaymentMethodList.js => PaymentMethodList.tsx} (73%) rename src/pages/settings/Wallet/{WalletEmptyState.js => WalletEmptyState.tsx} (87%) rename src/pages/settings/Wallet/WalletPage/{CardDetails.js => CardDetails.tsx} (75%) rename src/pages/settings/Wallet/WalletPage/{WalletPage.js => WalletPage.tsx} (87%) rename src/pages/settings/Wallet/WalletPage/{index.native.js => index.native.tsx} (100%) rename src/pages/settings/Wallet/WalletPage/{index.js => index.tsx} (100%) create mode 100644 src/pages/settings/Wallet/WalletPage/types.ts delete mode 100644 src/pages/settings/Wallet/WalletPage/walletPagePropTypes.js diff --git a/src/CONST.ts b/src/CONST.ts index c6849db630f2..c0eecf6858e3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1350,7 +1350,9 @@ const CONST = { CLOSED: 6, STATE_SUSPENDED: 7, }, - ACTIVE_STATES: [2, 3, 4, 7], + get ACTIVE_STATES() { + return [2, 3, 4, 7]; + }, }, AVATAR_ROW_SIZE: { DEFAULT: 4, diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 4abe5655e307..a936b0efdfa1 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -122,11 +122,8 @@ AddPaymentMethodMenu.propTypes = propTypes; AddPaymentMethodMenu.defaultProps = defaultProps; AddPaymentMethodMenu.displayName = 'AddPaymentMethodMenu'; -export default compose( - withWindowDimensions, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), -)(AddPaymentMethodMenu); +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(AddPaymentMethodMenu); diff --git a/src/components/KYCWall/index.js b/src/components/KYCWall/index.js index 49329c73d474..87f1496a494a 100644 --- a/src/components/KYCWall/index.js +++ b/src/components/KYCWall/index.js @@ -2,12 +2,13 @@ import React from 'react'; import BaseKYCWall from './BaseKYCWall'; import {defaultProps, propTypes} from './kycWallPropTypes'; -function KYCWall(props) { +function KYCWall({children, ...props}) { return ( ); } diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index d71ad9c2629a..bc18808886e2 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -29,7 +29,10 @@ function getMonthFromExpirationDateString(expirationDateString: string) { * @param cardID * @returns boolean */ -function isExpensifyCard(cardID: number) { +function isExpensifyCard(cardID?: number) { + if (!cardID) { + return false; + } const card = allCards[cardID]; if (!card) { return false; @@ -49,7 +52,10 @@ function isCorporateCard(cardID: number) { * @param cardID * @returns string in format % - %. */ -function getCardDescription(cardID: number) { +function getCardDescription(cardID?: number) { + if (!cardID) { + return ''; + } const card = allCards[cardID]; if (!card) { return ''; diff --git a/src/libs/PaymentUtils.ts b/src/libs/PaymentUtils.ts index dd35d0df5cfb..60dac04a09ac 100644 --- a/src/libs/PaymentUtils.ts +++ b/src/libs/PaymentUtils.ts @@ -40,7 +40,7 @@ function getPaymentMethodDescription(accountType: AccountType, account: BankAcco /** * Get the PaymentMethods list */ -function formatPaymentMethods(bankAccountList: Record, fundList: Record, styles: ThemeStyles): PaymentMethod[] { +function formatPaymentMethods(bankAccountList: Record, fundList: Record | Fund[], styles: ThemeStyles): PaymentMethod[] { const combinedPaymentMethods: PaymentMethod[] = []; Object.values(bankAccountList).forEach((bankAccount) => { diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index f7b7ec89c670..1ce6bc38191f 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -50,7 +50,7 @@ function setPlaidEvent(eventName: string) { /** * Open the personal bank account setup flow, with an optional exitReportID to redirect to once the flow is finished. */ -function openPersonalBankAccountSetupView(exitReportID: string) { +function openPersonalBankAccountSetupView(exitReportID?: string) { clearPlaid().then(() => { if (exitReportID) { Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {exitReportID}); diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index a7ae54f46416..6614d3516253 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -71,10 +71,10 @@ function openWalletPage() { } function getMakeDefaultPaymentOnyxData( - bankAccountID: number, - fundID: number, - previousPaymentMethod: PaymentMethod, - currentPaymentMethod: PaymentMethod, + bankAccountID: number | null, + fundID: number | null, + previousPaymentMethod?: PaymentMethod, + currentPaymentMethod?: PaymentMethod, isOptimisticData = true, ): OnyxUpdate[] { const onyxData: OnyxUpdate[] = [ @@ -130,10 +130,10 @@ function getMakeDefaultPaymentOnyxData( * Sets the default bank account or debit card for an Expensify Wallet * */ -function makeDefaultPaymentMethod(bankAccountID: number, fundID: number, previousPaymentMethod: PaymentMethod, currentPaymentMethod: PaymentMethod) { +function makeDefaultPaymentMethod(bankAccountID: number | null, fundID: number | null, previousPaymentMethod?: PaymentMethod, currentPaymentMethod?: PaymentMethod) { type MakeDefaultPaymentMethodParams = { - bankAccountID: number; - fundID: number; + bankAccountID: number | null; + fundID: number | null; }; const parameters: MakeDefaultPaymentMethodParams = { diff --git a/src/pages/settings/Wallet/PaymentMethodList.js b/src/pages/settings/Wallet/PaymentMethodList.tsx similarity index 73% rename from src/pages/settings/Wallet/PaymentMethodList.js rename to src/pages/settings/Wallet/PaymentMethodList.tsx index 06bd8afa6140..9cceaa60c10c 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.js +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -1,15 +1,17 @@ import {FlashList} from '@shopify/flash-list'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useMemo} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import React, {ReactElement, Ref, useCallback, useMemo} from 'react'; +import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import {TupleToUnion, ValueOf} from 'type-fest'; import _ from 'underscore'; import bankAccountPropTypes from '@components/bankAccountPropTypes'; import Button from '@components/Button'; import cardPropTypes from '@components/cardPropTypes'; import FormAlertWrapper from '@components/FormAlertWrapper'; import getBankIcon from '@components/Icon/BankIcons'; +import {BankName} from '@components/Icon/BankIconsUtils'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -28,112 +30,91 @@ import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import assignedCardPropTypes from './assignedCardPropTypes'; +import {AccountData, BankAccountList, Card, CardList, FundList, UserWallet} from '@src/types/onyx'; +import PaymentMethod from '@src/types/onyx/PaymentMethod'; -const propTypes = { - /** What to do when a menu item is pressed */ - onPress: PropTypes.func.isRequired, +const FILTER_TYPES = [CONST.PAYMENT_METHODS.DEBIT_CARD, CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, ''] as const; +const ACTION_PAYMENT_METHOD_TYPES = [...Object.values(CONST.PAYMENT_METHODS), ''] as const; +type PaymentMethodListOnyxProps = { /** List of bank accounts */ - bankAccountList: PropTypes.objectOf(bankAccountPropTypes), + bankAccountList: OnyxEntry; /** List of assigned cards */ - cardList: PropTypes.objectOf(assignedCardPropTypes), + cardList: OnyxEntry; /** List of user's cards */ - fundList: PropTypes.objectOf(cardPropTypes), - - /** Whether the add bank account button should be shown on the list */ - shouldShowAddBankAccount: PropTypes.bool, - - /** Whether the add Payment button be shown on the list */ - shouldShowAddPaymentMethodButton: PropTypes.bool, - - /** Whether the assigned cards should be shown on the list */ - shouldShowAssignedCards: PropTypes.bool, - - /** Whether the empty list message should be shown when the list is empty */ - shouldShowEmptyListMessage: PropTypes.bool, + fundList: OnyxEntry; /** Are we loading payment methods? */ - isLoadingPaymentMethods: PropTypes.bool, - - /** Type to filter the payment Method list */ - filterType: PropTypes.oneOf([CONST.PAYMENT_METHODS.DEBIT_CARD, CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, '']), + isLoadingPaymentMethods: OnyxEntry; /** User wallet props */ - userWallet: PropTypes.shape({ - /** The ID of the linked account */ - walletLinkedAccountID: PropTypes.number, - - /** The type of the linked account (debitCard or bankAccount) */ - walletLinkedAccountType: PropTypes.string, - }), + userWallet: OnyxEntry; +}; +type PaymentMethodListProps = PaymentMethodListOnyxProps & { /** Type of active/highlighted payment method */ - actionPaymentMethodType: PropTypes.oneOf([..._.values(CONST.PAYMENT_METHODS), '']), + actionPaymentMethodType?: TupleToUnion; /** ID of active/highlighted payment method */ - activePaymentMethodID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + activePaymentMethodID?: string | number; /** ID of selected payment method */ - selectedMethodID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + selectedMethodID?: string | number; /** Content for the FlatList header component */ - listHeaderComponent: PropTypes.func, + listHeaderComponent?: ReactElement; /** Callback for whenever FlatList component size changes */ - onListContentSizeChange: PropTypes.func, + onListContentSizeChange?: () => void; /** Should menu items be selectable with a checkbox */ - shouldShowSelectedState: PropTypes.bool, + shouldShowSelectedState?: boolean; /** React ref being forwarded to the PaymentMethodList Button */ - buttonRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + buttonRef?: Ref; /** To enable/disable scrolling */ - shouldEnableScroll: PropTypes.bool, + shouldEnableScroll?: boolean; /** List container style */ - style: stylePropTypes, + style?: StyleProp; + + /** Type to filter the payment Method list */ + filterType?: TupleToUnion; + /** Whether the add bank account button should be shown on the list */ + shouldShowAddBankAccount?: boolean; + + /** Whether the add Payment button be shown on the list */ + shouldShowAddPaymentMethodButton?: boolean; + + /** Whether the assigned cards should be shown on the list */ + shouldShowAssignedCards?: boolean; + + /** Whether the empty list message should be shown when the list is empty */ + shouldShowEmptyListMessage?: boolean; + + /** What to do when a menu item is pressed */ + onPress: (event?: GestureResponderEvent | KeyboardEvent, accountType?: string, accountData?: AccountData, isDefault?: boolean, methodID?: number) => void | Promise; }; -const defaultProps = { - bankAccountList: {}, - cardList: {}, - fundList: null, - userWallet: { - walletLinkedAccountID: 0, - walletLinkedAccountType: '', - }, - isLoadingPaymentMethods: true, - shouldShowAddBankAccount: true, - shouldShowAddPaymentMethodButton: true, - shouldShowAssignedCards: false, - shouldShowEmptyListMessage: true, - filterType: '', - actionPaymentMethodType: '', - activePaymentMethodID: '', - selectedMethodID: '', - listHeaderComponent: null, - buttonRef: () => {}, - onListContentSizeChange: () => {}, - shouldEnableScroll: true, - style: {}, - shouldShowSelectedState: false, +type PaymentMethodItem = PaymentMethod & { + onPress?: (e: GestureResponderEvent | KeyboardEvent | undefined) => void; + canDismissError?: boolean; + disabled?: boolean; + shouldShowRightIcon?: boolean; + interactive?: boolean; + brickRoadIndicator?: ValueOf; }; -/** - * Dismisses the error on the payment method - * @param {Object} item - */ -function dismissError(item) { +function dismissError(item: PaymentMethod) { const isBankAccount = item.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT; const paymentList = isBankAccount ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.FUND_LIST; const paymentID = isBankAccount ? lodashGet(item, ['accountData', 'bankAccountID'], '') : lodashGet(item, ['accountData', 'fundID'], ''); if (!paymentID) { - Log.info('Unable to clear payment method error: ', item); + Log.info('Unable to clear payment method error: ', undefined, item); return; } @@ -150,12 +131,7 @@ function dismissError(item) { } } -/** - * @param {Array} filteredPaymentMethods - * @param {Boolean} isDefault - * @returns {Boolean} - */ -function shouldShowDefaultBadge(filteredPaymentMethods, isDefault = false) { +function shouldShowDefaultBadge(filteredPaymentMethods: PaymentMethod[], isDefault = false) { if (!isDefault) { return false; } @@ -167,45 +143,35 @@ function shouldShowDefaultBadge(filteredPaymentMethods, isDefault = false) { return defaultablePaymentMethodCount > 1; } -/** - * @param {String} actionPaymentMethodType - * @param {String|Number} activePaymentMethodID - * @param {String} paymentMethod - * @return {Boolean} - */ -function isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod) { +function isPaymentMethodActive(actionPaymentMethodType: string, activePaymentMethodID: string | number, paymentMethod: PaymentMethod) { return paymentMethod.accountType === actionPaymentMethodType && paymentMethod.methodID === activePaymentMethodID; } -/** - * @param {Object} item - * @returns {String} - */ -function keyExtractor(item) { - return item.key; +function keyExtractor(item: PaymentMethod) { + return item.key || ''; } function PaymentMethodList({ - actionPaymentMethodType, - activePaymentMethodID, - bankAccountList, - buttonRef, - cardList, - fundList, - filterType, - isLoadingPaymentMethods, - onPress, - shouldShowSelectedState, - shouldShowAddPaymentMethodButton, - shouldShowAddBankAccount, - shouldShowEmptyListMessage, - shouldShowAssignedCards, - selectedMethodID, + actionPaymentMethodType = '', + activePaymentMethodID = '', + bankAccountList = {}, + buttonRef = () => {}, + cardList = {}, + fundList = {}, + filterType = '', listHeaderComponent, - onListContentSizeChange, - shouldEnableScroll, - style, -}) { + isLoadingPaymentMethods = true, + onPress, + shouldShowSelectedState = false, + shouldShowAddPaymentMethodButton = true, + shouldShowAddBankAccount = true, + shouldShowEmptyListMessage = true, + shouldShowAssignedCards = false, + selectedMethodID = '', + onListContentSizeChange = () => {}, + shouldEnableScroll = true, + style = {}, +}: PaymentMethodListProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); @@ -215,7 +181,7 @@ function PaymentMethodList({ if (shouldShowAssignedCards) { const assignedCards = _.chain(cardList) // Filter by physical, active cards associated with a domain - .filter((card) => !card.isVirtual && card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state)) + .filter((card) => !card.isVirtual && card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0)) .sortBy((card) => !CardUtils.isExpensifyCard(card.cardID)) .value(); @@ -223,15 +189,15 @@ function PaymentMethodList({ return _.map(assignedCards, (card) => { const isExpensifyCard = CardUtils.isExpensifyCard(card.cardID); - const icon = getBankIcon({bankName: card.bank, isCard: true, styles}); + const icon = getBankIcon({bankName: card.bank as BankName, isCard: true, styles}); // In the case a user has been assigned multiple physical Expensify Cards under one domain, display the Card with PAN const expensifyCardDescription = numberPhysicalExpensifyCards > 1 ? CardUtils.getCardDescription(card.cardID) : translate('walletPage.expensifyCard'); return { - key: card.cardID, + key: card.cardID.toString(), title: isExpensifyCard ? expensifyCardDescription : card.cardName, description: card.domainName, - onPress: isExpensifyCard ? () => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(card.domainName)) : () => {}, + onPress: isExpensifyCard ? () => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(card.domainName ?? '')) : () => {}, shouldShowRightIcon: isExpensifyCard, interactive: isExpensifyCard, canDismissError: isExpensifyCard, @@ -245,8 +211,8 @@ function PaymentMethodList({ const paymentCardList = fundList || {}; // Hide any billing cards that are not P2P debit cards for now because you cannot make them your default method, or delete them - const filteredCardList = _.filter(paymentCardList, (card) => card.accountData.additionalData.isP2PDebitCard); - let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList, filteredCardList, styles); + const filteredCardList = _.filter(paymentCardList, (card) => !!card.accountData?.additionalData?.isP2PDebitCard); + let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, filteredCardList, styles); if (!_.isEmpty(filterType)) { combinedPaymentMethods = _.filter(combinedPaymentMethods, (paymentMethod) => paymentMethod.accountType === filterType); @@ -261,10 +227,9 @@ function PaymentMethodList({ combinedPaymentMethods = _.map(combinedPaymentMethods, (paymentMethod) => { const isMethodActive = isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod); - return { ...paymentMethod, - onPress: (e) => onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.isDefault, paymentMethod.methodID), + onPress: (e: GestureResponderEvent) => onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.isDefault, paymentMethod.methodID), wrapperStyle: isMethodActive ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : null, disabled: paymentMethod.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, }; @@ -296,14 +261,9 @@ function PaymentMethodList({ /** * Create a menuItem for each passed paymentMethod - * - * @param {Object} params - * @param {Object} params.item - * - * @return {React.Component} */ const renderItem = useCallback( - ({item}) => ( + ({item}: {item: PaymentMethodItem}) => ( dismissError(item)} pendingAction={item.pendingAction} @@ -321,7 +281,7 @@ function PaymentMethodList({ iconHeight={item.iconHeight || item.iconSize} iconWidth={item.iconWidth || item.iconSize} iconStyles={item.iconStyles} - badgeText={shouldShowDefaultBadge(filteredPaymentMethods, item.isDefault) ? translate('paymentMethodList.defaultPaymentMethod') : null} + badgeText={shouldShowDefaultBadge(filteredPaymentMethods, item.isDefault) ? translate('paymentMethodList.defaultPaymentMethod') : undefined} wrapperStyle={styles.paymentMethod} shouldShowRightIcon={item.shouldShowRightIcon} shouldShowSelectedState={shouldShowSelectedState} @@ -340,7 +300,7 @@ function PaymentMethodList({ ({ bankAccountList: { key: ONYXKEYS.BANK_ACCOUNT_LIST, }, diff --git a/src/pages/settings/Wallet/WalletEmptyState.js b/src/pages/settings/Wallet/WalletEmptyState.tsx similarity index 87% rename from src/pages/settings/Wallet/WalletEmptyState.js rename to src/pages/settings/Wallet/WalletEmptyState.tsx index 7a3a9e9ce6b7..4ef9488368cb 100644 --- a/src/pages/settings/Wallet/WalletEmptyState.js +++ b/src/pages/settings/Wallet/WalletEmptyState.tsx @@ -11,9 +11,8 @@ import Navigation from '@libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -const propTypes = { - /** The function that is called when a menu item is pressed */ - onAddPaymentMethod: PropTypes.func.isRequired, +type WalletEmptyStateProps = { + onAddPaymentMethod: () => void; }; const WALLET_FEATURES = [ @@ -31,7 +30,7 @@ const WALLET_FEATURES = [ }, ]; -function WalletEmptyState({onAddPaymentMethod}) { +function WalletEmptyState({onAddPaymentMethod}: WalletEmptyStateProps) { const theme = useTheme(); const {translate} = useLocalize(); return ( @@ -58,7 +57,4 @@ function WalletEmptyState({onAddPaymentMethod}) { ); } -WalletEmptyState.displayName = 'WalletEmptyState'; -WalletEmptyState.propTypes = propTypes; - export default WalletEmptyState; diff --git a/src/pages/settings/Wallet/WalletPage/CardDetails.js b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx similarity index 75% rename from src/pages/settings/Wallet/WalletPage/CardDetails.js rename to src/pages/settings/Wallet/WalletPage/CardDetails.tsx index b51c34e89d17..09a7d937053b 100644 --- a/src/pages/settings/Wallet/WalletPage/CardDetails.js +++ b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle'; @@ -14,50 +14,39 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import {PrivatePersonalDetails} from '@src/types/onyx'; -const propTypes = { +const defaultPrivatePersonalDetails = { + address: { + street: '', + street2: '', + city: '', + state: '', + zip: '', + country: '', + }, +}; + +type CardDetailsOnyxProps = { + /** User's private personal details */ + privatePersonalDetails: OnyxEntry; +}; + +type CardDetailsProps = CardDetailsOnyxProps & { /** Card number */ - pan: PropTypes.string, + pan?: string; /** Card expiration date */ - expiration: PropTypes.string, + expiration?: string; /** 3 digit code */ - cvv: PropTypes.string, - - /** User's private personal details */ - privatePersonalDetails: PropTypes.shape({ - /** User's home address */ - address: PropTypes.shape({ - street: PropTypes.string, - city: PropTypes.string, - state: PropTypes.string, - zip: PropTypes.string, - country: PropTypes.string, - }), - }), + cvv?: string; /** Domain name */ - domain: PropTypes.string.isRequired, -}; - -const defaultProps = { - pan: '', - expiration: '', - cvv: '', - privatePersonalDetails: { - address: { - street: '', - street2: '', - city: '', - state: '', - zip: '', - country: '', - }, - }, + domain: string; }; -function CardDetails({pan, expiration, cvv, privatePersonalDetails, domain}) { +function CardDetails({pan = '', expiration = '', cvv = '', privatePersonalDetails = defaultPrivatePersonalDetails, domain}: CardDetailsProps) { const styles = useThemeStyles(); usePrivatePersonalDetails(); const {translate} = useLocalize(); @@ -79,6 +68,8 @@ function CardDetails({pan, expiration, cvv, privatePersonalDetails, domain}) { tooltipTextChecked={translate('reportActionContextMenu.copied')} icon={Expensicons.Copy} onPress={handleCopyToClipboard} + accessible={false} + text="" /> } @@ -96,7 +87,7 @@ function CardDetails({pan, expiration, cvv, privatePersonalDetails, domain}) { /> ({ privatePersonalDetails: { key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, }, diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx similarity index 87% rename from src/pages/settings/Wallet/WalletPage/WalletPage.js rename to src/pages/settings/Wallet/WalletPage/WalletPage.tsx index bf547bc4bd10..0463008cf7cb 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -1,7 +1,9 @@ import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; +import React, {Ref, useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import {ActivityIndicator, Dimensions, ScrollView, View} from 'react-native'; +import {GestureResponderEvent} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import {TupleToUnion} from 'type-fest'; import _ from 'underscore'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; import Button from '@components/Button'; @@ -20,6 +22,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import WalletSection from '@components/WalletSection'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -35,27 +38,46 @@ import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {defaultProps, propTypes} from './walletPagePropTypes'; +import {AccountData, BankAccount} from '@src/types/onyx'; +import PaymentMethod from '@src/types/onyx/PaymentMethod'; +import IconAsset from '@src/types/utils/IconAsset'; +import {WalletPageOnyxProps, WalletPageProps} from './types'; + +const ACTION_PAYMENT_METHOD_TYPES = [...Object.values(CONST.PAYMENT_METHODS), ''] as const; + +type PaymentMethodState = { + isSelectedPaymentMethodDefault: boolean; + selectedPaymentMethod: AccountData; + formattedSelectedPaymentMethod: { + title: string; + icon?: IconAsset; + description?: string; + type?: string; + }; + methodID: string | number; + selectedPaymentMethodType: string; +}; -function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethods, network, shouldListenForResize, userWallet, walletTerms}) { +function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadingPaymentMethods = true, shouldListenForResize = false, userWallet, walletTerms = {}}: WalletPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); + const network = useNetwork(); const {isSmallScreenWidth, windowWidth} = useWindowDimensions(); const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false); const [shouldShowDefaultDeleteMenu, setShouldShowDefaultDeleteMenu] = useState(false); const [shouldShowLoadingSpinner, setShouldShowLoadingSpinner] = useState(false); - const [paymentMethod, setPaymentMethod] = useState({ + const [paymentMethod, setPaymentMethod] = useState({ isSelectedPaymentMethodDefault: false, selectedPaymentMethod: {}, formattedSelectedPaymentMethod: { title: '', }, - methodID: null, - selectedPaymentMethodType: null, + methodID: '', + selectedPaymentMethodType: '', }); const addPaymentMethodAnchorRef = useRef(null); - const paymentMethodButtonRef = useRef(null); + const paymentMethodButtonRef = useRef(null); const [anchorPosition, setAnchorPosition] = useState({ anchorPositionHorizontal: 0, anchorPositionVertical: 0, @@ -66,7 +88,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod const hasBankAccount = !_.isEmpty(bankAccountList) || !_.isEmpty(fundList); const hasWallet = !_.isEmpty(userWallet); - const hasActivatedWallet = _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], userWallet.tierName); + const hasActivatedWallet = _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], userWallet?.tierName); const hasAssignedCard = !_.isEmpty(cardList); const shouldShowEmptyState = !hasBankAccount && !hasWallet && !hasAssignedCard; @@ -77,7 +99,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod // In order to prevent a loop, only update state of the spinner if there is a change const showLoadingSpinner = isLoadingPaymentMethods || false; if (showLoadingSpinner !== shouldShowLoadingSpinner) { - setShouldShowLoadingSpinner(isLoadingPaymentMethods && !network.isOffline); + setShouldShowLoadingSpinner(!!isLoadingPaymentMethods && !network.isOffline); } }, [isLoadingPaymentMethods, network.isOffline, shouldShowLoadingSpinner]); @@ -121,8 +143,8 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod formattedSelectedPaymentMethod: { title: '', }, - methodID: null, - selectedPaymentMethodType: null, + methodID: '', + selectedPaymentMethodType: '', }); }, [setPaymentMethod]); @@ -135,7 +157,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod * @param {Boolean} isDefault * @param {String|Number} methodID */ - const paymentMethodPressed = (nativeEvent, accountType, account, isDefault, methodID) => { + const paymentMethodPressed = (nativeEvent?: GestureResponderEvent | KeyboardEvent, accountType?: string, account?: AccountData, isDefault?: boolean, methodID?: string | number) => { if (shouldShowAddPaymentMenu) { setShouldShowAddPaymentMenu(false); return; @@ -145,32 +167,34 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod setShouldShowDefaultDeleteMenu(false); return; } - paymentMethodButtonRef.current = nativeEvent.currentTarget; + paymentMethodButtonRef.current = nativeEvent?.currentTarget as HTMLElement; // The delete/default menu if (accountType) { - let formattedSelectedPaymentMethod; + let formattedSelectedPaymentMethod = { + title: '', + }; if (accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { formattedSelectedPaymentMethod = { - title: account.addressName, - icon: account.icon, + title: account?.addressName ?? '', + icon: account?.icon, description: PaymentUtils.getPaymentMethodDescription(accountType, account), type: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, }; } else if (accountType === CONST.PAYMENT_METHODS.DEBIT_CARD) { formattedSelectedPaymentMethod = { - title: account.addressName, - icon: account.icon, + title: account?.addressName ?? '', + icon: account?.icon, description: PaymentUtils.getPaymentMethodDescription(accountType, account), type: CONST.PAYMENT_METHODS.DEBIT_CARD, }; } setPaymentMethod({ - isSelectedPaymentMethodDefault: isDefault, - selectedPaymentMethod: account, + isSelectedPaymentMethodDefault: !!isDefault, + selectedPaymentMethod: account || {}, selectedPaymentMethodType: accountType, formattedSelectedPaymentMethod, - methodID, + methodID: methodID ?? '', }); setShouldShowDefaultDeleteMenu(true); setMenuPosition(); @@ -189,10 +213,8 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod /** * Navigate to the appropriate payment type addition screen - * - * @param {String} paymentType */ - const addPaymentMethodTypePressed = (paymentType) => { + const addPaymentMethodTypePressed = (paymentType: string) => { hideAddPaymentMenu(); if (paymentType === CONST.PAYMENT_METHODS.DEBIT_CARD) { @@ -220,14 +242,14 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod const makeDefaultPaymentMethod = useCallback(() => { const paymentCardList = fundList || {}; // Find the previous default payment method so we can revert if the MakeDefaultPaymentMethod command errors - const paymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList, paymentCardList, styles); + const paymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList || {}, paymentCardList, styles); - const previousPaymentMethod = _.find(paymentMethods, (method) => method.isDefault); + const previousPaymentMethod = _.find(paymentMethods, (method) => !!method.isDefault); const currentPaymentMethod = _.find(paymentMethods, (method) => method.methodID === paymentMethod.methodID); if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { - PaymentMethods.makeDefaultPaymentMethod(paymentMethod.selectedPaymentMethod.bankAccountID, null, previousPaymentMethod, currentPaymentMethod); + PaymentMethods.makeDefaultPaymentMethod(paymentMethod.selectedPaymentMethod.bankAccountID || null, null, previousPaymentMethod, currentPaymentMethod); } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD) { - PaymentMethods.makeDefaultPaymentMethod(null, paymentMethod.selectedPaymentMethod.fundID, previousPaymentMethod, currentPaymentMethod); + PaymentMethods.makeDefaultPaymentMethod(null, paymentMethod.selectedPaymentMethod.fundID || null, previousPaymentMethod, currentPaymentMethod); } }, [ paymentMethod.methodID, @@ -240,19 +262,19 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod ]); const deletePaymentMethod = useCallback(() => { - if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { - BankAccounts.deletePaymentBankAccount(paymentMethod.selectedPaymentMethod.bankAccountID); - } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD) { - PaymentMethods.deletePaymentCard(paymentMethod.selectedPaymentMethod.fundID); + const bankAccountID = paymentMethod.selectedPaymentMethod.bankAccountID; + const fundID = paymentMethod.selectedPaymentMethod.fundID; + if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && bankAccountID) { + BankAccounts.deletePaymentBankAccount(bankAccountID); + } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD && fundID) { + PaymentMethods.deletePaymentCard(fundID); } }, [paymentMethod.selectedPaymentMethod.bankAccountID, paymentMethod.selectedPaymentMethod.fundID, paymentMethod.selectedPaymentMethodType]); /** * Navigate to the appropriate page after completing the KYC flow, depending on what initiated it - * - * @param {String} source */ - const navigateToWalletOrTransferBalancePage = (source) => { + const navigateToWalletOrTransferBalancePage = (source: string) => { Navigation.navigate(source === CONST.KYC_WALL_SOURCE.ENABLE_WALLET ? ROUTES.SETTINGS_WALLET : ROUTES.SETTINGS_WALLET_TRANSFER_BALANCE); }; @@ -306,9 +328,9 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod // We should reset selected payment method state values and close corresponding modals if the selected payment method is deleted let shouldResetPaymentMethodData = false; - if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && _.isEmpty(bankAccountList[paymentMethod.methodID])) { + if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && _.isEmpty(bankAccountList?.[paymentMethod.methodID])) { shouldResetPaymentMethodData = true; - } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD && _.isEmpty(fundList[paymentMethod.methodID])) { + } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD && _.isEmpty(fundList?.[paymentMethod.methodID])) { shouldResetPaymentMethodData = true; } if (shouldResetPaymentMethodData) { @@ -344,7 +366,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod style={styles.flex1} contentContainerStyle={styles.flex1} onClose={PaymentMethods.clearWalletError} - errors={userWallet.errors} + errors={userWallet?.errors} errorRowStyles={[styles.ph6]} > {hasWallet && ( @@ -363,7 +385,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod ) : ( navigateToWalletOrTransferBalancePage(source)} - onSelectPaymentMethod={(selectedPaymentMethod) => { + onSuccessfulKYC={(_iouPaymentType: string, source: string) => navigateToWalletOrTransferBalancePage(source)} + onSelectPaymentMethod={(selectedPaymentMethod: string) => { if (hasActivatedWallet || selectedPaymentMethod !== CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { return; } @@ -388,7 +410,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod source={hasActivatedWallet ? CONST.KYC_WALL_SOURCE.TRANSFER_BALANCE : CONST.KYC_WALL_SOURCE.ENABLE_WALLET} shouldIncludeDebitCard={hasActivatedWallet} > - {(triggerKYCFlow, buttonRef) => { + {(triggerKYCFlow: (e: GestureResponderEvent | KeyboardEvent | undefined) => void, buttonRef: Ref) => { if (shouldShowLoadingSpinner) { return null; } @@ -463,7 +485,6 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod shouldEnableScroll={false} onPress={paymentMethodPressed} style={styles.mt5} - isAddPaymentMenuActive={shouldShowAddPaymentMenu} actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} activePaymentMethodID={shouldShowDefaultDeleteMenu ? getSelectedPaymentMethodID() : ''} buttonRef={addPaymentMethodAnchorRef} @@ -480,7 +501,6 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod shouldShowAddPaymentMethodButton={false} shouldShowEmptyListMessage={false} onPress={paymentMethodPressed} - isAddPaymentMenuActive={shouldShowAddPaymentMenu} actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} activePaymentMethodID={shouldShowDefaultDeleteMenu ? getSelectedPaymentMethodID() : ''} buttonRef={addPaymentMethodAnchorRef} @@ -502,7 +522,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod anchorRef={paymentMethodButtonRef} > {!showConfirmDeleteModal && ( - + {isPopoverBottomMount && ( addPaymentMethodTypePressed(method)} + onItemSelected={(method: string) => addPaymentMethodTypePressed(method)} anchorRef={addPaymentMethodAnchorRef} /> ); } -WalletPage.propTypes = propTypes; -WalletPage.defaultProps = defaultProps; WalletPage.displayName = 'WalletPage'; -export default compose( - withNetwork(), - withOnyx({ - cardList: { - key: ONYXKEYS.CARD_LIST, - }, - walletTransfer: { - key: ONYXKEYS.WALLET_TRANSFER, - }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, - bankAccountList: { - key: ONYXKEYS.BANK_ACCOUNT_LIST, - }, - fundList: { - key: ONYXKEYS.FUND_LIST, - }, - walletTerms: { - key: ONYXKEYS.WALLET_TERMS, - }, - isLoadingPaymentMethods: { - key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS, - }, - }), -)(WalletPage); +export default withOnyx({ + cardList: { + key: ONYXKEYS.CARD_LIST, + }, + walletTransfer: { + key: ONYXKEYS.WALLET_TRANSFER, + }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, + bankAccountList: { + key: ONYXKEYS.BANK_ACCOUNT_LIST, + }, + fundList: { + key: ONYXKEYS.FUND_LIST, + }, + walletTerms: { + key: ONYXKEYS.WALLET_TERMS, + }, + isLoadingPaymentMethods: { + key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS, + }, +})(WalletPage); diff --git a/src/pages/settings/Wallet/WalletPage/index.native.js b/src/pages/settings/Wallet/WalletPage/index.native.tsx similarity index 100% rename from src/pages/settings/Wallet/WalletPage/index.native.js rename to src/pages/settings/Wallet/WalletPage/index.native.tsx diff --git a/src/pages/settings/Wallet/WalletPage/index.js b/src/pages/settings/Wallet/WalletPage/index.tsx similarity index 100% rename from src/pages/settings/Wallet/WalletPage/index.js rename to src/pages/settings/Wallet/WalletPage/index.tsx diff --git a/src/pages/settings/Wallet/WalletPage/types.ts b/src/pages/settings/Wallet/WalletPage/types.ts new file mode 100644 index 000000000000..731587f86f39 --- /dev/null +++ b/src/pages/settings/Wallet/WalletPage/types.ts @@ -0,0 +1,31 @@ +import {OnyxEntry} from 'react-native-onyx'; +import {BankAccountList, Beta, Card, CardList, Fund, FundList, Network, UserWallet, WalletTerms, WalletTransfer} from '@src/types/onyx'; + +type WalletPageOnyxProps = { + /** Wallet balance transfer props */ + walletTransfer: OnyxEntry; + + /** The user's wallet account */ + userWallet: OnyxEntry; + + /** List of bank accounts */ + bankAccountList: OnyxEntry; + + /** List of user's cards */ + fundList: OnyxEntry; + + /** Information about the user accepting the terms for payments */ + walletTerms: OnyxEntry; + + cardList: OnyxEntry; + + /** Are we loading payment methods? */ + isLoadingPaymentMethods: OnyxEntry; +}; + +type WalletPageProps = WalletPageOnyxProps & { + /** Listen for window resize event on web and desktop. */ + shouldListenForResize?: boolean; +}; + +export type {WalletPageOnyxProps, WalletPageProps}; diff --git a/src/pages/settings/Wallet/WalletPage/walletPagePropTypes.js b/src/pages/settings/Wallet/WalletPage/walletPagePropTypes.js deleted file mode 100644 index 23bdfe99b086..000000000000 --- a/src/pages/settings/Wallet/WalletPage/walletPagePropTypes.js +++ /dev/null @@ -1,52 +0,0 @@ -import PropTypes from 'prop-types'; -import bankAccountPropTypes from '@components/bankAccountPropTypes'; -import cardPropTypes from '@components/cardPropTypes'; -import networkPropTypes from '@components/networkPropTypes'; -import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; -import walletTermsPropTypes from '@pages/EnablePayments/walletTermsPropTypes'; -import walletTransferPropTypes from '@pages/settings/Wallet/walletTransferPropTypes'; - -const propTypes = { - /** Wallet balance transfer props */ - walletTransfer: walletTransferPropTypes, - - /** List of betas available to current user */ - betas: PropTypes.arrayOf(PropTypes.string), - - /** Are we loading payment methods? */ - isLoadingPaymentMethods: PropTypes.bool, - - /** Listen for window resize event on web and desktop. */ - shouldListenForResize: PropTypes.bool, - - /** The user's wallet account */ - userWallet: userWalletPropTypes, - - /** Information about the network */ - network: networkPropTypes.isRequired, - - /** List of bank accounts */ - bankAccountList: PropTypes.objectOf(bankAccountPropTypes), - - /** List of user's cards */ - fundList: PropTypes.objectOf(cardPropTypes), - - /** Information about the user accepting the terms for payments */ - walletTerms: walletTermsPropTypes, -}; - -const defaultProps = { - walletTransfer: { - shouldShowSuccess: false, - }, - betas: [], - isLoadingPaymentMethods: true, - shouldListenForResize: false, - userWallet: {}, - bankAccountList: {}, - cardList: {}, - fundList: null, - walletTerms: {}, -}; - -export {propTypes, defaultProps}; diff --git a/src/types/onyx/AccountData.ts b/src/types/onyx/AccountData.ts index 88511eec6864..124a006ff57c 100644 --- a/src/types/onyx/AccountData.ts +++ b/src/types/onyx/AccountData.ts @@ -51,6 +51,8 @@ type AccountData = { /** Any error message to show */ errors?: OnyxCommon.Errors; + + fundID?: number; }; export default AccountData; diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index e3b025ff5a2f..68acd88aa120 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -33,5 +33,7 @@ type TCardDetails = { }; }; +type CardList = Record; + export default Card; -export type {TCardDetails}; +export type {TCardDetails, CardList}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 7bd9c321be5e..d881379f7e05 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -5,6 +5,7 @@ import type BankAccount from './BankAccount'; import type Beta from './Beta'; import type BlockedFromConcierge from './BlockedFromConcierge'; import type Card from './Card'; +import type {CardList} from './Card'; import type Credentials from './Credentials'; import type Currency from './Currency'; import type CustomStatusDraft from './CustomStatusDraft'; @@ -74,6 +75,7 @@ export type { Beta, BlockedFromConcierge, Card, + CardList, Credentials, Currency, CustomStatusDraft, From dba03e1ecc55504615c99ea610e83d0052a0e73b Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 11 Jan 2024 15:19:06 +0700 Subject: [PATCH 039/635] fix type error --- src/components/AddPaymentMethodMenu.js | 12 +- src/components/KYCWall/index.js | 5 +- src/components/Modal/index.android.tsx | 3 +- src/components/Modal/index.ios.tsx | 3 +- src/components/Modal/index.tsx | 3 +- src/components/Modal/types.ts | 74 ++++++------ src/components/Popover/index.tsx | 12 +- src/components/Popover/types.ts | 13 +-- src/components/PopoverProvider/types.ts | 2 +- src/components/PopoverWithoutOverlay/types.ts | 2 +- src/libs/actions/PaymentMethods.ts | 8 +- .../settings/Wallet/PaymentMethodList.tsx | 92 +++++++-------- .../settings/Wallet/WalletEmptyState.tsx | 1 - .../Wallet/WalletPage/CardDetails.tsx | 10 +- .../settings/Wallet/WalletPage/WalletPage.tsx | 107 +++++++++--------- src/pages/settings/Wallet/WalletPage/types.ts | 4 +- 16 files changed, 166 insertions(+), 185 deletions(-) diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index a936b0efdfa1..033fb8765b6f 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -1,19 +1,17 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; -import compose from '@libs/compose'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import iouReportPropTypes from '@pages/iouReportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { withOnyx } from 'react-native-onyx'; +import _ from 'underscore'; import * as Expensicons from './Icon/Expensicons'; import PopoverMenu from './PopoverMenu'; import refPropTypes from './refPropTypes'; -import withWindowDimensions from './withWindowDimensions'; const propTypes = { /** Should the component be visible? */ diff --git a/src/components/KYCWall/index.js b/src/components/KYCWall/index.js index 87f1496a494a..e3a0d5e82cdc 100644 --- a/src/components/KYCWall/index.js +++ b/src/components/KYCWall/index.js @@ -8,8 +8,9 @@ function KYCWall({children, ...props}) { // eslint-disable-next-line react/jsx-props-no-spreading {...props} shouldListenForResize - children={children} - /> + > + {children} + ); } diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx index 4d7ae128a114..86a1fd272185 100644 --- a/src/components/Modal/index.android.tsx +++ b/src/components/Modal/index.android.tsx @@ -1,6 +1,5 @@ import React from 'react'; import {AppState} from 'react-native'; -import withWindowDimensions from '@components/withWindowDimensions'; import ComposerFocusManager from '@libs/ComposerFocusManager'; import BaseModal from './BaseModal'; import type BaseModalProps from './types'; @@ -28,4 +27,4 @@ function Modal({useNativeDriver = true, ...rest}: BaseModalProps) { } Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default Modal; diff --git a/src/components/Modal/index.ios.tsx b/src/components/Modal/index.ios.tsx index cbe58a071d7d..b26ba6cd0f89 100644 --- a/src/components/Modal/index.ios.tsx +++ b/src/components/Modal/index.ios.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import withWindowDimensions from '@components/withWindowDimensions'; import BaseModal from './BaseModal'; import type BaseModalProps from './types'; @@ -15,4 +14,4 @@ function Modal({children, ...rest}: BaseModalProps) { } Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default Modal; diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx index 56f3c76a8879..71c0fe47ffca 100644 --- a/src/components/Modal/index.tsx +++ b/src/components/Modal/index.tsx @@ -1,5 +1,4 @@ import React, {useState} from 'react'; -import withWindowDimensions from '@components/withWindowDimensions'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import StatusBar from '@libs/StatusBar'; @@ -55,4 +54,4 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = ( } Modal.displayName = 'Modal'; -export default withWindowDimensions(Modal); +export default Modal; diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 0fed37ffea8b..caba945cad30 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -1,7 +1,6 @@ import type {ViewStyle} from 'react-native'; import type {ModalProps} from 'react-native-modal'; import type {ValueOf} from 'type-fest'; -import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import type CONST from '@src/CONST'; type PopoverAnchorPosition = { @@ -11,57 +10,56 @@ type PopoverAnchorPosition = { left?: number; }; -type BaseModalProps = WindowDimensionsProps & - Partial & { - /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ - fullscreen?: boolean; +type BaseModalProps = Partial & { + /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ + fullscreen?: boolean; - /** Should we close modal on outside click */ - shouldCloseOnOutsideClick?: boolean; + /** Should we close modal on outside click */ + shouldCloseOnOutsideClick?: boolean; - /** Should we announce the Modal visibility changes? */ - shouldSetModalVisibility?: boolean; + /** Should we announce the Modal visibility changes? */ + shouldSetModalVisibility?: boolean; - /** Callback method fired when the user requests to close the modal */ - onClose: (ref?: React.RefObject) => void; + /** Callback method fired when the user requests to close the modal */ + onClose: (ref?: React.RefObject) => void; - /** State that determines whether to display the modal or not */ - isVisible: boolean; + /** State that determines whether to display the modal or not */ + isVisible: boolean; - /** Callback method fired when the user requests to submit the modal content. */ - onSubmit?: () => void; + /** Callback method fired when the user requests to submit the modal content. */ + onSubmit?: () => void; - /** Callback method fired when the modal is hidden */ - onModalHide?: () => void; + /** Callback method fired when the modal is hidden */ + onModalHide?: () => void; - /** Callback method fired when the modal is shown */ - onModalShow?: () => void; + /** Callback method fired when the modal is shown */ + onModalShow?: () => void; - /** Style of modal to display */ - type?: ValueOf; + /** Style of modal to display */ + type?: ValueOf; - /** The anchor position of a popover modal. Has no effect on other modal types. */ - popoverAnchorPosition?: PopoverAnchorPosition; + /** The anchor position of a popover modal. Has no effect on other modal types. */ + popoverAnchorPosition?: PopoverAnchorPosition; - outerStyle?: ViewStyle; + outerStyle?: ViewStyle; - /** Whether the modal should go under the system statusbar */ - statusBarTranslucent?: boolean; + /** Whether the modal should go under the system statusbar */ + statusBarTranslucent?: boolean; - /** Whether the modal should avoid the keyboard */ - avoidKeyboard?: boolean; + /** Whether the modal should avoid the keyboard */ + avoidKeyboard?: boolean; - /** Modal container styles */ - innerContainerStyle?: ViewStyle; + /** Modal container styles */ + innerContainerStyle?: ViewStyle; - /** - * Whether the modal should hide its content while animating. On iOS, set to true - * if `useNativeDriver` is also true, to avoid flashes in the UI. - * - * See: https://github.com/react-native-modal/react-native-modal/pull/116 - * */ - hideModalContentWhileAnimating?: boolean; - }; + /** + * Whether the modal should hide its content while animating. On iOS, set to true + * if `useNativeDriver` is also true, to avoid flashes in the UI. + * + * See: https://github.com/react-native-modal/react-native-modal/pull/116 + * */ + hideModalContentWhileAnimating?: boolean; +}; export default BaseModalProps; export type {PopoverAnchorPosition}; diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index 762e79fab63c..e1cd18ba4767 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -3,32 +3,32 @@ import {createPortal} from 'react-dom'; import Modal from '@components/Modal'; import {PopoverContext} from '@components/PopoverProvider'; import PopoverWithoutOverlay from '@components/PopoverWithoutOverlay'; -import withWindowDimensions from '@components/withWindowDimensions'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import CONST from '@src/CONST'; -import type {PopoverWithWindowDimensionsProps} from './types'; +import type {PopoverProps} from './types'; /* * This is a convenience wrapper around the Modal component for a responsive Popover. * On small screen widths, it uses BottomDocked modal type, and a Popover type on wide screen widths. */ -function Popover(props: PopoverWithWindowDimensionsProps) { +function Popover(props: PopoverProps) { const { isVisible, onClose, - isSmallScreenWidth, fullscreen, animationInTiming = CONST.ANIMATED_TRANSITION, onLayout, animationOutTiming, disableAnimation = true, - withoutOverlay, + withoutOverlay = false, anchorPosition = {}, anchorRef = () => {}, animationIn = 'fadeIn', animationOut = 'fadeOut', } = props; + const {isSmallScreenWidth} = useWindowDimensions(); const withoutOverlayRef = useRef(null); const {close, popover} = React.useContext(PopoverContext); @@ -106,4 +106,4 @@ function Popover(props: PopoverWithWindowDimensionsProps) { Popover.displayName = 'Popover'; -export default withWindowDimensions(Popover); +export default Popover; diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index 3d1f95822e6a..551756ed706c 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -1,7 +1,6 @@ import type {ValueOf} from 'type-fest'; import type {PopoverAnchorPosition} from '@components/Modal/types'; import type BaseModalProps from '@components/Modal/types'; -import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import type CONST from '@src/CONST'; type AnchorAlignment = { @@ -22,16 +21,16 @@ type PopoverProps = BaseModalProps & { anchorPosition?: PopoverAnchorPosition; /** The anchor alignment of the popover */ - anchorAlignment: AnchorAlignment; + anchorAlignment?: AnchorAlignment; /** The anchor ref of the popover */ - anchorRef: React.RefObject; + anchorRef?: React.RefObject; /** Whether disable the animations */ - disableAnimation: boolean; + disableAnimation?: boolean; /** Whether we don't want to show overlay */ - withoutOverlay: boolean; + withoutOverlay?: boolean; /** The dimensions of the popover */ popoverDimensions?: PopoverDimensions; @@ -46,6 +45,4 @@ type PopoverProps = BaseModalProps & { children: React.ReactNode; }; -type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps; - -export type {PopoverProps, PopoverWithWindowDimensionsProps, AnchorAlignment}; +export type {PopoverProps, AnchorAlignment}; diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts index ffd0087cd5ff..acc01fd74427 100644 --- a/src/components/PopoverProvider/types.ts +++ b/src/components/PopoverProvider/types.ts @@ -12,7 +12,7 @@ type PopoverContextValue = { type AnchorRef = { ref: React.RefObject; close: (anchorRef?: React.RefObject) => void; - anchorRef: React.RefObject; + anchorRef?: React.RefObject; onOpenCallback?: () => void; onCloseCallback?: () => void; }; diff --git a/src/components/PopoverWithoutOverlay/types.ts b/src/components/PopoverWithoutOverlay/types.ts index ff4f73fd4114..fd42bf7d8e42 100644 --- a/src/components/PopoverWithoutOverlay/types.ts +++ b/src/components/PopoverWithoutOverlay/types.ts @@ -13,7 +13,7 @@ type PopoverWithoutOverlayProps = ChildrenProps & }; /** The anchor ref of the popover */ - anchorRef: React.RefObject; + anchorRef?: React.RefObject; /** A react-native-animatable animation timing for the modal display animation */ animationInTiming?: number; diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index 6614d3516253..c152a968f030 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -83,7 +83,7 @@ function getMakeDefaultPaymentOnyxData( onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.USER_WALLET, value: { - walletLinkedAccountID: bankAccountID || fundID, + walletLinkedAccountID: bankAccountID ?? fundID, walletLinkedAccountType: bankAccountID ? CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT : CONST.PAYMENT_METHODS.DEBIT_CARD, // Only clear the error if this is optimistic data. If this is failure data, we do not want to clear the error that came from the server. errors: null, @@ -93,7 +93,7 @@ function getMakeDefaultPaymentOnyxData( onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.USER_WALLET, value: { - walletLinkedAccountID: bankAccountID || fundID, + walletLinkedAccountID: bankAccountID ?? fundID, walletLinkedAccountType: bankAccountID ? CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT : CONST.PAYMENT_METHODS.DEBIT_CARD, }, }, @@ -320,7 +320,7 @@ type PaymentListKey = typeof ONYXKEYS.BANK_ACCOUNT_LIST | typeof ONYXKEYS.FUND_L * @param paymentListKey The onyx key for the provided payment method * @param paymentMethodID */ -function clearDeletePaymentMethodError(paymentListKey: PaymentListKey, paymentMethodID: string) { +function clearDeletePaymentMethodError(paymentListKey: PaymentListKey, paymentMethodID: number) { Onyx.merge(paymentListKey, { [paymentMethodID]: { pendingAction: null, @@ -334,7 +334,7 @@ function clearDeletePaymentMethodError(paymentListKey: PaymentListKey, paymentMe * @param paymentListKey The onyx key for the provided payment method * @param paymentMethodID */ -function clearAddPaymentMethodError(paymentListKey: PaymentListKey, paymentMethodID: string) { +function clearAddPaymentMethodError(paymentListKey: PaymentListKey, paymentMethodID: number) { Onyx.merge(paymentListKey, { [paymentMethodID]: null, }); diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 9cceaa60c10c..e7c6ba37ed9f 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -1,17 +1,7 @@ -import {FlashList} from '@shopify/flash-list'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {ReactElement, Ref, useCallback, useMemo} from 'react'; -import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; -import {TupleToUnion, ValueOf} from 'type-fest'; -import _ from 'underscore'; -import bankAccountPropTypes from '@components/bankAccountPropTypes'; import Button from '@components/Button'; -import cardPropTypes from '@components/cardPropTypes'; import FormAlertWrapper from '@components/FormAlertWrapper'; import getBankIcon from '@components/Icon/BankIcons'; -import {BankName} from '@components/Icon/BankIconsUtils'; +import type { BankName } from '@components/Icon/BankIconsUtils'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -24,17 +14,26 @@ import * as CardUtils from '@libs/CardUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as PaymentUtils from '@libs/PaymentUtils'; -import stylePropTypes from '@styles/stylePropTypes'; -import variables from '@styles/variables'; -import * as PaymentMethods from '@userActions/PaymentMethods'; +import { FlashList } from '@shopify/flash-list'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {AccountData, BankAccountList, Card, CardList, FundList, UserWallet} from '@src/types/onyx'; -import PaymentMethod from '@src/types/onyx/PaymentMethod'; +import type { AccountData, BankAccountList, CardList, FundList } from '@src/types/onyx'; +import type PaymentMethod from '@src/types/onyx/PaymentMethod'; +import type IconAsset from '@src/types/utils/IconAsset'; +import variables from '@styles/variables'; +import * as PaymentMethods from '@userActions/PaymentMethods'; +import type { ReactElement, Ref } from 'react'; +import React, { useCallback, useMemo } from 'react'; +import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native'; +import { View } from 'react-native'; +import type { OnyxEntry } from 'react-native-onyx'; +import { withOnyx } from 'react-native-onyx'; +import type { TupleToUnion, ValueOf } from 'type-fest'; +import _ from 'lodash'; +import type { RenderSuggestionMenuItemProps } from '@components/AutoCompleteSuggestions/types'; const FILTER_TYPES = [CONST.PAYMENT_METHODS.DEBIT_CARD, CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, ''] as const; -const ACTION_PAYMENT_METHOD_TYPES = [...Object.values(CONST.PAYMENT_METHODS), ''] as const; type PaymentMethodListOnyxProps = { /** List of bank accounts */ @@ -48,14 +47,11 @@ type PaymentMethodListOnyxProps = { /** Are we loading payment methods? */ isLoadingPaymentMethods: OnyxEntry; - - /** User wallet props */ - userWallet: OnyxEntry; }; type PaymentMethodListProps = PaymentMethodListOnyxProps & { /** Type of active/highlighted payment method */ - actionPaymentMethodType?: TupleToUnion; + actionPaymentMethodType?: string; /** ID of active/highlighted payment method */ activePaymentMethodID?: string | number; @@ -96,7 +92,14 @@ type PaymentMethodListProps = PaymentMethodListOnyxProps & { shouldShowEmptyListMessage?: boolean; /** What to do when a menu item is pressed */ - onPress: (event?: GestureResponderEvent | KeyboardEvent, accountType?: string, accountData?: AccountData, isDefault?: boolean, methodID?: number) => void | Promise; + onPress: ( + event?: GestureResponderEvent | KeyboardEvent, + accountType?: string, + accountData?: AccountData, + icon?: IconAsset, + isDefault?: boolean, + methodID?: number, + ) => void; }; type PaymentMethodItem = PaymentMethod & { @@ -111,7 +114,7 @@ type PaymentMethodItem = PaymentMethod & { function dismissError(item: PaymentMethod) { const isBankAccount = item.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT; const paymentList = isBankAccount ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.FUND_LIST; - const paymentID = isBankAccount ? lodashGet(item, ['accountData', 'bankAccountID'], '') : lodashGet(item, ['accountData', 'fundID'], ''); + const paymentID = isBankAccount ? item.accountData?.bankAccountID ?? '' : item.accountData?.fundID ?? ''; if (!paymentID) { Log.info('Unable to clear payment method error: ', undefined, item); @@ -136,9 +139,8 @@ function shouldShowDefaultBadge(filteredPaymentMethods: PaymentMethod[], isDefau return false; } - const defaultablePaymentMethodCount = _.filter( - filteredPaymentMethods, - (method) => method.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT || method.accountType === CONST.PAYMENT_METHODS.DEBIT_CARD, + const defaultablePaymentMethodCount = filteredPaymentMethods.filter( + (method) => method.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT ?? method.accountType === CONST.PAYMENT_METHODS.DEBIT_CARD, ).length; return defaultablePaymentMethodCount > 1; } @@ -148,7 +150,7 @@ function isPaymentMethodActive(actionPaymentMethodType: string, activePaymentMet } function keyExtractor(item: PaymentMethod) { - return item.key || ''; + return item.key ?? ''; } function PaymentMethodList({ @@ -181,13 +183,13 @@ function PaymentMethodList({ if (shouldShowAssignedCards) { const assignedCards = _.chain(cardList) // Filter by physical, active cards associated with a domain - .filter((card) => !card.isVirtual && card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0)) + .filter((card) => !card.isVirtual && !!card.domainName && CONST.EXPENSIFY_CARD.ACTIVE_STATES.includes(card.state ?? 0)) .sortBy((card) => !CardUtils.isExpensifyCard(card.cardID)) .value(); - const numberPhysicalExpensifyCards = _.filter(assignedCards, (card) => CardUtils.isExpensifyCard(card.cardID)).length; + const numberPhysicalExpensifyCards = assignedCards.filter((card) => CardUtils.isExpensifyCard(card.cardID)).length; - return _.map(assignedCards, (card) => { + return assignedCards.map((card) => { const isExpensifyCard = CardUtils.isExpensifyCard(card.cardID); const icon = getBankIcon({bankName: card.bank as BankName, isCard: true, styles}); @@ -202,34 +204,33 @@ function PaymentMethodList({ interactive: isExpensifyCard, canDismissError: isExpensifyCard, errors: card.errors, - brickRoadIndicator: card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL ? 'error' : null, + brickRoadIndicator: card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN ?? card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL ? 'error' : null, ...icon, }; }); } - const paymentCardList = fundList || {}; + const paymentCardList = fundList ?? {}; // Hide any billing cards that are not P2P debit cards for now because you cannot make them your default method, or delete them - const filteredCardList = _.filter(paymentCardList, (card) => !!card.accountData?.additionalData?.isP2PDebitCard); + const filteredCardList = Object.values(paymentCardList).filter((card) => !!card.accountData?.additionalData?.isP2PDebitCard); let combinedPaymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, filteredCardList, styles); if (!_.isEmpty(filterType)) { - combinedPaymentMethods = _.filter(combinedPaymentMethods, (paymentMethod) => paymentMethod.accountType === filterType); + combinedPaymentMethods = combinedPaymentMethods.filter((paymentMethod) => paymentMethod.accountType === filterType); } if (!isOffline) { - combinedPaymentMethods = _.filter( - combinedPaymentMethods, - (paymentMethod) => paymentMethod.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(paymentMethod.errors), + combinedPaymentMethods = combinedPaymentMethods.filter( + (paymentMethod) => paymentMethod.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ?? !_.isEmpty(paymentMethod.errors), ); } - combinedPaymentMethods = _.map(combinedPaymentMethods, (paymentMethod) => { + combinedPaymentMethods = combinedPaymentMethods.map((paymentMethod) => { const isMethodActive = isPaymentMethodActive(actionPaymentMethodType, activePaymentMethodID, paymentMethod); return { ...paymentMethod, - onPress: (e: GestureResponderEvent) => onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.isDefault, paymentMethod.methodID), + onPress: (e: GestureResponderEvent) => onPress(e, paymentMethod.accountType, paymentMethod.accountData, paymentMethod.icon, paymentMethod.isDefault, paymentMethod.methodID), wrapperStyle: isMethodActive ? [StyleUtils.getButtonBackgroundColorStyle(CONST.BUTTON_STATES.PRESSED)] : null, disabled: paymentMethod.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, }; @@ -240,8 +241,6 @@ function PaymentMethodList({ /** * Render placeholder when there are no payments methods - * - * @return {React.Component} */ const renderListEmptyComponent = () => {translate('paymentMethodList.addFirstPaymentMethod')}; @@ -263,7 +262,7 @@ function PaymentMethodList({ * Create a menuItem for each passed paymentMethod */ const renderItem = useCallback( - ({item}: {item: PaymentMethodItem}) => ( + ({item}: RenderSuggestionMenuItemProps) => ( dismissError(item)} pendingAction={item.pendingAction} @@ -278,8 +277,8 @@ function PaymentMethodList({ icon={item.icon} disabled={item.disabled} displayInDefaultIconColor - iconHeight={item.iconHeight || item.iconSize} - iconWidth={item.iconWidth || item.iconSize} + iconHeight={item.iconHeight ?? item.iconSize} + iconWidth={item.iconWidth ?? item.iconSize} iconStyles={item.iconStyles} badgeText={shouldShowDefaultBadge(filteredPaymentMethods, item.isDefault) ? translate('paymentMethodList.defaultPaymentMethod') : undefined} wrapperStyle={styles.paymentMethod} @@ -317,7 +316,7 @@ function PaymentMethodList({ text={translate('paymentMethodList.addPaymentMethod')} icon={Expensicons.CreditCard} onPress={onPress} - isDisabled={isLoadingPaymentMethods || isFormOffline} + isDisabled={isLoadingPaymentMethods ?? isFormOffline} style={[styles.mh4, styles.buttonCTA]} iconStyles={[styles.buttonCTAIcon]} key="addPaymentMethodButton" @@ -348,7 +347,4 @@ export default withOnyx({ isLoadingPaymentMethods: { key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS, }, - userWallet: { - key: ONYXKEYS.USER_WALLET, - }, })(PaymentMethodList); diff --git a/src/pages/settings/Wallet/WalletEmptyState.tsx b/src/pages/settings/Wallet/WalletEmptyState.tsx index 4ef9488368cb..4060a3695b92 100644 --- a/src/pages/settings/Wallet/WalletEmptyState.tsx +++ b/src/pages/settings/Wallet/WalletEmptyState.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import Button from '@components/Button'; import FeatureList from '@components/FeatureList'; diff --git a/src/pages/settings/Wallet/WalletPage/CardDetails.tsx b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx index 09a7d937053b..a21172da2832 100644 --- a/src/pages/settings/Wallet/WalletPage/CardDetails.tsx +++ b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import { withOnyx} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import PressableWithDelayToggle from '@components/Pressable/PressableWithDelayToggle'; @@ -14,7 +14,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import {PrivatePersonalDetails} from '@src/types/onyx'; +import type {PrivatePersonalDetails} from '@src/types/onyx'; const defaultPrivatePersonalDetails = { address: { @@ -46,7 +46,7 @@ type CardDetailsProps = CardDetailsOnyxProps & { domain: string; }; -function CardDetails({pan = '', expiration = '', cvv = '', privatePersonalDetails = defaultPrivatePersonalDetails, domain}: CardDetailsProps) { +function CardDetails({pan = '', expiration = '', cvv = '', privatePersonalDetails, domain}: CardDetailsProps) { const styles = useThemeStyles(); usePrivatePersonalDetails(); const {translate} = useLocalize(); @@ -87,7 +87,7 @@ function CardDetails({pan = '', expiration = '', cvv = '', privatePersonalDetail /> { // In order to prevent a loop, only update state of the spinner if there is a change - const showLoadingSpinner = isLoadingPaymentMethods || false; + const showLoadingSpinner = isLoadingPaymentMethods ?? false; if (showLoadingSpinner !== shouldShowLoadingSpinner) { setShouldShowLoadingSpinner(!!isLoadingPaymentMethods && !network.isOffline); } @@ -107,8 +103,6 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi /** * Set position of the payment menu - * - * @param {Object} position */ const setMenuPosition = useCallback(() => { if (!paymentMethodButtonRef.current) { @@ -150,14 +144,15 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi /** * Display the delete/default menu, or the add payment method menu - * - * @param {Object} nativeEvent - * @param {String} accountType - * @param {String} account - * @param {Boolean} isDefault - * @param {String|Number} methodID */ - const paymentMethodPressed = (nativeEvent?: GestureResponderEvent | KeyboardEvent, accountType?: string, account?: AccountData, isDefault?: boolean, methodID?: string | number) => { + const paymentMethodPressed = ( + nativeEvent?: GestureResponderEvent | KeyboardEvent, + accountType?: string, + account?: AccountData, + icon?: IconAsset, + isDefault?: boolean, + methodID?: string | number, + ) => { if (shouldShowAddPaymentMenu) { setShouldShowAddPaymentMenu(false); return; @@ -171,27 +166,27 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi // The delete/default menu if (accountType) { - let formattedSelectedPaymentMethod = { + let formattedSelectedPaymentMethod: FormattedSelectedPaymentMethod = { title: '', }; if (accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { formattedSelectedPaymentMethod = { title: account?.addressName ?? '', - icon: account?.icon, + icon, description: PaymentUtils.getPaymentMethodDescription(accountType, account), type: CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, }; } else if (accountType === CONST.PAYMENT_METHODS.DEBIT_CARD) { formattedSelectedPaymentMethod = { title: account?.addressName ?? '', - icon: account?.icon, + icon, description: PaymentUtils.getPaymentMethodDescription(accountType, account), type: CONST.PAYMENT_METHODS.DEBIT_CARD, }; } setPaymentMethod({ isSelectedPaymentMethodDefault: !!isDefault, - selectedPaymentMethod: account || {}, + selectedPaymentMethod: account ?? {}, selectedPaymentMethodType: accountType, formattedSelectedPaymentMethod, methodID: methodID ?? '', @@ -222,7 +217,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi return; } - if (paymentType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT || paymentType === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { + if (paymentType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT ?? paymentType === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) { BankAccounts.openPersonalBankAccountSetupView(); return; } @@ -232,7 +227,6 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi /** * Hide the default / delete modal - * @param {boolean} shouldClearSelectedData - Clear selected payment method data if true */ const hideDefaultDeleteMenu = useCallback(() => { setShouldShowDefaultDeleteMenu(false); @@ -240,16 +234,16 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi }, [setShouldShowDefaultDeleteMenu, setShowConfirmDeleteModal]); const makeDefaultPaymentMethod = useCallback(() => { - const paymentCardList = fundList || {}; + const paymentCardList = fundList ?? {}; // Find the previous default payment method so we can revert if the MakeDefaultPaymentMethod command errors - const paymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList || {}, paymentCardList, styles); + const paymentMethods = PaymentUtils.formatPaymentMethods(bankAccountList ?? {}, paymentCardList, styles); - const previousPaymentMethod = _.find(paymentMethods, (method) => !!method.isDefault); - const currentPaymentMethod = _.find(paymentMethods, (method) => method.methodID === paymentMethod.methodID); + const previousPaymentMethod = paymentMethods.find((method) => !!method.isDefault); + const currentPaymentMethod = paymentMethods.find((method) => method.methodID === paymentMethod.methodID); if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { - PaymentMethods.makeDefaultPaymentMethod(paymentMethod.selectedPaymentMethod.bankAccountID || null, null, previousPaymentMethod, currentPaymentMethod); + PaymentMethods.makeDefaultPaymentMethod(paymentMethod.selectedPaymentMethod.bankAccountID ?? null, null, previousPaymentMethod, currentPaymentMethod); } else if (paymentMethod.selectedPaymentMethodType === CONST.PAYMENT_METHODS.DEBIT_CARD) { - PaymentMethods.makeDefaultPaymentMethod(null, paymentMethod.selectedPaymentMethod.fundID || null, previousPaymentMethod, currentPaymentMethod); + PaymentMethods.makeDefaultPaymentMethod(null, paymentMethod.selectedPaymentMethod.fundID ?? null, previousPaymentMethod, currentPaymentMethod); } }, [ paymentMethod.methodID, @@ -346,7 +340,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi !(paymentMethod.formattedSelectedPaymentMethod.type === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT && paymentMethod.selectedPaymentMethod.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS); // Determines whether or not the modal popup is mounted from the bottom of the screen instead of the side mount on Web or Desktop screens - const isPopoverBottomMount = anchorPosition.anchorPositionTop === 0 || isSmallScreenWidth; + const isPopoverBottomMount = anchorPosition.anchorPositionTop === 0 ?? isSmallScreenWidth; const alertTextStyle = [styles.inlineSystemMessage, styles.flexShrink1]; const alertViewStyle = [styles.flexRow, styles.alignItemsCenter, styles.w100, styles.ph5]; @@ -395,9 +389,10 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi )} navigateToWalletOrTransferBalancePage(source)} onSelectPaymentMethod={(selectedPaymentMethod: string) => { - if (hasActivatedWallet || selectedPaymentMethod !== CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { + if (hasActivatedWallet ?? selectedPaymentMethod !== CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { return; } // To allow upgrading to a gold wallet, continue with the KYC flow after adding a bank account @@ -488,7 +483,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} activePaymentMethodID={shouldShowDefaultDeleteMenu ? getSelectedPaymentMethodID() : ''} buttonRef={addPaymentMethodAnchorRef} - onListContentSizeChange={shouldShowAddPaymentMenu || shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} + onListContentSizeChange={shouldShowAddPaymentMenu ?? shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} /> ) : null} @@ -504,7 +499,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} activePaymentMethodID={shouldShowDefaultDeleteMenu ? getSelectedPaymentMethodID() : ''} buttonRef={addPaymentMethodAnchorRef} - onListContentSizeChange={shouldShowAddPaymentMenu || shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} + onListContentSizeChange={shouldShowAddPaymentMenu ?? shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} shouldEnableScroll={false} style={styles.mt5} /> @@ -525,7 +520,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi {isPopoverBottomMount && ( Date: Thu, 11 Jan 2024 15:24:39 +0700 Subject: [PATCH 040/635] resolve conflict --- src/pages/settings/Wallet/PaymentMethodList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index f318b77f8a0a..71c995363666 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -296,7 +296,7 @@ function PaymentMethodList({ return ( <> - + Date: Thu, 11 Jan 2024 15:29:21 +0700 Subject: [PATCH 041/635] lint --- src/components/AddPaymentMethodMenu.js | 10 ++--- .../settings/Wallet/PaymentMethodList.tsx | 39 ++++++++----------- .../Wallet/WalletPage/CardDetails.tsx | 2 +- .../settings/Wallet/WalletPage/WalletPage.tsx | 28 ++++++------- 4 files changed, 36 insertions(+), 43 deletions(-) diff --git a/src/components/AddPaymentMethodMenu.js b/src/components/AddPaymentMethodMenu.js index 033fb8765b6f..803b7f2cdabe 100644 --- a/src/components/AddPaymentMethodMenu.js +++ b/src/components/AddPaymentMethodMenu.js @@ -1,14 +1,14 @@ +import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import iouReportPropTypes from '@pages/iouReportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { withOnyx } from 'react-native-onyx'; -import _ from 'underscore'; import * as Expensicons from './Icon/Expensicons'; import PopoverMenu from './PopoverMenu'; import refPropTypes from './refPropTypes'; diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 71c995363666..c8dcee62988e 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -1,7 +1,17 @@ +import {FlashList} from '@shopify/flash-list'; +import _ from 'lodash'; +import type {ReactElement, Ref} from 'react'; +import React, {useCallback, useMemo} from 'react'; +import type {GestureResponderEvent, StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import type {TupleToUnion, ValueOf} from 'type-fest'; +import type {RenderSuggestionMenuItemProps} from '@components/AutoCompleteSuggestions/types'; import Button from '@components/Button'; import FormAlertWrapper from '@components/FormAlertWrapper'; import getBankIcon from '@components/Icon/BankIcons'; -import type { BankName } from '@components/Icon/BankIconsUtils'; +import type {BankName} from '@components/Icon/BankIconsUtils'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -14,24 +24,14 @@ import * as CardUtils from '@libs/CardUtils'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as PaymentUtils from '@libs/PaymentUtils'; -import { FlashList } from '@shopify/flash-list'; +import variables from '@styles/variables'; +import * as PaymentMethods from '@userActions/PaymentMethods'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type { AccountData, BankAccountList, CardList, FundList } from '@src/types/onyx'; +import type {AccountData, BankAccountList, CardList, FundList} from '@src/types/onyx'; import type PaymentMethod from '@src/types/onyx/PaymentMethod'; import type IconAsset from '@src/types/utils/IconAsset'; -import variables from '@styles/variables'; -import * as PaymentMethods from '@userActions/PaymentMethods'; -import type { ReactElement, Ref } from 'react'; -import React, { useCallback, useMemo } from 'react'; -import type { GestureResponderEvent, StyleProp, ViewStyle } from 'react-native'; -import { View } from 'react-native'; -import type { OnyxEntry } from 'react-native-onyx'; -import { withOnyx } from 'react-native-onyx'; -import type { TupleToUnion, ValueOf } from 'type-fest'; -import _ from 'lodash'; -import type { RenderSuggestionMenuItemProps } from '@components/AutoCompleteSuggestions/types'; const FILTER_TYPES = [CONST.PAYMENT_METHODS.DEBIT_CARD, CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT, ''] as const; @@ -92,14 +92,7 @@ type PaymentMethodListProps = PaymentMethodListOnyxProps & { shouldShowEmptyListMessage?: boolean; /** What to do when a menu item is pressed */ - onPress: ( - event?: GestureResponderEvent | KeyboardEvent, - accountType?: string, - accountData?: AccountData, - icon?: IconAsset, - isDefault?: boolean, - methodID?: number, - ) => void; + onPress: (event?: GestureResponderEvent | KeyboardEvent, accountType?: string, accountData?: AccountData, icon?: IconAsset, isDefault?: boolean, methodID?: number) => void; }; type PaymentMethodItem = PaymentMethod & { @@ -296,7 +289,7 @@ function PaymentMethodList({ return ( <> - + { // In order to prevent a loop, only update state of the spinner if there is a change From 5b0e92f9ed559f826d53ab2cfa41ca3baf7276ab Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Thu, 11 Jan 2024 13:49:29 -0500 Subject: [PATCH 042/635] lint --- src/libs/SidebarUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 2392a064ff37..6ffa6cc26450 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -129,8 +129,8 @@ function getOrderedReportIDs( const cachedReportsKey = JSON.stringify( [currentReportId, allReports, betas, policies, priorityMode, reportActionCount], /** - * Exclude some properties not to overwhelm a cached key value with huge data, - * which we don't need to store in a cacheKey + * Exclude some properties not to overwhelm a cached key value with huge data, + * which we don't need to store in a cacheKey */ (key, value: unknown) => (['participantAccountIDs', 'participants', 'lastMessageText', 'visibleChatMemberAccountIDs'].includes(key) ? undefined : value), ); From 37cfb7936a1c696bb7d0d0224d1784750e9b4752 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Thu, 11 Jan 2024 13:53:48 -0500 Subject: [PATCH 043/635] feat(Violations): remove duplicate from merge --- src/CONST.ts | 36 ------------------------------------ 1 file changed, 36 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 1c15f62169fe..c14db74ff173 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -3104,42 +3104,6 @@ const CONST = { TAX_REQUIRED: 'taxRequired', }, - VIOLATIONS: { - ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired', - AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense', - BILLABLE_EXPENSE: 'billableExpense', - CASH_EXPENSE_WITH_NO_RECEIPT: 'cashExpenseWithNoReceipt', - CATEGORY_OUT_OF_POLICY: 'categoryOutOfPolicy', - CONVERSION_SURCHARGE: 'conversionSurcharge', - CUSTOM_UNIT_OUT_OF_POLICY: 'customUnitOutOfPolicy', - DUPLICATED_TRANSACTION: 'duplicatedTransaction', - FIELD_REQUIRED: 'fieldRequired', - FUTURE_DATE: 'futureDate', - INVOICE_MARKUP: 'invoiceMarkup', - MAX_AGE: 'maxAge', - MISSING_CATEGORY: 'missingCategory', - MISSING_COMMENT: 'missingComment', - MISSING_TAG: 'missingTag', - MODIFIED_AMOUNT: 'modifiedAmount', - MODIFIED_DATE: 'modifiedDate', - NON_EXPENSIWORKS_EXPENSE: 'nonExpensiworksExpense', - OVER_AUTO_APPROVAL_LIMIT: 'overAutoApprovalLimit', - OVER_CATEGORY_LIMIT: 'overCategoryLimit', - OVER_LIMIT: 'overLimit', - OVER_LIMIT_ATTENDEE: 'overLimitAttendee', - PER_DAY_LIMIT: 'perDayLimit', - RECEIPT_NOT_SMART_SCANNED: 'receiptNotSmartScanned', - RECEIPT_REQUIRED: 'receiptRequired', - RTER: 'rter', - SMARTSCAN_FAILED: 'smartscanFailed', - SOME_TAG_LEVELS_REQUIRED: 'someTagLevelsRequired', - TAG_OUT_OF_POLICY: 'tagOutOfPolicy', - TAX_AMOUNT_CHANGED: 'taxAmountChanged', - TAX_OUT_OF_POLICY: 'taxOutOfPolicy', - TAX_RATE_CHANGED: 'taxRateChanged', - TAX_REQUIRED: 'taxRequired', - }, - /** Context menu types */ CONTEXT_MENU_TYPES: { LINK: 'LINK', From d166df1afb29b376f79adaee8d4c987c63b7962f Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Thu, 11 Jan 2024 13:54:29 -0500 Subject: [PATCH 044/635] feat(Violations): take transaction or transactionId for hasViolation --- src/libs/TransactionUtils.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 9cce4fd09b98..b2824e47e06c 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -524,8 +524,9 @@ function getRecentTransactions(transactions: Record, size = 2): /** * Checks if any violations for the provided transaction are of type 'violation' */ -function hasViolation(transaction: Transaction, transactionViolations: TransactionViolations): boolean { - return Boolean(transactionViolations[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transaction.transactionID]?.some((violation: TransactionViolation) => violation.type === 'violation')); +function hasViolation(transaction: Transaction | string, transactionViolations: TransactionViolations): boolean { + const transactionId = typeof transaction === 'string' ? transaction : transaction.transactionID; + return Boolean(transactionViolations[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionId]?.some((violation: TransactionViolation) => violation.type === 'violation')); } function getTransactionViolations({transactionID}: Transaction, transactionViolations: TransactionViolations): TransactionViolation[] | null { From 44229197e06e266e2540f33b8d5c911561044763 Mon Sep 17 00:00:00 2001 From: Lizzi Lindboe Date: Thu, 11 Jan 2024 14:47:23 -0500 Subject: [PATCH 045/635] feat(Violations): prettier --- src/libs/SidebarUtils.ts | 3 ++- src/libs/TransactionUtils.ts | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 07bc248dd0ed..a58d79a79778 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -126,7 +126,8 @@ function getOrderedReportIDs( const reportActionCount = allReportActions?.[reportIDKey]?.length ?? 1; // Generate a unique cache key based on the function arguments - const cachedReportsKey = JSON.stringify([currentReportId, allReports, betas, policies, priorityMode, reportActionCount], + const cachedReportsKey = JSON.stringify( + [currentReportId, allReports, betas, policies, priorityMode, reportActionCount], /** * Exclude some properties not to overwhelm a cached key value with huge data, * which we don't need to store in a cacheKey diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index aedc881528a7..cd75b8eb03e3 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -533,7 +533,6 @@ function getTransactionViolations({transactionID}: Transaction, transactionViola return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; } - /** * this is the formulae to calculate tax */ From b7045ec7091f7aba9d94939ea654c3ba5cc59b06 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 12 Jan 2024 16:05:21 -0500 Subject: [PATCH 046/635] fix: adding backTo param to ReportParticipantsPage.js --- src/pages/ReportParticipantsPage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 7dbc1c7036c4..9e480d2f0516 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -121,7 +121,12 @@ function ReportParticipantsPage(props) { }, ]} onSelectRow={(option) => { - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID)); + Navigation.navigate( + ROUTES.PROFILE.getRoute( + option.accountID, + ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID) + ) + ); }} hideSectionHeaders showTitleTooltip From e7b85131efe677ac38f177262f3c51b512643b82 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sat, 13 Jan 2024 04:50:32 +0100 Subject: [PATCH 047/635] fmt: prettier --- src/pages/ReportParticipantsPage.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 9e480d2f0516..3bc2783db981 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -121,12 +121,7 @@ function ReportParticipantsPage(props) { }, ]} onSelectRow={(option) => { - Navigation.navigate( - ROUTES.PROFILE.getRoute( - option.accountID, - ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID) - ) - ); + Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID))); }} hideSectionHeaders showTitleTooltip From ec74f831b88083175fa147d3ebb69cea73d4f842 Mon Sep 17 00:00:00 2001 From: Vadym Date: Mon, 15 Jan 2024 02:10:23 +0000 Subject: [PATCH 048/635] chore: migrates reactionPropTypes, BaseReactionList and HeaderReactionList to TS --- ...seReactionList.js => BaseReactionList.tsx} | 53 +++++++++---------- .../report/ReactionList/HeaderReactionList.js | 48 ----------------- .../ReactionList/HeaderReactionList.tsx | 48 +++++++++++++++++ .../report/ReactionList/reactionPropTypes.js | 17 ------ .../report/ReactionList/reactionPropTypes.ts | 13 +++++ 5 files changed, 86 insertions(+), 93 deletions(-) rename src/pages/home/report/ReactionList/{BaseReactionList.js => BaseReactionList.tsx} (69%) delete mode 100644 src/pages/home/report/ReactionList/HeaderReactionList.js create mode 100644 src/pages/home/report/ReactionList/HeaderReactionList.tsx delete mode 100644 src/pages/home/report/ReactionList/reactionPropTypes.js create mode 100644 src/pages/home/report/ReactionList/reactionPropTypes.ts diff --git a/src/pages/home/report/ReactionList/BaseReactionList.js b/src/pages/home/report/ReactionList/BaseReactionList.tsx similarity index 69% rename from src/pages/home/report/ReactionList/BaseReactionList.js rename to src/pages/home/report/ReactionList/BaseReactionList.tsx index 2d881d080c31..82541b28c9c8 100755 --- a/src/pages/home/report/ReactionList/BaseReactionList.js +++ b/src/pages/home/report/ReactionList/BaseReactionList.tsx @@ -1,36 +1,34 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; import React from 'react'; -import {FlatList} from 'react-native'; +import {FlatList, type FlatListProps} from 'react-native'; import OptionRow from '@components/OptionRow'; -import participantPropTypes from '@components/participantPropTypes'; -import withWindowDimensions from '@components/withWindowDimensions'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import Navigation from '@libs/Navigation/Navigation'; import * as UserUtils from '@libs/UserUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {PersonalDetails} from '@src/types/onyx'; import HeaderReactionList from './HeaderReactionList'; -import reactionPropTypes from './reactionPropTypes'; +import type {ReactionListProps} from './reactionPropTypes'; -const propTypes = { +type BaseReactionListProps = ReactionListProps & { /** * Array of personal detail objects */ - users: PropTypes.arrayOf(participantPropTypes).isRequired, + users: PersonalDetails[]; /** * Returns true if the current account has reacted to the report action (with the given skin tone). */ - hasUserReacted: PropTypes.bool, + hasUserReacted: boolean; - ...reactionPropTypes, -}; - -const defaultProps = { - hasUserReacted: false, + /** + * Returns true if the reaction list is visible + */ + isVisible: boolean; }; /** @@ -39,7 +37,7 @@ const defaultProps = { * @param {Number} index * @return {String} */ -const keyExtractor = (item, index) => `${item.login}+${index}`; +const keyExtractor: FlatListProps['keyExtractor'] = (item, index) => `${item.login}+${index}`; /** * This function will be used with FlatList getItemLayout property for optimization purpose that allows skipping @@ -51,14 +49,15 @@ const keyExtractor = (item, index) => `${item.login}+${index}`; * @param {Number} index row index * @returns {Object} */ -const getItemLayout = (_, index) => ({ +const getItemLayout = (_: any, index: number): {length: number; offset: number; index: number} => ({ index, length: variables.listItemHeightNormal, offset: variables.listItemHeightNormal * index, }); -function BaseReactionList(props) { - const styles = useThemeStyles(); +function BaseReactionList(props: BaseReactionListProps) { + const {isSmallScreenWidth} = useWindowDimensions(); + const {hoveredComponentBG, reactionListContainer, reactionListContainerFixedWidth, pv2} = useThemeStyles(); if (!props.isVisible) { return null; } @@ -72,25 +71,25 @@ function BaseReactionList(props) { * @param {Object} params.item * @return {React.Component} */ - const renderItem = ({item}) => ( + const renderItem: FlatListProps['renderItem'] = ({item}) => ( { - props.onClose(); + props.onClose && props.onClose(); Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); }} option={{ - text: Str.removeSMSDomain(item.displayName), + reportID: String(item.accountID), + text: Str.removeSMSDomain(item.displayName || ''), alternateText: Str.removeSMSDomain(item.login || ''), participantsList: [item], icons: [ { id: item.accountID, source: UserUtils.getAvatar(item.avatar, item.accountID), - name: item.login, + name: item.login || '', type: CONST.ICON_TYPE_AVATAR, }, ], @@ -113,15 +112,13 @@ function BaseReactionList(props) { renderItem={renderItem} keyExtractor={keyExtractor} getItemLayout={getItemLayout} - contentContainerStyle={styles.pv2} - style={[styles.reactionListContainer, !props.isSmallScreenWidth && styles.reactionListContainerFixedWidth]} + contentContainerStyle={pv2} + style={[reactionListContainer, !isSmallScreenWidth && reactionListContainerFixedWidth]} /> ); } -BaseReactionList.propTypes = propTypes; -BaseReactionList.defaultProps = defaultProps; BaseReactionList.displayName = 'BaseReactionList'; -export default withWindowDimensions(BaseReactionList); +export default BaseReactionList; diff --git a/src/pages/home/report/ReactionList/HeaderReactionList.js b/src/pages/home/report/ReactionList/HeaderReactionList.js deleted file mode 100644 index 04b124f969a9..000000000000 --- a/src/pages/home/report/ReactionList/HeaderReactionList.js +++ /dev/null @@ -1,48 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as EmojiUtils from '@libs/EmojiUtils'; -import reactionPropTypes from './reactionPropTypes'; - -const propTypes = { - ...reactionPropTypes, - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, - - /** - * Returns true if the current account has reacted to the report action (with the given skin tone). - */ - hasUserReacted: PropTypes.bool, -}; - -const defaultProps = { - hasUserReacted: false, -}; - -function HeaderReactionList(props) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - return ( - - - - {props.emojiCodes.join('')} - {props.emojiCount} - - {`:${EmojiUtils.getLocalizedEmojiName(props.emojiName, props.preferredLocale)}:`} - - - ); -} - -HeaderReactionList.propTypes = propTypes; -HeaderReactionList.defaultProps = defaultProps; -HeaderReactionList.displayName = 'HeaderReactionList'; - -export default compose(withWindowDimensions, withLocalize)(HeaderReactionList); diff --git a/src/pages/home/report/ReactionList/HeaderReactionList.tsx b/src/pages/home/report/ReactionList/HeaderReactionList.tsx new file mode 100644 index 000000000000..551059b697e6 --- /dev/null +++ b/src/pages/home/report/ReactionList/HeaderReactionList.tsx @@ -0,0 +1,48 @@ +import {View} from 'react-native'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import type {ReactionListProps} from './reactionPropTypes'; + +type HeaderReactionListProps = ReactionListProps & { + /** + * Returns true if the current account has reacted to the report action (with the given skin tone). + */ + hasUserReacted: boolean; +}; + +function HeaderReactionList(props: HeaderReactionListProps) { + const { + flexRow, + justifyContentBetween, + alignItemsCenter, + emojiReactionListHeader, + pt4, + emojiReactionListHeaderBubble, + miniQuickEmojiReactionText, + reactionCounterText, + reactionListHeaderText, + } = useThemeStyles(); + const {getEmojiReactionBubbleStyle, getEmojiReactionBubbleTextStyle, getEmojiReactionCounterTextStyle} = useStyleUtils(); + const {preferredLocale} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); + + return ( + + + + {props.emojiCodes.join('')} + {props.emojiCount} + + {`:${EmojiUtils.getLocalizedEmojiName(props.emojiName, preferredLocale)}:`} + + + ); +} + +HeaderReactionList.displayName = 'HeaderReactionList'; + +export default HeaderReactionList; diff --git a/src/pages/home/report/ReactionList/reactionPropTypes.js b/src/pages/home/report/ReactionList/reactionPropTypes.js deleted file mode 100644 index 3421af230399..000000000000 --- a/src/pages/home/report/ReactionList/reactionPropTypes.js +++ /dev/null @@ -1,17 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Hide the ReactionList modal popover */ - onClose: PropTypes.func, - - /** The emoji codes */ - emojiCodes: PropTypes.arrayOf(PropTypes.string).isRequired, - - /** The name of the emoji */ - emojiName: PropTypes.string.isRequired, - - /** Count of the emoji */ - emojiCount: PropTypes.number.isRequired, -}; - -export default propTypes; diff --git a/src/pages/home/report/ReactionList/reactionPropTypes.ts b/src/pages/home/report/ReactionList/reactionPropTypes.ts new file mode 100644 index 000000000000..d88316754600 --- /dev/null +++ b/src/pages/home/report/ReactionList/reactionPropTypes.ts @@ -0,0 +1,13 @@ +export type ReactionListProps = { + /** Hide the ReactionList modal popover */ + onClose?: () => void; + + /** The emoji codes */ + emojiCodes: string[]; + + /** The name of the emoji */ + emojiName: string; + + /** Count of the emoji */ + emojiCount: number; +}; From cb70663a543eaee30f6f0a0302e740b8f5a70d5f Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 15 Jan 2024 16:51:24 +0700 Subject: [PATCH 049/635] change ?? to || --- src/libs/actions/PaymentMethods.ts | 6 +++-- .../settings/Wallet/PaymentMethodList.tsx | 18 ++++++++++----- .../settings/Wallet/WalletEmptyState.tsx | 3 +++ .../Wallet/WalletPage/CardDetails.tsx | 3 ++- .../settings/Wallet/WalletPage/WalletPage.tsx | 23 +++++++++++-------- 5 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index c152a968f030..24899087c46d 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -83,7 +83,8 @@ function getMakeDefaultPaymentOnyxData( onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.USER_WALLET, value: { - walletLinkedAccountID: bankAccountID ?? fundID, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + walletLinkedAccountID: bankAccountID || fundID, walletLinkedAccountType: bankAccountID ? CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT : CONST.PAYMENT_METHODS.DEBIT_CARD, // Only clear the error if this is optimistic data. If this is failure data, we do not want to clear the error that came from the server. errors: null, @@ -93,7 +94,8 @@ function getMakeDefaultPaymentOnyxData( onyxMethod: Onyx.METHOD.MERGE, key: ONYXKEYS.USER_WALLET, value: { - walletLinkedAccountID: bankAccountID ?? fundID, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + walletLinkedAccountID: bankAccountID || fundID, walletLinkedAccountType: bankAccountID ? CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT : CONST.PAYMENT_METHODS.DEBIT_CARD, }, }, diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index c8dcee62988e..db04bfb9f183 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -133,7 +133,8 @@ function shouldShowDefaultBadge(filteredPaymentMethods: PaymentMethod[], isDefau } const defaultablePaymentMethodCount = filteredPaymentMethods.filter( - (method) => method.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT ?? method.accountType === CONST.PAYMENT_METHODS.DEBIT_CARD, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (method) => method.accountType === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT || method.accountType === CONST.PAYMENT_METHODS.DEBIT_CARD, ).length; return defaultablePaymentMethodCount > 1; } @@ -197,7 +198,8 @@ function PaymentMethodList({ interactive: isExpensifyCard, canDismissError: isExpensifyCard, errors: card.errors, - brickRoadIndicator: card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN ?? card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL ? 'error' : null, + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + brickRoadIndicator: card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN || card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.INDIVIDUAL ? 'error' : null, ...icon, }; }); @@ -215,7 +217,8 @@ function PaymentMethodList({ if (!isOffline) { combinedPaymentMethods = combinedPaymentMethods.filter( - (paymentMethod) => paymentMethod.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE ?? !_.isEmpty(paymentMethod.errors), + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + (paymentMethod) => paymentMethod.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(paymentMethod.errors), ); } @@ -270,8 +273,10 @@ function PaymentMethodList({ icon={item.icon} disabled={item.disabled} displayInDefaultIconColor - iconHeight={item.iconHeight ?? item.iconSize} - iconWidth={item.iconWidth ?? item.iconSize} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + iconHeight={item.iconHeight || item.iconSize} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + iconWidth={item.iconWidth || item.iconSize} iconStyles={item.iconStyles} badgeText={shouldShowDefaultBadge(filteredPaymentMethods, item.isDefault) ? translate('paymentMethodList.defaultPaymentMethod') : undefined} wrapperStyle={styles.paymentMethod} @@ -309,7 +314,8 @@ function PaymentMethodList({ text={translate('paymentMethodList.addPaymentMethod')} icon={Expensicons.CreditCard} onPress={onPress} - isDisabled={isLoadingPaymentMethods ?? isFormOffline} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + isDisabled={isLoadingPaymentMethods || isFormOffline} style={[styles.mh4, styles.buttonCTA]} iconStyles={[styles.buttonCTAIcon]} key="addPaymentMethodButton" diff --git a/src/pages/settings/Wallet/WalletEmptyState.tsx b/src/pages/settings/Wallet/WalletEmptyState.tsx index 4060a3695b92..bfec7fa6314b 100644 --- a/src/pages/settings/Wallet/WalletEmptyState.tsx +++ b/src/pages/settings/Wallet/WalletEmptyState.tsx @@ -11,6 +11,7 @@ import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; type WalletEmptyStateProps = { + /** The function that is called when a menu item is pressed */ onAddPaymentMethod: () => void; }; @@ -56,4 +57,6 @@ function WalletEmptyState({onAddPaymentMethod}: WalletEmptyStateProps) { ); } +WalletEmptyState.displayName = 'WalletEmptyState'; + export default WalletEmptyState; diff --git a/src/pages/settings/Wallet/WalletPage/CardDetails.tsx b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx index 2732e817ae47..ce2dbc6b2cb5 100644 --- a/src/pages/settings/Wallet/WalletPage/CardDetails.tsx +++ b/src/pages/settings/Wallet/WalletPage/CardDetails.tsx @@ -87,7 +87,8 @@ function CardDetails({pan = '', expiration = '', cvv = '', privatePersonalDetail /> navigateToWalletOrTransferBalancePage(source)} onSelectPaymentMethod={(selectedPaymentMethod: string) => { - if (hasActivatedWallet ?? selectedPaymentMethod !== CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (hasActivatedWallet || selectedPaymentMethod !== CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) { return; } // To allow upgrading to a gold wallet, continue with the KYC flow after adding a bank account @@ -483,7 +486,8 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} activePaymentMethodID={shouldShowDefaultDeleteMenu ? getSelectedPaymentMethodID() : ''} buttonRef={addPaymentMethodAnchorRef} - onListContentSizeChange={shouldShowAddPaymentMenu ?? shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + onListContentSizeChange={shouldShowAddPaymentMenu || shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} /> ) : null} @@ -499,7 +503,8 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi actionPaymentMethodType={shouldShowDefaultDeleteMenu ? paymentMethod.selectedPaymentMethodType : ''} activePaymentMethodID={shouldShowDefaultDeleteMenu ? getSelectedPaymentMethodID() : ''} buttonRef={addPaymentMethodAnchorRef} - onListContentSizeChange={shouldShowAddPaymentMenu ?? shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + onListContentSizeChange={shouldShowAddPaymentMenu || shouldShowDefaultDeleteMenu ? setMenuPosition : () => {}} shouldEnableScroll={false} style={styles.mt5} /> @@ -520,7 +525,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi {isPopoverBottomMount && ( Date: Tue, 16 Jan 2024 03:11:39 +0100 Subject: [PATCH 050/635] chore: migrates BasePopoverReactionList and ReactionList to TS --- ...ionList.js => BasePopoverReactionList.tsx} | 104 +++++++++++++----- .../ReactionList/PopoverReactionList/index.js | 67 ----------- .../PopoverReactionList/index.tsx | 60 ++++++++++ 3 files changed, 136 insertions(+), 95 deletions(-) rename src/pages/home/report/ReactionList/PopoverReactionList/{BasePopoverReactionList.js => BasePopoverReactionList.tsx} (65%) delete mode 100644 src/pages/home/report/ReactionList/PopoverReactionList/index.js create mode 100644 src/pages/home/report/ReactionList/PopoverReactionList/index.tsx diff --git a/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.js b/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx similarity index 65% rename from src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.js rename to src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx index a06ed18de957..8f532253fc43 100644 --- a/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.js +++ b/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx @@ -1,36 +1,63 @@ import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React from 'react'; import {Dimensions} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import _ from 'underscore'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; -import EmojiReactionsPropTypes from '@components/Reactions/EmojiReactionsPropTypes'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import withCurrentUserPersonalDetails, {type WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withLocalize, {type WithLocalizeProps} from '@components/withLocalize'; import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import BaseReactionList from '@pages/home/report/ReactionList/BaseReactionList'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportActionReactions} from '@src/types/onyx'; +import {ReportActionReaction} from '@src/types/onyx/ReportActionReactions'; -const propTypes = { - reportActionID: PropTypes.string, - emojiName: PropTypes.string, - emojiReactions: EmojiReactionsPropTypes, +type BasePopoverReactionListOnyxProps = { + /** The reactions for the report action */ + emojiReactions: OnyxEntry; +}; + +type BasePopoverReactionListProps = { + /** The ID of the report action */ + reportActionID: string; + + /** The emoji name */ + emojiName: string; - ...withLocalizePropTypes, + /** The ref of the action */ + ref: React.Ref; }; -const defaultProps = { - reportActionID: '', - emojiName: '', - emojiReactions: {}, +type BasePopoverReactionListWithLocalizeProps = WithLocalizeProps & WithCurrentUserPersonalDetailsProps; + +type BasePopoverReactionListPropsWithLocalWithOnyx = BasePopoverReactionListWithLocalizeProps & BasePopoverReactionListOnyxProps & BasePopoverReactionListProps; +type BasePopoverReactionListState = { + /** Whether the popover is visible */ + isPopoverVisible: boolean; + + /** The horizontal and vertical position (relative to the screen) where the popover will display. */ + popoverAnchorPosition: { + horizontal: number; + vertical: number; + }; + + /** The horizontal and vertical position (relative to the screen) where the cursor is. */ + cursorRelativePosition: { + horizontal: number; + vertical: number; + }; }; -class BasePopoverReactionList extends React.Component { - constructor(props) { +class BasePopoverReactionList extends React.Component { + reactionListAnchor: React.RefObject; + dimensionsEventListener: { + remove: () => void; + } | null; + constructor(props: BasePopoverReactionListPropsWithLocalWithOnyx) { super(props); this.state = { @@ -60,8 +87,9 @@ class BasePopoverReactionList extends React.Component { this.dimensionsEventListener = Dimensions.addEventListener('change', this.measureReactionListPosition); } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: BasePopoverReactionListPropsWithLocalWithOnyx, nextState: BasePopoverReactionListState) { if (!this.state.isPopoverVisible && !nextState.isPopoverVisible) { + // If the popover is not visible, we don't need to update the component return false; } @@ -81,11 +109,13 @@ class BasePopoverReactionList extends React.Component { componentDidUpdate() { if (!this.state.isPopoverVisible) { + // If the popover is not visible, we don't need to update the component return; } // Hide the list when all reactions are removed - const isEmptyList = !_.some(lodashGet(this.props.emojiReactions, [this.props.emojiName, 'users'])); + const emojiReactions = lodashGet(this.props.emojiReactions, [this.props.emojiName, 'users']); + const isEmptyList = !emojiReactions || !_.some(emojiReactions); if (!isEmptyList) { return; } @@ -94,6 +124,7 @@ class BasePopoverReactionList extends React.Component { } componentWillUnmount() { + // Remove the event listener if (!this.dimensionsEventListener) { return; } @@ -106,11 +137,13 @@ class BasePopoverReactionList extends React.Component { * * @returns {Promise} */ - getReactionListMeasuredLocation() { + getReactionListMeasuredLocation(): Promise<{x: number; y: number}> { return new Promise((resolve) => { - if (this.reactionListAnchor.current) { - this.reactionListAnchor.current.measureInWindow((x, y) => resolve({x, y})); + const reactionListAnchor = this.reactionListAnchor.current as HTMLElement & {measureInWindow: (callback: (x: number, y: number) => void) => void}; + if (reactionListAnchor) { + reactionListAnchor.measureInWindow((x, y) => resolve({x, y})); } else { + // If the anchor is not available, we return 0, 0 resolve({x: 0, y: 0}); } }); @@ -123,8 +156,9 @@ class BasePopoverReactionList extends React.Component { * @param {String} emojiName * @returns {Object} */ - getReactionInformation(selectedReaction, emojiName) { + getReactionInformation(selectedReaction: ReportActionReaction | null | undefined, emojiName: string) { if (!selectedReaction) { + // If there is no reaction, we return default values return { emojiName: '', reactionCount: 0, @@ -152,9 +186,18 @@ class BasePopoverReactionList extends React.Component { * @param {Object} [event] - A press event. * @param {Element} reactionListAnchor - reactionListAnchor */ - showReactionList(event, reactionListAnchor) { + showReactionList( + event: { + nativeEvent: { + pageX: number; + pageY: number; + }; + }, + reactionListAnchor: HTMLElement, + ) { + // We get the cursor coordinates and the reactionListAnchor coordinates to calculate the popover position const nativeEvent = event.nativeEvent || {}; - this.reactionListAnchor.current = reactionListAnchor; + this.reactionListAnchor = {current: reactionListAnchor}; this.getReactionListMeasuredLocation().then(({x, y}) => { this.setState({ cursorRelativePosition: { @@ -175,6 +218,7 @@ class BasePopoverReactionList extends React.Component { */ measureReactionListPosition() { if (!this.state.isPopoverVisible) { + // If the popover is not visible, we don't need to update the component return; } this.getReactionListMeasuredLocation().then(({x, y}) => { @@ -200,7 +244,10 @@ class BasePopoverReactionList extends React.Component { } render() { + // Get the selected reaction const selectedReaction = this.state.isPopoverVisible ? lodashGet(this.props.emojiReactions, [this.props.emojiName]) : null; + + // Get the reaction information const {emojiName, emojiCodes, reactionCount, hasUserReacted, users} = this.getReactionInformation(selectedReaction, this.props.emojiName); return ( @@ -215,9 +262,12 @@ class BasePopoverReactionList extends React.Component { fullscreen withoutOverlay anchorRef={this.reactionListAnchor} + anchorAlignment={{ + horizontal: 'left', + vertical: 'top', + }} > ({ emojiReactions: { key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, }, diff --git a/src/pages/home/report/ReactionList/PopoverReactionList/index.js b/src/pages/home/report/ReactionList/PopoverReactionList/index.js deleted file mode 100644 index 9f1e7b3113fc..000000000000 --- a/src/pages/home/report/ReactionList/PopoverReactionList/index.js +++ /dev/null @@ -1,67 +0,0 @@ -import PropTypes from 'prop-types'; -import React, {forwardRef, useImperativeHandle, useRef, useState} from 'react'; -import BasePopoverReactionList from './BasePopoverReactionList'; - -const propTypes = { - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), -}; - -const defaultProps = { - innerRef: () => {}, -}; - -function PopoverReactionList(props) { - const innerReactionListRef = useRef(); - const [reactionListReportActionID, setReactionListReportActionID] = useState(''); - const [reactionListEmojiName, setReactionListEmojiName] = useState(''); - - /** - * Show the ReactionList modal popover. - * - * @param {Object} [event] - A press event. - * @param {Element} reactionListAnchor - reactionListAnchor - * @param {String} emojiName - Name of emoji - * @param {String} reportActionID - ID of the report action - */ - const showReactionList = (event, reactionListAnchor, emojiName, reportActionID) => { - setReactionListReportActionID(reportActionID); - setReactionListEmojiName(emojiName); - innerReactionListRef.current.showReactionList(event, reactionListAnchor); - }; - - const hideReactionList = () => { - innerReactionListRef.current.hideReactionList(); - }; - - /** - * Whether PopoverReactionList is active for the Report Action. - * - * @param {Number|String} actionID - * @return {Boolean} - */ - const isActiveReportAction = (actionID) => Boolean(actionID) && reactionListReportActionID === actionID; - - useImperativeHandle(props.innerRef, () => ({showReactionList, hideReactionList, isActiveReportAction})); - - return ( - - ); -} - -PopoverReactionList.propTypes = propTypes; -PopoverReactionList.defaultProps = defaultProps; -PopoverReactionList.displayName = 'PopoverReactionList'; - -export default React.memo( - forwardRef((props, ref) => ( - - )), -); diff --git a/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx b/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx new file mode 100644 index 000000000000..e4a7aa42791b --- /dev/null +++ b/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx @@ -0,0 +1,60 @@ +import React, {forwardRef, Ref, useImperativeHandle, useRef, useState} from 'react'; +import BasePopoverReactionList from './BasePopoverReactionList'; + +type PopoverReactionListProps = { + innerRef: Ref; +}; + +type InnerReactionListRefType = { + showReactionList: ( + event: { + nativeEvent: { + pageX: number; + pageY: number; + }; + }, + reactionListAnchor: HTMLElement, + ) => void; + hideReactionList: () => void; + isActiveReportAction: (actionID: number | string) => boolean; +}; + +const PopoverReactionList = (props: PopoverReactionListProps) => { + const innerReactionListRef = useRef(null); + const [reactionListReportActionID, setReactionListReportActionID] = useState(''); + const [reactionListEmojiName, setReactionListEmojiName] = useState(''); + + const showReactionList = (event: React.MouseEvent, reactionListAnchor: HTMLElement, emojiName: string, reportActionID: string) => { + setReactionListReportActionID(reportActionID); + setReactionListEmojiName(emojiName); + innerReactionListRef.current?.showReactionList(event, reactionListAnchor); + }; + + const hideReactionList = () => { + innerReactionListRef.current?.hideReactionList(); + }; + + const isActiveReportAction = (actionID: number | string) => Boolean(actionID) && reactionListReportActionID === actionID; + + useImperativeHandle(props.innerRef, () => ({showReactionList, hideReactionList, isActiveReportAction})); + + return ( + + ); +}; + +PopoverReactionList.displayName = 'PopoverReactionList'; + +export default React.memo( + forwardRef((props, ref) => ( + + )), +); From 28dfadc15e19eb23a9c361e336c698c4da4bf586 Mon Sep 17 00:00:00 2001 From: vadymbokatov <138146362+vadymbokatov@users.noreply.github.com> Date: Tue, 16 Jan 2024 10:07:10 +0100 Subject: [PATCH 051/635] Update src/pages/home/report/ReactionList/HeaderReactionList.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Błażej Kustra <46095609+blazejkustra@users.noreply.github.com> --- src/pages/home/report/ReactionList/HeaderReactionList.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pages/home/report/ReactionList/HeaderReactionList.tsx b/src/pages/home/report/ReactionList/HeaderReactionList.tsx index 551059b697e6..869dd95baf1e 100644 --- a/src/pages/home/report/ReactionList/HeaderReactionList.tsx +++ b/src/pages/home/report/ReactionList/HeaderReactionList.tsx @@ -8,9 +8,7 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import type {ReactionListProps} from './reactionPropTypes'; type HeaderReactionListProps = ReactionListProps & { - /** - * Returns true if the current account has reacted to the report action (with the given skin tone). - */ + /** Returns true if the current account has reacted to the report action (with the given skin tone). */ hasUserReacted: boolean; }; From 952c5232776b082eecb746119bf0e5b3e399eac3 Mon Sep 17 00:00:00 2001 From: Vadym Date: Tue, 16 Jan 2024 15:14:47 +0100 Subject: [PATCH 052/635] chore: renames the types file and fixes its linting errors --- src/pages/home/report/ReactionList/BaseReactionList.tsx | 2 +- src/pages/home/report/ReactionList/HeaderReactionList.tsx | 2 +- .../report/ReactionList/{reactionPropTypes.ts => types.ts} | 4 +++- 3 files changed, 5 insertions(+), 3 deletions(-) rename src/pages/home/report/ReactionList/{reactionPropTypes.ts => types.ts} (79%) diff --git a/src/pages/home/report/ReactionList/BaseReactionList.tsx b/src/pages/home/report/ReactionList/BaseReactionList.tsx index 82541b28c9c8..5d41d2f6656f 100755 --- a/src/pages/home/report/ReactionList/BaseReactionList.tsx +++ b/src/pages/home/report/ReactionList/BaseReactionList.tsx @@ -12,7 +12,7 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {PersonalDetails} from '@src/types/onyx'; import HeaderReactionList from './HeaderReactionList'; -import type {ReactionListProps} from './reactionPropTypes'; +import type {ReactionListProps} from './types'; type BaseReactionListProps = ReactionListProps & { /** diff --git a/src/pages/home/report/ReactionList/HeaderReactionList.tsx b/src/pages/home/report/ReactionList/HeaderReactionList.tsx index 869dd95baf1e..e715f6010fc3 100644 --- a/src/pages/home/report/ReactionList/HeaderReactionList.tsx +++ b/src/pages/home/report/ReactionList/HeaderReactionList.tsx @@ -5,7 +5,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as EmojiUtils from '@libs/EmojiUtils'; -import type {ReactionListProps} from './reactionPropTypes'; +import type {ReactionListProps} from './types'; type HeaderReactionListProps = ReactionListProps & { /** Returns true if the current account has reacted to the report action (with the given skin tone). */ diff --git a/src/pages/home/report/ReactionList/reactionPropTypes.ts b/src/pages/home/report/ReactionList/types.ts similarity index 79% rename from src/pages/home/report/ReactionList/reactionPropTypes.ts rename to src/pages/home/report/ReactionList/types.ts index d88316754600..ec03b141080d 100644 --- a/src/pages/home/report/ReactionList/reactionPropTypes.ts +++ b/src/pages/home/report/ReactionList/types.ts @@ -1,4 +1,4 @@ -export type ReactionListProps = { +type ReactionListProps = { /** Hide the ReactionList modal popover */ onClose?: () => void; @@ -11,3 +11,5 @@ export type ReactionListProps = { /** Count of the emoji */ emojiCount: number; }; + +export type {ReactionListProps}; From efe9d72aaa7f9079b0657c66619ebdfbd857b194 Mon Sep 17 00:00:00 2001 From: Vadym Date: Tue, 16 Jan 2024 16:52:08 +0100 Subject: [PATCH 053/635] chore: removes any typed and fixes linting errors on the index file --- .../BasePopoverReactionList.tsx | 2 ++ .../PopoverReactionList/index.tsx | 31 ++++++++----------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx b/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx index 8f532253fc43..f2c226d7856a 100644 --- a/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx +++ b/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx @@ -54,9 +54,11 @@ type BasePopoverReactionListState = { class BasePopoverReactionList extends React.Component { reactionListAnchor: React.RefObject; + dimensionsEventListener: { remove: () => void; } | null; + constructor(props: BasePopoverReactionListPropsWithLocalWithOnyx) { super(props); diff --git a/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx b/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx index e4a7aa42791b..8ac89cef6656 100644 --- a/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx +++ b/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx @@ -1,33 +1,28 @@ -import React, {forwardRef, Ref, useImperativeHandle, useRef, useState} from 'react'; +import React, {forwardRef, useImperativeHandle, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; import BasePopoverReactionList from './BasePopoverReactionList'; type PopoverReactionListProps = { - innerRef: Ref; + ref: ForwardedRef; }; +type ShowReactionList = (event: React.MouseEvent, reactionListAnchor: HTMLElement, emojiName: string, reportActionID: string) => void; + type InnerReactionListRefType = { - showReactionList: ( - event: { - nativeEvent: { - pageX: number; - pageY: number; - }; - }, - reactionListAnchor: HTMLElement, - ) => void; + showReactionList: ShowReactionList; hideReactionList: () => void; isActiveReportAction: (actionID: number | string) => boolean; }; -const PopoverReactionList = (props: PopoverReactionListProps) => { +function PopoverReactionList(props: PopoverReactionListProps) { const innerReactionListRef = useRef(null); const [reactionListReportActionID, setReactionListReportActionID] = useState(''); const [reactionListEmojiName, setReactionListEmojiName] = useState(''); - const showReactionList = (event: React.MouseEvent, reactionListAnchor: HTMLElement, emojiName: string, reportActionID: string) => { + const showReactionList: ShowReactionList = (event, reactionListAnchor, emojiName, reportActionID) => { setReactionListReportActionID(reportActionID); setReactionListEmojiName(emojiName); - innerReactionListRef.current?.showReactionList(event, reactionListAnchor); + innerReactionListRef.current?.showReactionList(event, reactionListAnchor, emojiName, reportActionID); }; const hideReactionList = () => { @@ -36,7 +31,7 @@ const PopoverReactionList = (props: PopoverReactionListProps) => { const isActiveReportAction = (actionID: number | string) => Boolean(actionID) && reactionListReportActionID === actionID; - useImperativeHandle(props.innerRef, () => ({showReactionList, hideReactionList, isActiveReportAction})); + useImperativeHandle(props.ref, () => ({showReactionList, hideReactionList, isActiveReportAction})); return ( { emojiName={reactionListEmojiName} /> ); -}; +} PopoverReactionList.displayName = 'PopoverReactionList'; export default React.memo( - forwardRef((props, ref) => ( + forwardRef((props, ref) => ( )), ); From a303a5e6f55cef79a27cc54eec24d650b4e20bdb Mon Sep 17 00:00:00 2001 From: Vadym Date: Tue, 16 Jan 2024 17:22:07 +0100 Subject: [PATCH 054/635] fix: some linting errors and any types --- .../report/ReactionList/BaseReactionList.tsx | 36 ++++++++++--------- .../BasePopoverReactionList.tsx | 10 +++--- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/pages/home/report/ReactionList/BaseReactionList.tsx b/src/pages/home/report/ReactionList/BaseReactionList.tsx index 5d41d2f6656f..80cbc834f344 100755 --- a/src/pages/home/report/ReactionList/BaseReactionList.tsx +++ b/src/pages/home/report/ReactionList/BaseReactionList.tsx @@ -1,7 +1,8 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import Str from 'expensify-common/lib/str'; import React from 'react'; -import {FlatList, type FlatListProps} from 'react-native'; +import {FlatList} from 'react-native'; +import type {FlatListProps} from 'react-native'; import OptionRow from '@components/OptionRow'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -33,9 +34,9 @@ type BaseReactionListProps = ReactionListProps & { /** * Create a unique key for each action in the FlatList. - * @param {Object} item - * @param {Number} index - * @return {String} + * @param item object + * @param index number + * @return string */ const keyExtractor: FlatListProps['keyExtractor'] = (item, index) => `${item.login}+${index}`; @@ -45,11 +46,11 @@ const keyExtractor: FlatListProps['keyExtractor'] = (item, inde * Generate and return an object with properties length(height of each individual row), * offset(distance of the current row from the top of the FlatList), index(current row index) * - * @param {*} _ FlatList item - * @param {Number} index row index - * @returns {Object} + * @param data FlatList item + * @param index number - row index + * @returns object */ -const getItemLayout = (_: any, index: number): {length: number; offset: number; index: number} => ({ +const getItemLayout = (data: ArrayLike | null | undefined, index: number): {length: number; offset: number; index: number} => ({ index, length: variables.listItemHeightNormal, offset: variables.listItemHeightNormal * index, @@ -67,9 +68,9 @@ function BaseReactionList(props: BaseReactionListProps) { * Items with the code "SPACER" return nothing and are used to fill rows up to 8 * so that the sticky headers function properly * - * @param {Object} params - * @param {Object} params.item - * @return {React.Component} + * @param params object + * @param params.item object + * @return React.Component */ const renderItem: FlatListProps['renderItem'] = ({item}) => ( { - props.onClose && props.onClose(); + if (props.onClose) { + props.onClose(); + } + Navigation.navigate(ROUTES.PROFILE.getRoute(item.accountID)); }} option={{ reportID: String(item.accountID), - text: Str.removeSMSDomain(item.displayName || ''), - alternateText: Str.removeSMSDomain(item.login || ''), + text: Str.removeSMSDomain(item.displayName ?? ''), + alternateText: Str.removeSMSDomain(item.login ?? ''), participantsList: [item], icons: [ { id: item.accountID, source: UserUtils.getAvatar(item.avatar, item.accountID), - name: item.login || '', + name: item.login ?? '', type: CONST.ICON_TYPE_AVATAR, }, ], - keyForList: item.login || String(item.accountID), + keyForList: item.login ?? String(item.accountID), }} /> ); diff --git a/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx b/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx index f2c226d7856a..4bee2e8d1d3c 100644 --- a/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx +++ b/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx @@ -5,8 +5,10 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import _ from 'underscore'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; -import withCurrentUserPersonalDetails, {type WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {type WithLocalizeProps} from '@components/withLocalize'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withLocalize from '@components/withLocalize'; +import type {WithLocalizeProps} from '@components/withLocalize'; import compose from '@libs/compose'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -14,7 +16,7 @@ import BaseReactionList from '@pages/home/report/ReactionList/BaseReactionList'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {ReportActionReactions} from '@src/types/onyx'; -import {ReportActionReaction} from '@src/types/onyx/ReportActionReactions'; +import type {ReportActionReaction} from '@src/types/onyx/ReportActionReactions'; type BasePopoverReactionListOnyxProps = { /** The reactions for the report action */ @@ -29,7 +31,7 @@ type BasePopoverReactionListProps = { emojiName: string; /** The ref of the action */ - ref: React.Ref; + ref: React.Ref; }; type BasePopoverReactionListWithLocalizeProps = WithLocalizeProps & WithCurrentUserPersonalDetailsProps; From f7518085c7c7dc5d35879ba6e726b3e090c193bf Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 17 Jan 2024 12:05:58 +0100 Subject: [PATCH 055/635] feat: add implementation for educational interstitial --- src/ONYXKEYS.ts | 4 +++ src/components/MoneyRequestHeader.js | 41 +++++++++++++++++++++++- src/libs/actions/IOU.js | 5 +++ src/pages/ProcessMoneyRequestHoldPage.js | 3 +- 4 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 89ddbdc06883..8eaa78a41521 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -155,6 +155,9 @@ const ONYXKEYS = { /** Whether the user has tried focus mode yet */ NVP_TRY_FOCUS_MODE: 'tryFocusMode', + /** Whether the user has been shown the hold educational interstitial yet */ + NVP_HOLD_USE_EXPLAINED: 'holdUseExplained', + /** Boolean flag used to display the focus mode notification */ FOCUS_MODE_NOTIFICATION: 'focusModeNotification', @@ -390,6 +393,7 @@ type OnyxValues = { [ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE]: OnyxTypes.BlockedFromConcierge; [ONYXKEYS.NVP_PRIVATE_PUSH_NOTIFICATION_ID]: string; [ONYXKEYS.NVP_TRY_FOCUS_MODE]: boolean; + [ONYXKEYS.NVP_HOLD_USE_EXPLAINED]: boolean; [ONYXKEYS.FOCUS_MODE_NOTIFICATION]: boolean; [ONYXKEYS.NVP_LAST_PAYMENT_METHOD]: Record; [ONYXKEYS.NVP_RECENT_WAYPOINTS]: OnyxTypes.RecentWaypoint[]; diff --git a/src/components/MoneyRequestHeader.js b/src/components/MoneyRequestHeader.js index 5f889d78a5ef..4e69c9414a92 100644 --- a/src/components/MoneyRequestHeader.js +++ b/src/components/MoneyRequestHeader.js @@ -24,6 +24,7 @@ import HoldBanner from './HoldBanner'; import * as Expensicons from './Icon/Expensicons'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; import participantPropTypes from './participantPropTypes'; +import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; import transactionPropTypes from './transactionPropTypes'; const propTypes = { @@ -54,6 +55,8 @@ const propTypes = { /** All the data for the transaction */ transaction: transactionPropTypes, + + shownHoldUseExplaination: PropTypes.bool, }; const defaultProps = { @@ -63,10 +66,11 @@ const defaultProps = { parentReport: {}, parentReportAction: {}, transaction: {}, + shownHoldUseExplaination: true, policy: {}, }; -function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, policy, personalDetails}) { +function MoneyRequestHeader({session, parentReport, report, parentReportAction, transaction, shownHoldUseExplaination, policy, personalDetails}) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); @@ -136,6 +140,30 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, } } + const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); + + useEffect(() => { + setShouldShowHoldMenu(isOnHold && !shownHoldUseExplaination); + }, [isOnHold, shownHoldUseExplaination]); + + // eslint-disable-next-line rulesdir/prefer-early-return + useEffect(() => { + if (shouldShowHoldMenu) { + if (isSmallScreenWidth) { + if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD) { + Navigation.goBack(); + } + } else { + Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD); + } + } + }, [isSmallScreenWidth, shouldShowHoldMenu]); + + const handleHoldRequestClose = () => { + setShouldShowHoldMenu(false); + IOU.setShownHoldUseExplaination(); + }; + if (canModifyRequest) { if (!TransactionUtils.hasReceipt(transaction)) { threeDotsMenuItems.push({ @@ -196,6 +224,13 @@ function MoneyRequestHeader({session, parentReport, report, parentReportAction, cancelText={translate('common.cancel')} danger /> + {isSmallScreenWidth && shouldShowHoldMenu && ( + + )} ); } @@ -225,5 +260,9 @@ export default compose( return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`; }, }, + shownHoldUseExplaination: { + key: ONYXKEYS.NVP_HOLD_USE_EXPLAINED, + initWithStoredValues: false, + }, }), )(MoneyRequestHeader); diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index db60b0a30cb1..c5fc3b1e37de 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -3484,6 +3484,10 @@ function setMoneyRequestReceipt(receiptPath, receiptFilename) { Onyx.merge(ONYXKEYS.IOU, {receiptPath, receiptFilename, merchant: ''}); } +function setShownHoldUseExplaination() { + Onyx.set(ONYXKEYS.NVP_HOLD_USE_EXPLAINED, true); +} + function setUpDistanceTransaction() { const transactionID = NumberUtils.rand64(); Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { @@ -3725,6 +3729,7 @@ export { setMoneyRequestTaxAmount, setMoneyRequestTaxRate, setUpDistanceTransaction, + setShownHoldUseExplaination, navigateToNextPage, updateMoneyRequestDate, updateMoneyRequestMerchant, diff --git a/src/pages/ProcessMoneyRequestHoldPage.js b/src/pages/ProcessMoneyRequestHoldPage.js index c9de16f874a2..2e2340b9ec78 100644 --- a/src/pages/ProcessMoneyRequestHoldPage.js +++ b/src/pages/ProcessMoneyRequestHoldPage.js @@ -8,13 +8,14 @@ import TextPill from '@components/TextPill'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import * as IOU from '@userActions/IOU'; function ProcessMoneyRequestHoldPage() { const styles = useThemeStyles(); const {translate} = useLocalize(); const onConfirm = useCallback(() => { - // Currently only goes back, this will be changed after backends for hold will be merged + IOU.setShownHoldUseExplaination(); Navigation.goBack(); }, []); From e77b28d9e1083f24594e26be0700135098f380ac Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 17 Jan 2024 12:13:36 +0100 Subject: [PATCH 056/635] fix: add correct spanish translations --- src/languages/es.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 06c764fc4449..3474f5d7167b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -643,19 +643,19 @@ export default { }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', - holdRequest: 'Solicitud de retención', - unholdRequest: 'Solicitud de cancelación de retención', - explainHold: 'Explique por qué mantiene esta solicitud.', + holdRequest: 'Retener Pedido', + unholdRequest: 'Cancelar Retención', + explainHold: 'Explica la razón para retener este pedido.', reason: 'Razón', - holdReasonRequired: 'Se requiere una razón al sostener.', - requestOnHold: 'Esta solicitud quedó en suspenso. Revise los comentarios para los próximos pasos.', + holdReasonRequired: 'Se requiere una razón para retener.', + requestOnHold: 'Este pedido está en retener. Revise los comentarios para los próximos pasos.', confirmApprove: 'Confirmar qué aprobar', confirmApprovalAmount: 'Aprobar el total del informe completo o solo el monto no retenido.', confirmPay: 'Confirmar que pagar', confirmPayAmount: 'Pague todos los gastos de bolsillo o solo el monto no retenido.', - payOnly: 'Paga solo', - approveOnly: 'Aprobar sólo', - hold: 'Hold', + payOnly: 'Solo pagar', + approveOnly: 'Solo aprobar', + hold: 'Retención', holdEducationalTitle: 'Esta solicitud está en', whatIsHoldTitle: '¿Qué es Hold?', whatIsHoldExplain: 'Hold es nuestra forma de agilizar la colaboración financiera. ¡"Rechazar" es tan duro!', From bc0a36a17f0080492ff63db5ec49b8a94de6467a Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 17 Jan 2024 17:01:05 +0100 Subject: [PATCH 057/635] ValuePicker migration --- ...electorModal.js => ValueSelectorModal.tsx} | 53 ++------ src/components/ValuePicker/index.js | 119 ------------------ src/components/ValuePicker/index.tsx | 64 ++++++++++ src/components/ValuePicker/types.ts | 58 +++++++++ 4 files changed, 135 insertions(+), 159 deletions(-) rename src/components/ValuePicker/{ValueSelectorModal.js => ValueSelectorModal.tsx} (51%) delete mode 100644 src/components/ValuePicker/index.js create mode 100644 src/components/ValuePicker/index.tsx create mode 100644 src/components/ValuePicker/types.ts diff --git a/src/components/ValuePicker/ValueSelectorModal.js b/src/components/ValuePicker/ValueSelectorModal.tsx similarity index 51% rename from src/components/ValuePicker/ValueSelectorModal.js rename to src/components/ValuePicker/ValueSelectorModal.tsx index e45ba873d8a3..61588b9f8e37 100644 --- a/src/components/ValuePicker/ValueSelectorModal.js +++ b/src/components/ValuePicker/ValueSelectorModal.tsx @@ -1,5 +1,3 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; import React, {useEffect, useState} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; @@ -7,45 +5,22 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; +import type {ValuePickerItem, ValueSelectorModalProps} from './types'; -const propTypes = { - /** Whether the modal is visible */ - isVisible: PropTypes.bool.isRequired, - - /** Items to pick from */ - items: PropTypes.arrayOf(PropTypes.shape({value: PropTypes.string, label: PropTypes.string})), - - /** The selected item */ - selectedItem: PropTypes.shape({value: PropTypes.string, label: PropTypes.string}), - - /** Label for values */ - label: PropTypes.string, - - /** Function to call when the user selects a item */ - onItemSelected: PropTypes.func, - - /** Function to call when the user closes the modal */ - onClose: PropTypes.func, - - /** Whether to show the toolip text */ - shouldShowTooltips: PropTypes.bool, -}; - -const defaultProps = { - items: [], - selectedItem: {}, - label: '', - onClose: () => {}, - onItemSelected: () => {}, - shouldShowTooltips: true, -}; - -function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onItemSelected, shouldShowTooltips}) { +function ValueSelectorModal({items = [], selectedItem, label = '', isVisible, onClose, onItemSelected, shouldShowTooltips}: ValueSelectorModalProps) { const styles = useThemeStyles(); - const [sectionsData, setSectionsData] = useState([]); + const [sectionsData, setSectionsData] = useState([]); useEffect(() => { - const itemsData = _.map(items, (item) => ({value: item.value, alternateText: item.description, keyForList: item.value, text: item.label, isSelected: item === selectedItem})); + const itemsData = items.map((item, index) => ({ + value: item?.value, + alternateText: item?.description, + keyForList: item.value ?? '', + text: item?.label ?? '', + isSelected: item === selectedItem, + sectionIndex: 0, + index, + })); setSectionsData(itemsData); }, [items, selectedItem]); @@ -71,7 +46,7 @@ function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onI @@ -80,8 +55,6 @@ function ValueSelectorModal({items, selectedItem, label, isVisible, onClose, onI ); } -ValueSelectorModal.propTypes = propTypes; -ValueSelectorModal.defaultProps = defaultProps; ValueSelectorModal.displayName = 'ValueSelectorModal'; export default ValueSelectorModal; diff --git a/src/components/ValuePicker/index.js b/src/components/ValuePicker/index.js deleted file mode 100644 index d90529114af4..000000000000 --- a/src/components/ValuePicker/index.js +++ /dev/null @@ -1,119 +0,0 @@ -import _ from 'lodash'; -import PropTypes from 'prop-types'; -import React, {useState} from 'react'; -import {View} from 'react-native'; -import FormHelpMessage from '@components/FormHelpMessage'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import refPropTypes from '@components/refPropTypes'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import variables from '@styles/variables'; -import ValueSelectorModal from './ValueSelectorModal'; - -const propTypes = { - /** Form Error description */ - errorText: PropTypes.string, - - /** Item to display */ - value: PropTypes.string, - - /** A placeholder value to display */ - placeholder: PropTypes.string, - - /** Items to pick from */ - items: PropTypes.arrayOf(PropTypes.shape({value: PropTypes.string, label: PropTypes.string})), - - /** Label of picker */ - label: PropTypes.string, - - /** Callback to call when the input changes */ - onInputChange: PropTypes.func, - - /** Text to display under the main menu item */ - furtherDetails: PropTypes.string, - - /** A ref to forward to MenuItemWithTopDescription */ - forwardedRef: refPropTypes, - - /** Whether to show the toolip text */ - shouldShowTooltips: PropTypes.bool, -}; - -const defaultProps = { - value: undefined, - label: undefined, - placeholder: '', - items: {}, - forwardedRef: undefined, - errorText: '', - furtherDetails: undefined, - onInputChange: () => {}, - shouldShowTooltips: true, -}; - -function ValuePicker({value, label, items, placeholder, errorText, onInputChange, furtherDetails, shouldShowTooltips, forwardedRef}) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const [isPickerVisible, setIsPickerVisible] = useState(false); - - const showPickerModal = () => { - setIsPickerVisible(true); - }; - - const hidePickerModal = () => { - setIsPickerVisible(false); - }; - - const updateInput = (item) => { - if (item.value !== value) { - onInputChange(item.value); - } - hidePickerModal(); - }; - - const descStyle = !value || value.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null; - const selectedItem = _.find(items, {value}); - const selectedLabel = selectedItem ? selectedItem.label : ''; - - return ( - - - - - - - - ); -} - -ValuePicker.propTypes = propTypes; -ValuePicker.defaultProps = defaultProps; -ValuePicker.displayName = 'ValuePicker'; - -const ValuePickerWithRef = React.forwardRef((props, ref) => ( - -)); - -ValuePickerWithRef.displayName = 'ValuePickerWithRef'; - -export default ValuePickerWithRef; diff --git a/src/components/ValuePicker/index.tsx b/src/components/ValuePicker/index.tsx new file mode 100644 index 000000000000..1137a3a00d81 --- /dev/null +++ b/src/components/ValuePicker/index.tsx @@ -0,0 +1,64 @@ +import React, {forwardRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; +import {View} from 'react-native'; +import FormHelpMessage from '@components/FormHelpMessage'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import type {ValuePickerItem, ValuePickerProps} from './types'; +import ValueSelectorModal from './ValueSelectorModal'; + +function ValuePicker({value, label, items, placeholder = '', errorText = '', onInputChange, furtherDetails, shouldShowTooltips = true}: ValuePickerProps, forwardedRef: ForwardedRef) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const [isPickerVisible, setIsPickerVisible] = useState(false); + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const updateInput = (item: ValuePickerItem) => { + if (item?.value && item.value !== value) { + onInputChange?.(item.value); + } + hidePickerModal(); + }; + + const descStyle = value?.length === 0 ? StyleUtils.getFontSizeStyle(variables.fontSizeLabel) : null; + const selectedItem = items?.find((item) => item.value === value); + + return ( + + + + + + + + ); +} + +ValuePicker.displayName = 'ValuePicker'; + +export default forwardRef(ValuePicker); diff --git a/src/components/ValuePicker/types.ts b/src/components/ValuePicker/types.ts new file mode 100644 index 000000000000..70bdfbe1f302 --- /dev/null +++ b/src/components/ValuePicker/types.ts @@ -0,0 +1,58 @@ +import type {RadioItem} from '@components/SelectionList/types'; + +type ValuePickerItem = RadioItem & { + value?: string; + label?: string; + description?: string; +}; + +type ValueSelectorModalProps = { + /** Whether the modal is visible */ + isVisible: boolean; + + /** Items to pick from */ + items?: ValuePickerItem[]; + + /** The selected item */ + selectedItem?: ValuePickerItem; + + /** Label for values */ + label?: string; + + /** Function to call when the user selects a item */ + onItemSelected: (item: ValuePickerItem) => void; + + /** Function to call when the user closes the modal */ + onClose: () => void; + + /** Whether to show the toolip text */ + shouldShowTooltips?: boolean; +}; + +type ValuePickerProps = { + /** Item to display */ + value?: string; + + /** Label of picker */ + label?: string; + + /** Items to pick from */ + items?: ValuePickerItem[]; + + /** A placeholder value to display */ + placeholder?: string; + + /** Form Error description */ + errorText?: string; + + /** Callback to call when the input changes */ + onInputChange?: (value: string) => void; + + /** Text to display under the main menu item */ + furtherDetails?: string; + + /** Whether to show the toolip text */ + shouldShowTooltips?: boolean; +}; + +export type {ValuePickerItem, ValueSelectorModalProps, ValuePickerProps}; From 8045ca6f214867395c2d0af3a99fcfcfba4b241e Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Wed, 17 Jan 2024 19:17:49 +0100 Subject: [PATCH 058/635] Add optional mark to props --- src/components/ValuePicker/ValueSelectorModal.tsx | 4 ++-- src/components/ValuePicker/types.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ValuePicker/ValueSelectorModal.tsx b/src/components/ValuePicker/ValueSelectorModal.tsx index 61588b9f8e37..a7b5f494aa67 100644 --- a/src/components/ValuePicker/ValueSelectorModal.tsx +++ b/src/components/ValuePicker/ValueSelectorModal.tsx @@ -28,7 +28,7 @@ function ValueSelectorModal({items = [], selectedItem, label = '', isVisible, on onClose?.()} onModalHide={onClose} hideModalContentWhileAnimating useNativeDriver @@ -45,7 +45,7 @@ function ValueSelectorModal({items = [], selectedItem, label = '', isVisible, on /> onItemSelected?.(item)} initiallyFocusedOptionKey={selectedItem?.value} shouldStopPropagation shouldShowTooltips={shouldShowTooltips} diff --git a/src/components/ValuePicker/types.ts b/src/components/ValuePicker/types.ts index 70bdfbe1f302..285299bc0101 100644 --- a/src/components/ValuePicker/types.ts +++ b/src/components/ValuePicker/types.ts @@ -20,10 +20,10 @@ type ValueSelectorModalProps = { label?: string; /** Function to call when the user selects a item */ - onItemSelected: (item: ValuePickerItem) => void; + onItemSelected?: (item: ValuePickerItem) => void; /** Function to call when the user closes the modal */ - onClose: () => void; + onClose?: () => void; /** Whether to show the toolip text */ shouldShowTooltips?: boolean; From 00e958e057588cdb6bff53dc254e324546d21800 Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 18 Jan 2024 18:33:45 +0700 Subject: [PATCH 059/635] fix: Pin & Delete request appear on Receipt image when quickly tap on receipt and 3-Dot menu --- src/components/Modal/BaseModal.tsx | 9 +++--- src/components/Modal/types.ts | 3 ++ src/components/Popover/index.tsx | 2 ++ .../PopoverWithoutOverlay/index.tsx | 2 +- src/components/ThreeDotsMenu/index.js | 30 ++++++++++++++++--- src/libs/actions/Modal.ts | 4 +-- src/types/onyx/Modal.ts | 2 ++ 7 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 6e5b4eddae9e..4bec71b5a2dd 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -38,6 +38,7 @@ function BaseModal( onLayout, avoidKeyboard = false, children, + isPopover = false, }: BaseModalProps, ref: React.ForwardedRef, ) { @@ -57,7 +58,7 @@ function BaseModal( */ const hideModal = useCallback( (callHideCallback = true) => { - Modal.willAlertModalBecomeVisible(false); + Modal.willAlertModalBecomeVisible(false, isPopover); if (shouldSetModalVisibility) { Modal.setModalVisibility(false); } @@ -69,14 +70,14 @@ function BaseModal( ComposerFocusManager.setReadyToFocus(); } }, - [shouldSetModalVisibility, onModalHide, fullscreen], + [shouldSetModalVisibility, onModalHide, fullscreen, isPopover], ); useEffect(() => { isVisibleRef.current = isVisible; let removeOnCloseListener: () => void; if (isVisible) { - Modal.willAlertModalBecomeVisible(true); + Modal.willAlertModalBecomeVisible(true, isPopover); // To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu removeOnCloseListener = Modal.setCloseModal(onClose); } @@ -87,7 +88,7 @@ function BaseModal( } removeOnCloseListener(); }; - }, [isVisible, wasVisible, onClose]); + }, [isVisible, wasVisible, onClose, isPopover]); useEffect( () => () => { diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 0773f0741233..d6fdd4cabea8 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -59,6 +59,9 @@ type BaseModalProps = Partial & { * See: https://github.com/react-native-modal/react-native-modal/pull/116 * */ hideModalContentWhileAnimating?: boolean; + + /** Whether the modal is popover or not */ + isPopover?: boolean; }; export default BaseModalProps; diff --git a/src/components/Popover/index.tsx b/src/components/Popover/index.tsx index 762e79fab63c..246286abdcbc 100644 --- a/src/components/Popover/index.tsx +++ b/src/components/Popover/index.tsx @@ -69,6 +69,7 @@ function Popover(props: PopoverWithWindowDimensionsProps) { onLayout={onLayout} animationIn={animationIn} animationOut={animationOut} + isPopover />, document.body, ); @@ -100,6 +101,7 @@ function Popover(props: PopoverWithWindowDimensionsProps) { onLayout={onLayout} animationIn={animationIn} animationOut={animationOut} + isPopover /> ); } diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index 58d022ef9d65..437fb4946f86 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -59,7 +59,7 @@ function PopoverWithoutOverlay( close(anchorRef); Modal.onModalDidClose(); } - Modal.willAlertModalBecomeVisible(isVisible); + Modal.willAlertModalBecomeVisible(isVisible, true); return () => { if (!removeOnClose) { diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.js index 150487b2aa57..cc90067a2e70 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.js @@ -1,6 +1,7 @@ import PropTypes from 'prop-types'; -import React, {useRef, useState} from 'react'; +import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -13,6 +14,7 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ThreeDotsMenuItemPropTypes from './ThreeDotsMenuItemPropTypes'; const propTypes = { @@ -57,6 +59,13 @@ const propTypes = { /** Should we announce the Modal visibility changes? */ shouldSetModalVisibility: PropTypes.bool, + + /** Details about any modals being used */ + modal: PropTypes.shape({ + isVisible: PropTypes.bool, + willAlertModalBecomeVisible: PropTypes.bool, + isPopover: PropTypes.bool, + }), }; const defaultProps = { @@ -72,14 +81,16 @@ const defaultProps = { }, shouldOverlay: false, shouldSetModalVisibility: true, + modal: {}, }; -function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment, shouldOverlay, shouldSetModalVisibility, disabled}) { +function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment, shouldOverlay, shouldSetModalVisibility, disabled, modal}) { const theme = useTheme(); const styles = useThemeStyles(); const [isPopupMenuVisible, setPopupMenuVisible] = useState(false); const buttonRef = useRef(null); const {translate} = useLocalize(); + const isBehindModal = modal.willAlertModalBecomeVisible && !modal.isPopover && !shouldOverlay; const showPopoverMenu = () => { setPopupMenuVisible(true); @@ -89,6 +100,13 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me setPopupMenuVisible(false); }; + useEffect(() => { + if (!isBehindModal || !isPopupMenuVisible) { + return; + } + hidePopoverMenu(); + }, [isBehindModal, isPopupMenuVisible]); + return ( <> @@ -126,7 +144,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me Date: Thu, 18 Jan 2024 16:13:41 +0100 Subject: [PATCH 060/635] feat: migrate HoldReasonPage to ts --- .../{HoldReasonPage.js => HoldReasonPage.tsx} | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) rename src/pages/iou/{HoldReasonPage.js => HoldReasonPage.tsx} (64%) diff --git a/src/pages/iou/HoldReasonPage.js b/src/pages/iou/HoldReasonPage.tsx similarity index 64% rename from src/pages/iou/HoldReasonPage.js rename to src/pages/iou/HoldReasonPage.tsx index 777d35ca2002..035b1b98be90 100644 --- a/src/pages/iou/HoldReasonPage.js +++ b/src/pages/iou/HoldReasonPage.tsx @@ -1,8 +1,6 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useCallback, useRef} from 'react'; +import type {RouteProp} from '@react-navigation/native'; +import React, {useCallback} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -12,48 +10,49 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import * as ValidationUtils from '@libs/ValidationUtils'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; +import type {Route} from '@src/ROUTES'; -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: PropTypes.shape({ - /** Route specific parameters used on this screen via route :iouType/new/category/:reportID? */ - params: PropTypes.shape({ - /** ID of the transaction the page was opened for */ - transactionID: PropTypes.string, +type HoldReasonPageRouteParams = { + /** ID of the transaction the page was opened for */ + transactionID: string; - /** ID of the report that user is providing hold reason to */ - reportID: PropTypes.string, + /** ID of the report that user is providing hold reason to */ + reportID: string; - /** Link to previous page */ - backTo: PropTypes.string, - }), - }).isRequired, + /** Link to previous page */ + backTo: Route; }; -function HoldReasonPage({route}) { +type HoldReasonPageProps = { + /** Navigation route context info provided by react navigation */ + route: RouteProp<{params: HoldReasonPageRouteParams}>; +}; + +type FormValues = {comment: string}; + +function HoldReasonPage({route}: HoldReasonPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const reasonRef = useRef(); - const transactionID = lodashGet(route, 'params.transactionID', ''); - const reportID = lodashGet(route, 'params.reportID', ''); - const backTo = lodashGet(route, 'params.backTo', ''); + const {transactionID, reportID, backTo} = route.params; const navigateBack = () => { Navigation.navigate(backTo); }; - const onSubmit = (values) => { + const onSubmit = (values: FormValues) => { IOU.putOnHold(transactionID, values.comment, reportID); navigateBack(); }; - const validate = useCallback((value) => { - const errors = {}; + const validate = useCallback((values: FormValues) => { + const requiredFields = ['comment']; + const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); - if (_.isEmpty(value.comment)) { + if (!values.comment) { errors.comment = 'common.error.fieldRequired'; } @@ -70,6 +69,7 @@ function HoldReasonPage({route}) { title={translate('iou.holdRequest')} onBackButtonPress={navigateBack} /> + {/** @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} {translate('iou.explainHold')} (reasonRef.current = e)} autoFocus /> @@ -99,6 +99,5 @@ function HoldReasonPage({route}) { } HoldReasonPage.displayName = 'MoneyRequestHoldReasonPage'; -HoldReasonPage.propTypes = propTypes; export default HoldReasonPage; From e06439574fdd6054acae8a483462cdc49e0d46ef Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Thu, 18 Jan 2024 16:20:18 +0100 Subject: [PATCH 061/635] feat: migrate ProcessMoneyRequestHoldPage to ts --- ...essMoneyRequestHoldPage.js => ProcessMoneyRequestHoldPage.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/pages/{ProcessMoneyRequestHoldPage.js => ProcessMoneyRequestHoldPage.tsx} (100%) diff --git a/src/pages/ProcessMoneyRequestHoldPage.js b/src/pages/ProcessMoneyRequestHoldPage.tsx similarity index 100% rename from src/pages/ProcessMoneyRequestHoldPage.js rename to src/pages/ProcessMoneyRequestHoldPage.tsx From d1b5b355b1986240abe3e76c60977324efc24a1a Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 18 Jan 2024 23:55:12 +0100 Subject: [PATCH 062/635] fix: added onBackButtonPress prop --- src/pages/ReportParticipantsPage.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 9e480d2f0516..80253c1f1793 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -99,6 +99,7 @@ function ReportParticipantsPage(props) { {({safeAreaPaddingBottomStyle}) => ( Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID))} title={props.translate( ReportUtils.isChatRoom(props.report) || ReportUtils.isPolicyExpenseChat(props.report) || @@ -121,12 +122,7 @@ function ReportParticipantsPage(props) { }, ]} onSelectRow={(option) => { - Navigation.navigate( - ROUTES.PROFILE.getRoute( - option.accountID, - ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID) - ) - ); + Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID))); }} hideSectionHeaders showTitleTooltip From d96edaece8a187a4c6174f24f5bb3e1ba5ec6954 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 19 Jan 2024 16:02:16 +0700 Subject: [PATCH 063/635] clean code --- src/pages/settings/Wallet/PaymentMethodList.tsx | 1 + src/pages/settings/Wallet/WalletPage/WalletPage.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index db04bfb9f183..48f6e1b671f4 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -79,6 +79,7 @@ type PaymentMethodListProps = PaymentMethodListOnyxProps & { /** Type to filter the payment Method list */ filterType?: TupleToUnion; + /** Whether the add bank account button should be shown on the list */ shouldShowAddBankAccount?: boolean; diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 0a24e7da9d81..230ad920171d 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -13,6 +13,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import KYCWall from '@components/KYCWall'; +import {Source, TransferMethod} from '@components/KYCWall/types'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Popover from '@components/Popover'; @@ -38,7 +39,6 @@ import ROUTES from '@src/ROUTES'; import type {AccountData} from '@src/types/onyx'; import type IconAsset from '@src/types/utils/IconAsset'; import type {WalletPageOnyxProps, WalletPageProps} from './types'; -import { Source, TransferMethod } from '@components/KYCWall/types'; type FormattedSelectedPaymentMethod = { title: string; @@ -408,7 +408,10 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi source={hasActivatedWallet ? CONST.KYC_WALL_SOURCE.TRANSFER_BALANCE : CONST.KYC_WALL_SOURCE.ENABLE_WALLET} shouldIncludeDebitCard={hasActivatedWallet} > - {(triggerKYCFlow: (event: SyntheticEvent, iouPaymentType: TransferMethod) => void, buttonRef: ForwardedRef) => { + {( + triggerKYCFlow: (event: SyntheticEvent, iouPaymentType: TransferMethod) => void, + buttonRef: ForwardedRef, + ) => { if (shouldShowLoadingSpinner) { return null; } From 5f6ee9c528d133f78b5e50fdc2b2c13d9862d045 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 22 Jan 2024 16:26:12 +0700 Subject: [PATCH 064/635] change BaseKYCWall type --- src/components/KYCWall/BaseKYCWall.tsx | 5 +++-- src/components/KYCWall/types.ts | 6 +++--- src/components/Popover/types.ts | 2 +- src/pages/settings/Wallet/PaymentMethodList.tsx | 2 +- src/pages/settings/Wallet/WalletPage/WalletPage.tsx | 10 +++++----- src/types/onyx/WalletTerms.ts | 5 ++--- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx index 04c8397bc33b..15f9b295325e 100644 --- a/src/components/KYCWall/BaseKYCWall.tsx +++ b/src/components/KYCWall/BaseKYCWall.tsx @@ -2,6 +2,7 @@ import React, {useCallback, useEffect, useRef, useState} from 'react'; import type {SyntheticEvent} from 'react'; import {Dimensions} from 'react-native'; import type {EmitterSubscription, NativeTouchEvent} from 'react-native'; +import type {GestureResponderEvent} from 'react-native-modal'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; @@ -146,7 +147,7 @@ function KYCWall({ * */ const continueAction = useCallback( - (event?: SyntheticEvent, iouPaymentType?: TransferMethod) => { + (event?: GestureResponderEvent | KeyboardEvent | SyntheticEvent, iouPaymentType?: TransferMethod) => { const currentSource = walletTerms?.source ?? source; /** @@ -161,7 +162,7 @@ function KYCWall({ } // Use event target as fallback if anchorRef is null for safety - const targetElement = anchorRef.current ?? (event?.nativeEvent.target as HTMLDivElement); + const targetElement = anchorRef.current ?? ((event as SyntheticEvent)?.nativeEvent.target as HTMLDivElement); transferBalanceButtonRef.current = targetElement; diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts index 8a654cfc25d8..68374834e254 100644 --- a/src/components/KYCWall/types.ts +++ b/src/components/KYCWall/types.ts @@ -1,5 +1,5 @@ -import type {ForwardedRef, SyntheticEvent} from 'react'; -import type {NativeTouchEvent} from 'react-native'; +import type {ForwardedRef} from 'react'; +import type {GestureResponderEvent} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; @@ -67,7 +67,7 @@ type KYCWallProps = { onSuccessfulKYC: (currentSource?: Source, iouPaymentType?: TransferMethod) => void; /** Children to build the KYC */ - children: (continueAction: (event: SyntheticEvent, method: TransferMethod) => void, anchorRef: ForwardedRef) => void; + children: (continueAction: (event?: GestureResponderEvent | KeyboardEvent, method?: TransferMethod) => void, anchorRef: ForwardedRef) => void; }; export type {AnchorPosition, KYCWallProps, PaymentMethod, TransferMethod, DomRect, Source}; diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index bdc5ae493771..7a8b8d6a7f1f 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -34,7 +34,7 @@ type PopoverProps = BaseModalProps & disableAnimation?: boolean; /** Whether we don't want to show overlay */ - withoutOverlay: boolean; + withoutOverlay?: boolean; /** The dimensions of the popover */ popoverDimensions?: PopoverDimensions; diff --git a/src/pages/settings/Wallet/PaymentMethodList.tsx b/src/pages/settings/Wallet/PaymentMethodList.tsx index 48f6e1b671f4..c1fb1bbd8253 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.tsx +++ b/src/pages/settings/Wallet/PaymentMethodList.tsx @@ -128,7 +128,7 @@ function dismissError(item: PaymentMethod) { } } -function shouldShowDefaultBadge(filteredPaymentMethods: PaymentMethod[], isDefault = false) { +function shouldShowDefaultBadge(filteredPaymentMethods: PaymentMethod[], isDefault = false): boolean { if (!isDefault) { return false; } diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx index 230ad920171d..d1823beddef4 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.tsx +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.tsx @@ -1,7 +1,7 @@ import _ from 'lodash'; -import type {ForwardedRef, Ref, RefObject, SyntheticEvent} from 'react'; +import type {ForwardedRef, RefObject} from 'react'; import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent, NativeTouchEvent} from 'react-native'; +import type {GestureResponderEvent} from 'react-native'; import {ActivityIndicator, Dimensions, ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; @@ -13,7 +13,7 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; import KYCWall from '@components/KYCWall'; -import {Source, TransferMethod} from '@components/KYCWall/types'; +import type {Source, TransferMethod} from '@components/KYCWall/types'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Popover from '@components/Popover'; @@ -409,7 +409,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi shouldIncludeDebitCard={hasActivatedWallet} > {( - triggerKYCFlow: (event: SyntheticEvent, iouPaymentType: TransferMethod) => void, + triggerKYCFlow: (event?: GestureResponderEvent | KeyboardEvent, iouPaymentType?: TransferMethod) => void, buttonRef: ForwardedRef, ) => { if (shouldShowLoadingSpinner) { @@ -522,7 +522,7 @@ function WalletPage({bankAccountList = {}, cardList = {}, fundList = {}, isLoadi top: anchorPosition.anchorPositionTop, right: anchorPosition.anchorPositionRight, }} - anchorRef={paymentMethodButtonRef} + anchorRef={paymentMethodButtonRef as RefObject} > {!showConfirmDeleteModal && ( diff --git a/src/types/onyx/WalletTerms.ts b/src/types/onyx/WalletTerms.ts index f0563310859a..c2653cae0f97 100644 --- a/src/types/onyx/WalletTerms.ts +++ b/src/types/onyx/WalletTerms.ts @@ -1,5 +1,4 @@ -import type {ValueOf} from 'type-fest'; -import type CONST from '@src/CONST'; +import type {Source} from '@components/KYCWall/types'; import type * as OnyxCommon from './OnyxCommon'; type WalletTerms = { @@ -10,7 +9,7 @@ type WalletTerms = { chatReportID?: string; /** The source that triggered the KYC wall */ - source?: ValueOf; + source?: Source; /** Loading state to provide feedback when we are waiting for a request to finish */ isLoading?: boolean; From be5367fc5e9078c60afeefa7477889ada34a6838 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:13:45 +0530 Subject: [PATCH 065/635] ts fixes --- src/libs/TransactionUtils.ts | 6 +++--- tests/ui/UnreadIndicatorsTest.js | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 2e8207db860a..813bb128cb84 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -9,7 +9,7 @@ import type {PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type PolicyTaxRate from '@src/types/onyx/PolicyTaxRates'; import type {Comment, PendingFieldsCollection, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; -import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; @@ -100,7 +100,7 @@ function buildOptimisticTransaction( category = '', tag = '', billable = false, - pendingFields: PendingFieldsCollection | null = null, + pendingFields: PendingFieldsCollection | undefined = undefined, ): Transaction { // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) @@ -115,7 +115,7 @@ function buildOptimisticTransaction( } return { - ...(isNotEmptyObject(pendingFields) ? {pendingFields} : {}), + ...(!isEmptyObject(pendingFields) ? {pendingFields} : {}), transactionID, amount, currency, diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index e4d4d877f66b..88576f3dc89b 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -24,12 +24,16 @@ import appSetup from '../../src/setup'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; +import PendingMapView from '../../src/components/MapView/PendingMapView'; // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); +jest.mock('../../src/components/ConfirmedRoute.tsx', () => ( + +)); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From e255ec88eb5f77c8e5b0a77a05ec4693afd66199 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:25:05 +0530 Subject: [PATCH 066/635] test fix --- tests/ui/UnreadIndicatorsTest.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 88576f3dc89b..01a3d735104b 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -24,16 +24,13 @@ import appSetup from '../../src/setup'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -import PendingMapView from '../../src/components/MapView/PendingMapView'; // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); -jest.mock('../../src/components/ConfirmedRoute.tsx', () => ( - -)); +jest.mock('../../src/components/ConfirmedRoute.tsx'); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From 17cb86a947002e25768b4cd2db2eac268cb4a569 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:29:58 +0530 Subject: [PATCH 067/635] test fix attempt 2 --- tests/ui/UnreadIndicatorsTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 01a3d735104b..2241711497ff 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -30,7 +30,7 @@ jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); -jest.mock('../../src/components/ConfirmedRoute.tsx'); +jest.mock('../../src/components/ConfirmedRoute.tsx', (props) => props.children); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From c7165ddb967bf655bfb8843d50d051bbd5b72e53 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:45:19 +0530 Subject: [PATCH 068/635] test fix attempt 3 --- tests/ui/UnreadIndicatorsTest.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 2241711497ff..363c899d4328 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -30,7 +30,10 @@ jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); -jest.mock('../../src/components/ConfirmedRoute.tsx', (props) => props.children); +jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { + const Comp = (props) => props.children; + return Comp; +}); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From b0068d381f1e212f131fe04a7ca3107a5bf2b274 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:50:55 +0530 Subject: [PATCH 069/635] perf-test fix --- tests/perf-test/ReportActionsList.perf-test.js | 5 +++++ tests/perf-test/ReportScreen.perf-test.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tests/perf-test/ReportActionsList.perf-test.js b/tests/perf-test/ReportActionsList.perf-test.js index 8e3312cfa4c7..515447a7a2ed 100644 --- a/tests/perf-test/ReportActionsList.perf-test.js +++ b/tests/perf-test/ReportActionsList.perf-test.js @@ -44,6 +44,11 @@ jest.mock('@react-navigation/native', () => { }; }); +jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { + const Comp = (props) => props.children; + return Comp; +}); + beforeAll(() => Onyx.init({ keys: ONYXKEYS, diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index d58f71fa7ab4..97ea04ff55fb 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -29,6 +29,11 @@ jest.mock('react-native-reanimated', () => ({ useAnimatedRef: jest.fn, })); +jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { + const Comp = (props) => props.children; + return Comp; +}); + jest.mock('../../src/components/withNavigationFocus', () => (Component) => { function WithNavigationFocus(props) { return ( From 365d37967684c85c0c059744637748346c4f82a6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 23:06:12 +0530 Subject: [PATCH 070/635] perf-test fix attempt 2 --- src/components/__mocks__/ConfirmedRoute.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/components/__mocks__/ConfirmedRoute.tsx diff --git a/src/components/__mocks__/ConfirmedRoute.tsx b/src/components/__mocks__/ConfirmedRoute.tsx new file mode 100644 index 000000000000..a759a2c1e193 --- /dev/null +++ b/src/components/__mocks__/ConfirmedRoute.tsx @@ -0,0 +1,8 @@ +import {View} from "react-native"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any +function ConfirmedRoute(props: any){ + return +} + +export default ConfirmedRoute; \ No newline at end of file From fb5a70ef1a038cffa40e8c001c020b489f04c5e3 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 23:30:26 +0530 Subject: [PATCH 071/635] perf-test fix attempt 3 --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index de7ed4b1f974..b5335f07482f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,7 @@ module.exports = { `/?(*.)+(spec|test).${testFileExtension}`, ], transform: { - '^.+\\.jsx?$': 'babel-jest', + '^.+\\.[jt]sx?$': 'babel-jest', '^.+\\.svg?$': 'jest-transformer-svg', }, transformIgnorePatterns: ['/node_modules/(?!react-native)/'], From 38015459c9140e3fb4710dc2f71ee4730f778ee7 Mon Sep 17 00:00:00 2001 From: Trevor Coleman Date: Mon, 22 Jan 2024 16:50:39 -0500 Subject: [PATCH 072/635] feat(Violations): fix dependency array --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index b425c9e070e6..87b33ee6f3c4 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -264,7 +264,7 @@ function MoneyRequestPreview(props) { } return CurrencyUtils.convertToDisplayString(requestAmount, requestCurrency); - }, [hasPendingWaypoints, isDistanceRequest, isScanning, props.transaction, requestAmount, requestCurrency, translate]); + }, [hasPendingWaypoints, isDistanceRequest, isScanning, isSettled, props.transaction, requestAmount, requestCurrency, translate]); const displayDeleteAmountText = useMemo(() => { const {amount, currency} = ReportUtils.getTransactionDetails(props.action.originalMessage); From adb33f65224f4c82df7a375fdb0628e71a768010 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 23 Jan 2024 07:25:37 +0530 Subject: [PATCH 073/635] perf-test fix attempt 4 --- tests/perf-test/ReportActionsList.perf-test.js | 5 +---- tests/perf-test/ReportScreen.perf-test.js | 5 +---- tests/ui/UnreadIndicatorsTest.js | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/perf-test/ReportActionsList.perf-test.js b/tests/perf-test/ReportActionsList.perf-test.js index 515447a7a2ed..c81c4aa51df8 100644 --- a/tests/perf-test/ReportActionsList.perf-test.js +++ b/tests/perf-test/ReportActionsList.perf-test.js @@ -44,10 +44,7 @@ jest.mock('@react-navigation/native', () => { }; }); -jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { - const Comp = (props) => props.children; - return Comp; -}); +jest.mock('../../src/components/ConfirmedRoute.tsx'); beforeAll(() => Onyx.init({ diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index 97ea04ff55fb..faa72fd3a367 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -29,10 +29,7 @@ jest.mock('react-native-reanimated', () => ({ useAnimatedRef: jest.fn, })); -jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { - const Comp = (props) => props.children; - return Comp; -}); +jest.mock('../../src/components/ConfirmedRoute.tsx'); jest.mock('../../src/components/withNavigationFocus', () => (Component) => { function WithNavigationFocus(props) { diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 363c899d4328..01a3d735104b 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -30,10 +30,7 @@ jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); -jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { - const Comp = (props) => props.children; - return Comp; -}); +jest.mock('../../src/components/ConfirmedRoute.tsx'); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From 94c115adec14ae658d098d2d4e0a794e7da411fd Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 23 Jan 2024 08:16:55 +0530 Subject: [PATCH 074/635] fix lint --- src/components/__mocks__/ConfirmedRoute.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/__mocks__/ConfirmedRoute.tsx b/src/components/__mocks__/ConfirmedRoute.tsx index a759a2c1e193..3c78e764ebea 100644 --- a/src/components/__mocks__/ConfirmedRoute.tsx +++ b/src/components/__mocks__/ConfirmedRoute.tsx @@ -1,8 +1,8 @@ -import {View} from "react-native"; +import {View} from 'react-native'; // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any -function ConfirmedRoute(props: any){ - return +function ConfirmedRoute(props: any) { + return ; } -export default ConfirmedRoute; \ No newline at end of file +export default ConfirmedRoute; From 0129e58af57e4a81de7fb8eaed1e53543be24a93 Mon Sep 17 00:00:00 2001 From: Vadym Date: Tue, 23 Jan 2024 16:11:22 +0100 Subject: [PATCH 075/635] chore: converts BasePopoverReactionList to functional component --- src/hooks/useBasePopoverReactionList/index.ts | 134 +++++++ src/hooks/useBasePopoverReactionList/types.ts | 65 ++++ src/pages/home/ReportScreenContext.ts | 4 +- .../BasePopoverReactionList.tsx | 334 +++--------------- .../PopoverReactionList/index.tsx | 18 +- 5 files changed, 256 insertions(+), 299 deletions(-) create mode 100644 src/hooks/useBasePopoverReactionList/index.ts create mode 100644 src/hooks/useBasePopoverReactionList/types.ts diff --git a/src/hooks/useBasePopoverReactionList/index.ts b/src/hooks/useBasePopoverReactionList/index.ts new file mode 100644 index 000000000000..cccb8a99b526 --- /dev/null +++ b/src/hooks/useBasePopoverReactionList/index.ts @@ -0,0 +1,134 @@ +import {useEffect, useMemo, useRef, useState} from 'react'; +import type {SyntheticEvent} from 'react'; +import {Dimensions} from 'react-native'; +import * as EmojiUtils from '@libs/EmojiUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import type {BasePopoverReactionListHookProps, ReactionListAnchor, ShowReactionList} from './types'; + +export default function useBasePopoverReactionList({emojiName, emojiReactions, accountID, reportActionID, preferredLocale}: BasePopoverReactionListHookProps) { + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + const [cursorRelativePosition, setCursorRelativePosition] = useState({horizontal: 0, vertical: 0}); + const [popoverAnchorPosition, setPopoverAnchorPosition] = useState({horizontal: 0, vertical: 0}); + const reactionListRef = useRef(null); + + // Get the selected reaction + const selectedReaction = useMemo(() => (isPopoverVisible ? emojiReactions?.emojiName : null), [isPopoverVisible, emojiReactions]); + + // custom methods + function getReactionInformation() { + if (!selectedReaction) { + // If there is no reaction, we return default values + return { + emojiName: '', + reactionCount: 0, + emojiCodes: [], + hasUserReacted: false, + users: [], + }; + } + + const {emojiCodes, reactionCount, hasUserReacted, userAccountIDs} = EmojiUtils.getEmojiReactionDetails(emojiName, selectedReaction, accountID); + + const users = PersonalDetailsUtils.getPersonalDetailsByIDs(userAccountIDs, accountID, true); + return { + emojiName, + emojiCodes, + reactionCount, + hasUserReacted, + users, + }; + } + + /** + * Get the BasePopoverReactionList anchor position + * We calculate the achor coordinates from measureInWindow async method + * + * @returns promise + */ + function getReactionListMeasuredLocation(): Promise<{x: number; y: number}> { + return new Promise((resolve) => { + const reactionListAnchor = reactionListRef.current; + if (reactionListAnchor && 'measureInWindow' in reactionListAnchor) { + reactionListAnchor.measureInWindow((x, y) => resolve({x, y})); + } else { + // If the anchor is not available or does not have the measureInWindow method, we return 0, 0 + resolve({x: 0, y: 0}); + } + }); + } + + /** + * Show the ReactionList modal popover. + * + * @param event - Object - A press event. + * @param reactionListAnchor - Element - reactionListAnchor + */ + const showReactionList: ShowReactionList = (event, reactionListAnchor) => { + // We get the cursor coordinates and the reactionListAnchor coordinates to calculate the popover position + const nativeEvent = (event as SyntheticEvent)?.nativeEvent || {}; + reactionListRef.current = reactionListAnchor; + getReactionListMeasuredLocation().then(({x, y}) => { + setCursorRelativePosition({horizontal: nativeEvent.pageX - x, vertical: nativeEvent.pageY - y}); + setPopoverAnchorPosition({ + horizontal: nativeEvent.pageX, + vertical: nativeEvent.pageY, + }); + setIsPopoverVisible(true); + }); + }; + + /** + * Hide the ReactionList modal popover. + */ + function hideReactionList() { + setIsPopoverVisible(false); + } + + useEffect(() => { + const dimensionsEventListener = Dimensions.addEventListener('change', () => { + if (!isPopoverVisible) { + // If the popover is not visible, we don't need to update the component + return; + } + getReactionListMeasuredLocation().then(({x, y}) => { + if (!x || !y) { + return; + } + setPopoverAnchorPosition({ + horizontal: cursorRelativePosition.horizontal + x, + vertical: cursorRelativePosition.vertical + y, + }); + }); + }); + + return () => { + dimensionsEventListener.remove(); + }; + }, [ + isPopoverVisible, + reportActionID, + preferredLocale, + cursorRelativePosition.horizontal, + cursorRelativePosition.vertical, + popoverAnchorPosition.horizontal, + popoverAnchorPosition.vertical, + ]); + + useEffect(() => { + if (!isPopoverVisible) { + // If the popover is not visible, we don't need to update the component + return; + } + + // Hide the list when all reactions are removed + const emojiReactionsList = emojiReactions?.emojiName.users; + const isEmptyList = Array.isArray(emojiReactionsList) && !emojiReactionsList.some((emojiReaction) => emojiReaction); + if (!isEmptyList) { + return; + } + + hideReactionList(); + }); + + return {isPopoverVisible, cursorRelativePosition, popoverAnchorPosition, getReactionInformation, hideReactionList, reactionListRef, showReactionList}; +} diff --git a/src/hooks/useBasePopoverReactionList/types.ts b/src/hooks/useBasePopoverReactionList/types.ts new file mode 100644 index 000000000000..e4ba9263b41b --- /dev/null +++ b/src/hooks/useBasePopoverReactionList/types.ts @@ -0,0 +1,65 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import type {ReactionListAnchor, ReactionListEvent} from '@pages/home/ReportScreenContext'; +import type {ReportActionReactions} from '@src/types/onyx'; + +type BasePopoverReactionListOnyxProps = { + /** The reactions for the report action */ + emojiReactions: OnyxEntry; +}; + +type BasePopoverReactionListProps = { + /** The ID of the report action */ + reportActionID: string; + + /** The emoji name */ + emojiName: string; +}; + +type BasePopoverReactionListHookProps = BasePopoverReactionListProps & { + /** The reactions for the report action */ + emojiReactions: OnyxEntry; + + /** The current user's account ID */ + accountID: WithCurrentUserPersonalDetailsProps['currentUserPersonalDetails']['accountID']; + + preferredLocale: LocaleContextProps['preferredLocale']; +}; + +type BasePopoverReactionListPropsWithLocalWithOnyx = WithCurrentUserPersonalDetailsProps & BasePopoverReactionListOnyxProps & BasePopoverReactionListProps; +type BasePopoverReactionListState = { + /** Whether the popover is visible */ + isPopoverVisible: boolean; + + /** The horizontal and vertical position (relative to the screen) where the popover will display. */ + popoverAnchorPosition: { + horizontal: number; + vertical: number; + }; + + /** The horizontal and vertical position (relative to the screen) where the cursor is. */ + cursorRelativePosition: { + horizontal: number; + vertical: number; + }; +}; + +type ShowReactionList = (event: ReactionListEvent | undefined, reactionListAnchor: ReactionListAnchor) => void; + +type InnerReactionListRefType = { + showReactionList: ShowReactionList; + hideReactionList: () => void; + isActiveReportAction: (actionID: number | string) => boolean; +}; + +export type { + BasePopoverReactionListProps, + BasePopoverReactionListHookProps, + BasePopoverReactionListPropsWithLocalWithOnyx, + BasePopoverReactionListState, + BasePopoverReactionListOnyxProps, + ShowReactionList, + ReactionListAnchor, + InnerReactionListRefType, +}; diff --git a/src/pages/home/ReportScreenContext.ts b/src/pages/home/ReportScreenContext.ts index 3b4e574e01a1..e9440ab932d6 100644 --- a/src/pages/home/ReportScreenContext.ts +++ b/src/pages/home/ReportScreenContext.ts @@ -1,10 +1,10 @@ -import type {RefObject} from 'react'; +import type {RefObject, SyntheticEvent} from 'react'; import {createContext} from 'react'; import type {FlatList, GestureResponderEvent, View} from 'react-native'; type ReactionListAnchor = View | HTMLDivElement | null; -type ReactionListEvent = GestureResponderEvent | MouseEvent; +type ReactionListEvent = GestureResponderEvent | MouseEvent | SyntheticEvent; type ReactionListRef = { showReactionList: (event: ReactionListEvent | undefined, reactionListAnchor: ReactionListAnchor, emojiName: string, reportActionID: string) => void; diff --git a/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx b/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx index 4bee2e8d1d3c..3f5fdeb6428f 100644 --- a/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx +++ b/src/pages/home/report/ReactionList/PopoverReactionList/BasePopoverReactionList.tsx @@ -1,297 +1,61 @@ -import lodashGet from 'lodash/get'; import React from 'react'; -import {Dimensions} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx'; -import _ from 'underscore'; import PopoverWithMeasuredContent from '@components/PopoverWithMeasuredContent'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; -import withLocalize from '@components/withLocalize'; -import type {WithLocalizeProps} from '@components/withLocalize'; -import compose from '@libs/compose'; -import * as EmojiUtils from '@libs/EmojiUtils'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import useBasePopoverReactionList from '@hooks/useBasePopoverReactionList'; +import type {BasePopoverReactionListOnyxProps, BasePopoverReactionListPropsWithLocalWithOnyx} from '@hooks/useBasePopoverReactionList/types'; +import useLocalize from '@hooks/useLocalize'; import BaseReactionList from '@pages/home/report/ReactionList/BaseReactionList'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {ReportActionReactions} from '@src/types/onyx'; -import type {ReportActionReaction} from '@src/types/onyx/ReportActionReactions'; -type BasePopoverReactionListOnyxProps = { - /** The reactions for the report action */ - emojiReactions: OnyxEntry; -}; - -type BasePopoverReactionListProps = { - /** The ID of the report action */ - reportActionID: string; - - /** The emoji name */ - emojiName: string; - - /** The ref of the action */ - ref: React.Ref; -}; - -type BasePopoverReactionListWithLocalizeProps = WithLocalizeProps & WithCurrentUserPersonalDetailsProps; - -type BasePopoverReactionListPropsWithLocalWithOnyx = BasePopoverReactionListWithLocalizeProps & BasePopoverReactionListOnyxProps & BasePopoverReactionListProps; -type BasePopoverReactionListState = { - /** Whether the popover is visible */ - isPopoverVisible: boolean; - - /** The horizontal and vertical position (relative to the screen) where the popover will display. */ - popoverAnchorPosition: { - horizontal: number; - vertical: number; - }; - - /** The horizontal and vertical position (relative to the screen) where the cursor is. */ - cursorRelativePosition: { - horizontal: number; - vertical: number; - }; -}; - -class BasePopoverReactionList extends React.Component { - reactionListAnchor: React.RefObject; - - dimensionsEventListener: { - remove: () => void; - } | null; - - constructor(props: BasePopoverReactionListPropsWithLocalWithOnyx) { - super(props); - - this.state = { - isPopoverVisible: false, - cursorRelativePosition: { - horizontal: 0, - vertical: 0, - }, - - // The horizontal and vertical position (relative to the screen) where the popover will display. - popoverAnchorPosition: { - horizontal: 0, - vertical: 0, - }, - }; - - this.reactionListAnchor = React.createRef(); - this.showReactionList = this.showReactionList.bind(this); - this.hideReactionList = this.hideReactionList.bind(this); - this.measureReactionListPosition = this.measureReactionListPosition.bind(this); - this.getReactionListMeasuredLocation = this.getReactionListMeasuredLocation.bind(this); - this.getReactionInformation = this.getReactionInformation.bind(this); - this.dimensionsEventListener = null; - } - - componentDidMount() { - this.dimensionsEventListener = Dimensions.addEventListener('change', this.measureReactionListPosition); - } - - shouldComponentUpdate(nextProps: BasePopoverReactionListPropsWithLocalWithOnyx, nextState: BasePopoverReactionListState) { - if (!this.state.isPopoverVisible && !nextState.isPopoverVisible) { - // If the popover is not visible, we don't need to update the component - return false; - } - - const previousLocale = lodashGet(this.props, 'preferredLocale', CONST.LOCALES.DEFAULT); - const nextLocale = lodashGet(nextProps, 'preferredLocale', CONST.LOCALES.DEFAULT); - const prevReaction = lodashGet(this.props.emojiReactions, this.props.emojiName); - const nextReaction = lodashGet(nextProps.emojiReactions, nextProps.emojiName); - - return ( - this.props.reportActionID !== nextProps.reportActionID || - this.props.emojiName !== nextProps.emojiName || - !_.isEqual(prevReaction, nextReaction) || - !_.isEqual(this.state, nextState) || - previousLocale !== nextLocale - ); - } - - componentDidUpdate() { - if (!this.state.isPopoverVisible) { - // If the popover is not visible, we don't need to update the component - return; - } - - // Hide the list when all reactions are removed - const emojiReactions = lodashGet(this.props.emojiReactions, [this.props.emojiName, 'users']); - const isEmptyList = !emojiReactions || !_.some(emojiReactions); - if (!isEmptyList) { - return; - } - - this.hideReactionList(); - } - - componentWillUnmount() { - // Remove the event listener - if (!this.dimensionsEventListener) { - return; - } - this.dimensionsEventListener.remove(); - } - - /** - * Get the BasePopoverReactionList anchor position - * We calculate the achor coordinates from measureInWindow async method - * - * @returns {Promise} - */ - getReactionListMeasuredLocation(): Promise<{x: number; y: number}> { - return new Promise((resolve) => { - const reactionListAnchor = this.reactionListAnchor.current as HTMLElement & {measureInWindow: (callback: (x: number, y: number) => void) => void}; - if (reactionListAnchor) { - reactionListAnchor.measureInWindow((x, y) => resolve({x, y})); - } else { - // If the anchor is not available, we return 0, 0 - resolve({x: 0, y: 0}); - } - }); - } - - /** - * Get the reaction information. - * - * @param {Object} selectedReaction - * @param {String} emojiName - * @returns {Object} - */ - getReactionInformation(selectedReaction: ReportActionReaction | null | undefined, emojiName: string) { - if (!selectedReaction) { - // If there is no reaction, we return default values - return { - emojiName: '', - reactionCount: 0, - emojiCodes: [], - hasUserReacted: false, - users: [], - }; - } - - const {emojiCodes, reactionCount, hasUserReacted, userAccountIDs} = EmojiUtils.getEmojiReactionDetails(emojiName, selectedReaction, this.props.currentUserPersonalDetails.accountID); - - const users = PersonalDetailsUtils.getPersonalDetailsByIDs(userAccountIDs, this.props.currentUserPersonalDetails.accountID, true); - return { - emojiName, - emojiCodes, - reactionCount, - hasUserReacted, - users, - }; - } - - /** - * Show the ReactionList modal popover. - * - * @param {Object} [event] - A press event. - * @param {Element} reactionListAnchor - reactionListAnchor - */ - showReactionList( - event: { - nativeEvent: { - pageX: number; - pageY: number; - }; - }, - reactionListAnchor: HTMLElement, - ) { - // We get the cursor coordinates and the reactionListAnchor coordinates to calculate the popover position - const nativeEvent = event.nativeEvent || {}; - this.reactionListAnchor = {current: reactionListAnchor}; - this.getReactionListMeasuredLocation().then(({x, y}) => { - this.setState({ - cursorRelativePosition: { - horizontal: nativeEvent.pageX - x, - vertical: nativeEvent.pageY - y, - }, - popoverAnchorPosition: { - horizontal: nativeEvent.pageX, - vertical: nativeEvent.pageY, - }, - isPopoverVisible: true, - }); - }); - } - - /** - * This gets called on Dimensions change to find the anchor coordinates for the action BasePopoverReactionList. - */ - measureReactionListPosition() { - if (!this.state.isPopoverVisible) { - // If the popover is not visible, we don't need to update the component - return; - } - this.getReactionListMeasuredLocation().then(({x, y}) => { - if (!x || !y) { - return; - } - this.setState((prev) => ({ - popoverAnchorPosition: { - horizontal: prev.cursorRelativePosition.horizontal + x, - vertical: prev.cursorRelativePosition.vertical + y, - }, - })); - }); - } - - /** - * Hide the ReactionList modal popover. - */ - hideReactionList() { - this.setState({ - isPopoverVisible: false, - }); - } - - render() { - // Get the selected reaction - const selectedReaction = this.state.isPopoverVisible ? lodashGet(this.props.emojiReactions, [this.props.emojiName]) : null; - - // Get the reaction information - const {emojiName, emojiCodes, reactionCount, hasUserReacted, users} = this.getReactionInformation(selectedReaction, this.props.emojiName); - - return ( - - - - ); - } +function BasePopoverReactionList(props: BasePopoverReactionListPropsWithLocalWithOnyx) { + // hooks + const {emojiReactions, emojiName, reportActionID, currentUserPersonalDetails} = props; + const {preferredLocale} = useLocalize(); + const {isPopoverVisible, hideReactionList, popoverAnchorPosition, reactionListRef, getReactionInformation} = useBasePopoverReactionList({ + emojiName, + emojiReactions, + accountID: currentUserPersonalDetails.accountID, + reportActionID, + preferredLocale, + }); + // Get the reaction information + const {emojiCodes, reactionCount, hasUserReacted, users} = getReactionInformation(); + + return ( + + + + ); } -export default compose( - // @ts-ignore TODO: Fix this when the type is fixed - withLocalize, - withCurrentUserPersonalDetails, - withOnyx({ +export default withCurrentUserPersonalDetails( + withOnyx({ emojiReactions: { key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`, }, - }), -)(BasePopoverReactionList); + })(BasePopoverReactionList), +); diff --git a/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx b/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx index 8ac89cef6656..610146559786 100644 --- a/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx +++ b/src/pages/home/report/ReactionList/PopoverReactionList/index.tsx @@ -1,17 +1,11 @@ import React, {forwardRef, useImperativeHandle, useRef, useState} from 'react'; import type {ForwardedRef} from 'react'; +import type {InnerReactionListRefType} from '@hooks/useBasePopoverReactionList/types'; +import type {ReactionListRef} from '@pages/home/ReportScreenContext'; import BasePopoverReactionList from './BasePopoverReactionList'; type PopoverReactionListProps = { - ref: ForwardedRef; -}; - -type ShowReactionList = (event: React.MouseEvent, reactionListAnchor: HTMLElement, emojiName: string, reportActionID: string) => void; - -type InnerReactionListRefType = { - showReactionList: ShowReactionList; - hideReactionList: () => void; - isActiveReportAction: (actionID: number | string) => boolean; + ref: ForwardedRef; }; function PopoverReactionList(props: PopoverReactionListProps) { @@ -19,10 +13,10 @@ function PopoverReactionList(props: PopoverReactionListProps) { const [reactionListReportActionID, setReactionListReportActionID] = useState(''); const [reactionListEmojiName, setReactionListEmojiName] = useState(''); - const showReactionList: ShowReactionList = (event, reactionListAnchor, emojiName, reportActionID) => { + const showReactionList: ReactionListRef['showReactionList'] = (event, reactionListAnchor, emojiName, reportActionID) => { setReactionListReportActionID(reportActionID); setReactionListEmojiName(emojiName); - innerReactionListRef.current?.showReactionList(event, reactionListAnchor, emojiName, reportActionID); + innerReactionListRef.current?.showReactionList(event, reactionListAnchor); }; const hideReactionList = () => { @@ -45,7 +39,7 @@ function PopoverReactionList(props: PopoverReactionListProps) { PopoverReactionList.displayName = 'PopoverReactionList'; export default React.memo( - forwardRef((props, ref) => ( + forwardRef((props, ref) => ( Date: Wed, 24 Jan 2024 08:32:01 +0100 Subject: [PATCH 076/635] start migrating WorkspaceMembersPage to TypeScript --- ...embersPage.js => WorkspaceMembersPage.tsx} | 223 ++++++++++-------- 1 file changed, 127 insertions(+), 96 deletions(-) rename src/pages/workspace/{WorkspaceMembersPage.js => WorkspaceMembersPage.tsx} (67%) diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.tsx similarity index 67% rename from src/pages/workspace/WorkspaceMembersPage.js rename to src/pages/workspace/WorkspaceMembersPage.tsx index 92bc5ecc8e9c..7d29d6e72978 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -1,8 +1,11 @@ import {useIsFocused} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {TextInput} from 'react-native'; import {InteractionManager, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -10,20 +13,20 @@ import Button from '@components/Button'; import ConfirmModal from '@components/ConfirmModal'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MessagesRow from '@components/MessagesRow'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import Text from '@components/Text'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -33,8 +36,12 @@ import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetailsList, PolicyMembers, Session} from '@src/types/onyx'; +import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import SearchInputManager from './SearchInputManager'; import {policyDefaultProps, policyPropTypes} from './withPolicy'; +import type {WithPolicyAndFullscreenLoadingProps} from './withPolicyAndFullscreenLoading'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; const propTypes = { @@ -58,10 +65,32 @@ const propTypes = { isLoadingReportData: PropTypes.bool, ...policyPropTypes, - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, ...withCurrentUserPersonalDetailsPropTypes, - network: networkPropTypes.isRequired, +}; + +type WorkspaceMembersPageOnyxProps = { + personalDetails: OnyxEntry; + session: OnyxEntry; + isLoadingReportData: OnyxEntry; +}; + +type WorkspaceMembersPageProps = Omit & + WithCurrentUserPersonalDetailsProps & + WorkspaceMembersPageOnyxProps & + StackScreenProps; + +type MemberOption = { + keyForList: string; + accountID: number; + isSelected: boolean; + isDisabled: boolean; + text: string; + alternateText: string; + rightElement: React.ReactNode | null; + icons: Icon[]; + errors?: Errors; + pendingAction?: PendingAction; + invitedSecondaryLogin?: string; }; const defaultProps = { @@ -74,18 +103,20 @@ const defaultProps = { ...withCurrentUserPersonalDetailsDefaultProps, }; -function WorkspaceMembersPage(props) { +function WorkspaceMembersPage({policyMembers, personalDetails, route, policy, session, currentUserPersonalDetails, isLoadingReportData}: WorkspaceMembersPageProps) { const styles = useThemeStyles(); const [selectedEmployees, setSelectedEmployees] = useState([]); const [removeMembersConfirmModalVisible, setRemoveMembersConfirmModalVisible] = useState(false); const [errors, setErrors] = useState({}); const [searchValue, setSearchValue] = useState(''); - const prevIsOffline = usePrevious(props.network.isOffline); - const accountIDs = useMemo(() => _.map(_.keys(props.policyMembers), (accountID) => Number(accountID)), [props.policyMembers]); + const {isOffline} = useNetwork(); + const prevIsOffline = usePrevious(isOffline); + const accountIDs = useMemo(() => Object.keys(policyMembers ?? {}).map((accountID) => Number(accountID)), [policyMembers]); const prevAccountIDs = usePrevious(accountIDs); - const textInputRef = useRef(null); - const isOfflineAndNoMemberDataAvailable = _.isEmpty(props.policyMembers) && props.network.isOffline; - const prevPersonalDetails = usePrevious(props.personalDetails); + const textInputRef = useRef(null); + const isOfflineAndNoMemberDataAvailable = _.isEmpty(policyMembers) && isOffline; + const prevPersonalDetails = usePrevious(personalDetails); + const {translate, formatPhoneNumber, preferredLocale} = useLocalize(); const isFocusedScreen = useIsFocused(); @@ -93,51 +124,49 @@ function WorkspaceMembersPage(props) { setSearchValue(SearchInputManager.searchInput); }, [isFocusedScreen]); - useEffect(() => () => (SearchInputManager.searchInput = ''), []); + useEffect(() => { + SearchInputManager.searchInput = ''; + }, []); /** * Get filtered personalDetails list with current policyMembers - * @param {Object} policyMembers - * @param {Object} personalDetails - * @returns {Object} + * @param policyMembers + * @param personalDetails + * @returns */ - const filterPersonalDetails = (policyMembers, personalDetails) => - _.reduce( - _.keys(policyMembers), - (result, key) => { - if (personalDetails[key]) { - return { - ...result, - [key]: personalDetails[key], - }; - } - return result; - }, - {}, - ); + const filterPersonalDetails = (members: OnyxEntry, details: OnyxEntry) => + Object.keys(members ?? {}).reduce((result, key) => { + if (details?.[key]) { + return { + ...result, + [key]: details[key], + }; + } + return result; + }, {}); /** * Get members for the current workspace */ const getWorkspaceMembers = useCallback(() => { - Policy.openWorkspaceMembersPage(props.route.params.policyID, _.keys(PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails))); - }, [props.route.params.policyID, props.policyMembers, props.personalDetails]); + Policy.openWorkspaceMembersPage(route.params.policyID, Object.keys(PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetails))); + }, [route.params.policyID, policyMembers, personalDetails]); /** * Check if the current selection includes members that cannot be removed */ const validateSelection = useCallback(() => { const newErrors = {}; - const ownerAccountID = _.first(PersonalDetailsUtils.getAccountIDsByLogins(props.policy.owner ? [props.policy.owner] : [])); + const ownerAccountID = PersonalDetailsUtils.getAccountIDsByLogins(policy?.owner ? [policy.owner] : [])[0]; _.each(selectedEmployees, (member) => { - if (member !== ownerAccountID && member !== props.session.accountID) { + if (member !== ownerAccountID && member !== session.accountID) { return; } - newErrors[member] = props.translate('workspace.people.error.cannotRemove'); + newErrors[member] = translate('workspace.people.error.cannotRemove'); }); setErrors(newErrors); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedEmployees, props.policy.owner, props.session.accountID]); + }, [selectedEmployees, policy?.owner, session?.accountID]); useEffect(() => { getWorkspaceMembers(); @@ -146,7 +175,7 @@ function WorkspaceMembersPage(props) { useEffect(() => { validateSelection(); - }, [props.preferredLocale, validateSelection]); + }, [preferredLocale, validateSelection]); useEffect(() => { if (removeMembersConfirmModalVisible && !_.isEqual(accountIDs, prevAccountIDs)) { @@ -154,32 +183,32 @@ function WorkspaceMembersPage(props) { } setSelectedEmployees((prevSelected) => { // Filter all personal details in order to use the elements needed for the current workspace - const currentPersonalDetails = filterPersonalDetails(props.policyMembers, props.personalDetails); + const currentPersonalDetails = filterPersonalDetails(policyMembers, personalDetails); // We need to filter the previous selected employees by the new personal details, since unknown/new user id's change when transitioning from offline to online const prevSelectedElements = _.map(prevSelected, (id) => { const prevItem = lodashGet(prevPersonalDetails, id); const res = _.find(_.values(currentPersonalDetails), (item) => lodashGet(prevItem, 'login') === lodashGet(item, 'login')); return lodashGet(res, 'accountID', id); }); - return _.intersection(prevSelectedElements, _.values(PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails))); + return _.intersection(prevSelectedElements, _.values(PolicyUtils.getMemberAccountIDsForWorkspace(policyMembers, personalDetails))); }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.policyMembers]); + }, [policyMembers]); useEffect(() => { - const isReconnecting = prevIsOffline && !props.network.isOffline; + const isReconnecting = prevIsOffline && !isOffline; if (!isReconnecting) { return; } getWorkspaceMembers(); - }, [props.network.isOffline, prevIsOffline, getWorkspaceMembers]); + }, [isOffline, prevIsOffline, getWorkspaceMembers]); /** * Open the modal to invite a user */ const inviteUser = () => { setSearchValue(''); - Navigation.navigate(ROUTES.WORKSPACE_INVITE.getRoute(props.route.params.policyID)); + Navigation.navigate(ROUTES.WORKSPACE_INVITE.getRoute(route.params.policyID)); }; /** @@ -191,9 +220,9 @@ function WorkspaceMembersPage(props) { } // Remove the admin from the list - const accountIDsToRemove = _.without(selectedEmployees, props.session.accountID); + const accountIDsToRemove = _.without(selectedEmployees, session.accountID); - Policy.removeMembers(accountIDsToRemove, props.route.params.policyID); + Policy.removeMembers(accountIDsToRemove, route.params.policyID); setSelectedEmployees([]); setRemoveMembersConfirmModalVisible(false); }; @@ -210,7 +239,7 @@ function WorkspaceMembersPage(props) { /** * Add or remove all users passed from the selectedEmployees list - * @param {Object} memberList + * @param memberList */ const toggleAllUsers = (memberList) => { const enabledAccounts = _.filter(memberList, (member) => !member.isDisabled); @@ -283,37 +312,39 @@ function WorkspaceMembersPage(props) { const dismissError = useCallback( (item) => { if (item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { - Policy.clearDeleteMemberError(props.route.params.policyID, item.accountID); + Policy.clearDeleteMemberError(route.params.policyID, item.accountID); } else { - Policy.clearAddMemberError(props.route.params.policyID, item.accountID); + Policy.clearAddMemberError(route.params.policyID, item.accountID); } }, - [props.route.params.policyID], + [route.params.policyID], ); /** * Check if the policy member is deleted from the workspace * - * @param {Object} policyMember - * @returns {Boolean} + * @param policyMember + * @returns */ - const isDeletedPolicyMember = (policyMember) => !props.network.isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && _.isEmpty(policyMember.errors); - const policyOwner = lodashGet(props.policy, 'owner'); - const currentUserLogin = lodashGet(props.currentUserPersonalDetails, 'login'); - const policyID = lodashGet(props.route, 'params.policyID'); - const policyName = lodashGet(props.policy, 'name'); - const invitedPrimaryToSecondaryLogins = _.invert(props.policy.primaryLoginsInvited); + const isDeletedPolicyMember = (policyMember) => !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && _.isEmpty(policyMember.errors); + const policyOwner = lodashGet(policy, 'owner'); + const currentUserLogin = lodashGet(currentUserPersonalDetails, 'login'); + const policyID = lodashGet(route, 'params.policyID'); + const policyName = lodashGet(policy, 'name'); + const invitedPrimaryToSecondaryLogins = _.invert(policy?.primaryLoginsInvited); const getMemberOptions = () => { - let result = []; + let result: MemberOption[] = []; - _.each(props.policyMembers, (policyMember, accountIDKey) => { + console.log('*** POLICY MEMBERS ***', policyMembers); + + Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => { const accountID = Number(accountIDKey); if (isDeletedPolicyMember(policyMember)) { return; } - const details = props.personalDetails[accountID]; + const details = personalDetails?.[accountID]; if (!details) { Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`); @@ -347,34 +378,34 @@ function WorkspaceMembersPage(props) { // If this policy is owned by Expensify then show all support (expensify.com or team.expensify.com) emails // We don't want to show guides as policy members unless the user is a guide. Some customers get confused when they // see random people added to their policy, but guides having access to the policies help set them up. - if (PolicyUtils.isExpensifyTeam(details.login || details.displayName)) { + if (PolicyUtils.isExpensifyTeam(details?.login ?? details?.displayName ?? '')) { if (policyOwner && currentUserLogin && !PolicyUtils.isExpensifyTeam(policyOwner) && !PolicyUtils.isExpensifyTeam(currentUserLogin)) { return; } } - const isAdmin = props.session.email === details.login || policyMember.role === CONST.POLICY.ROLE.ADMIN; + const isAdmin = session?.email === details.login || policyMember.role === CONST.POLICY.ROLE.ADMIN; result.push({ keyForList: accountIDKey, accountID, - isSelected: _.contains(selectedEmployees, accountID), + isSelected: selectedEmployees.includes(accountID), isDisabled: - accountID === props.session.accountID || - details.login === props.policy.owner || + accountID === session?.accountID || + details.login === policy?.owner || policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || - !_.isEmpty(policyMember.errors), - text: props.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), - alternateText: props.formatPhoneNumber(details.login), + Object.keys(policyMember.errors ?? {}).length > 0, + text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), + alternateText: formatPhoneNumber(details?.login ?? ''), rightElement: isAdmin ? ( - {props.translate('common.admin')} + {translate('common.admin')} ) : null, icons: [ { source: UserUtils.getAvatar(details.avatar, accountID), - name: props.formatPhoneNumber(details.login), + name: formatPhoneNumber(details?.login ?? ''), type: CONST.ICON_TYPE_AVATAR, id: accountID, }, @@ -383,11 +414,11 @@ function WorkspaceMembersPage(props) { pendingAction: policyMember.pendingAction, // Note which secondary login was used to invite this primary login - invitedSecondaryLogin: invitedPrimaryToSecondaryLogins[details.login] || '', + invitedSecondaryLogin: details?.login ? invitedPrimaryToSecondaryLogins[details.login] ?? '' : '', }); }); - result = _.sortBy(result, (value) => value.text.toLowerCase()); + result = result.sort((a, b) => a.text.localeCompare(b.text.toLowerCase())); return result; }; @@ -395,9 +426,9 @@ function WorkspaceMembersPage(props) { const getHeaderMessage = () => { if (isOfflineAndNoMemberDataAvailable) { - return props.translate('workspace.common.mustBeOnlineToViewMembers'); + return translate('workspace.common.mustBeOnlineToViewMembers'); } - return searchValue.trim() && !data.length ? props.translate('workspace.common.memberNotFound') : ''; + return searchValue.trim() && !data.length ? translate('workspace.common.memberNotFound') : ''; }; const getHeaderContent = () => { @@ -407,13 +438,12 @@ function WorkspaceMembersPage(props) { return ( Policy.dismissAddedWithPrimaryLoginMessages(policyID)} /> ); }; - return ( Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} > { setSearchValue(''); @@ -437,28 +467,28 @@ function WorkspaceMembersPage(props) { /> setRemoveMembersConfirmModalVisible(false)} - prompt={props.translate('workspace.people.removeMembersPrompt')} - confirmText={props.translate('common.remove')} - cancelText={props.translate('common.cancel')} - onModalHide={() => + prompt={translate('workspace.people.removeMembersPrompt')} + confirmText={translate('common.remove')} + cancelText={translate('common.cancel')} + onModalHide={() => { InteractionManager.runAfterInteractions(() => { if (!textInputRef.current) { return; } textInputRef.current.focus(); - }) - } + }); + }} />