From 55845ca42f7965fe2524ab27358b3f6f8d6bfb3e Mon Sep 17 00:00:00 2001 From: Edu Date: Wed, 23 Aug 2023 07:12:33 -0300 Subject: [PATCH 001/503] checking the last message changes --- src/pages/home/report/ReportActionsList.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 7f897ee825fb..84be0cf8a8e8 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -112,8 +112,7 @@ function ReportActionsList({ const currentUnreadMarker = useRef(null); const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); - const reportActionSize = useRef(sortedReportActions.length); - + const lastMessage = sortedReportActions.length > 0 ? sortedReportActions[0] : null; // Considering that renderItem is enclosed within a useCallback, marking it as "read" twice will retain the value as "true," preventing the useCallback from re-executing. // However, if we create and listen to an object, it will lead to a new useCallback execution. const [messageManuallyMarked, setMessageManuallyMarked] = useState({read: false}); @@ -143,8 +142,9 @@ function ReportActionsList({ if (!userActiveSince.current || report.reportID !== prevReportID) { return; } + const isUnreadMessage = ReportUtils.isUnread(report); - if (ReportUtils.isUnread(report)) { + if (isUnreadMessage) { if (scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD) { Report.readNewestAction(report.reportID); } else { @@ -152,14 +152,18 @@ function ReportActionsList({ } } - if (currentUnreadMarker.current || reportActionSize.current === sortedReportActions.length) { + // Deleted message is marked as 'deleted', if the last message is deleted, we have to remove the unread marker + if (!(isUnreadMessage && lastMessage && lastMessage.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !isOffline)) { + return; + } + + if (currentUnreadMarker.current) { return; } - reportActionSize.current = sortedReportActions.length; currentUnreadMarker.current = null; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sortedReportActions.length, report.reportID]); + }, [lastMessage, report.reportID, isOffline]); useEffect(() => { const didManuallyMarkReportAsUnread = report.lastReadTime < DateUtils.getDBTime() && ReportUtils.isUnread(report); From 5263c234be7fd78e9022566296182b3832fbe111 Mon Sep 17 00:00:00 2001 From: Edu Date: Wed, 23 Aug 2023 16:22:43 -0300 Subject: [PATCH 002/503] moving currentMarker out --- src/pages/home/report/ReportActionsList.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 84be0cf8a8e8..04f317952304 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -74,6 +74,8 @@ const MSG_VISIBLE_THRESHOLD = 250; // the useRef value gets reset when the reportID changes, so we use a global variable to keep track let prevReportID = null; +const currentUnreadMarker = {current: null}; + /** * Create a unique key for each action in the FlatList. * We use the reportActionID that is a string representation of a random 64-bit int, which should be @@ -109,7 +111,6 @@ function ReportActionsList({ const {isOffline} = useNetwork(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); - const currentUnreadMarker = useRef(null); const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); const lastMessage = sortedReportActions.length > 0 ? sortedReportActions[0] : null; @@ -135,6 +136,10 @@ function ReportActionsList({ } else { userActiveSince.current = DateUtils.getDBTime(); } + + if (prevReportID !== report.reportID) { + currentUnreadMarker.current = null; + } prevReportID = report.reportID; }, [report.reportID]); @@ -143,7 +148,6 @@ function ReportActionsList({ return; } const isUnreadMessage = ReportUtils.isUnread(report); - if (isUnreadMessage) { if (scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD) { Report.readNewestAction(report.reportID); @@ -157,28 +161,27 @@ function ReportActionsList({ return; } - if (currentUnreadMarker.current) { - return; - } - currentUnreadMarker.current = null; // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastMessage, report.reportID, isOffline]); useEffect(() => { + if (!userActiveSince.current || report.reportID !== prevReportID) { + return; + } const didManuallyMarkReportAsUnread = report.lastReadTime < DateUtils.getDBTime() && ReportUtils.isUnread(report); + if (!didManuallyMarkReportAsUnread) { setMessageManuallyMarked({read: false}); return; } - // Clearing the current unread marker so that it can be recalculated currentUnreadMarker.current = null; setMessageManuallyMarked({read: true}); // We only care when a new lastReadTime is set in the report // eslint-disable-next-line react-hooks/exhaustive-deps - }, [report.lastReadTime]); + }, [report.lastReadTime, report.reportID]); /** * Show/hide the new floating message counter when user is scrolling back/forth in the history of messages. From a64a152892698c676084f86e5c70958b9575c1e5 Mon Sep 17 00:00:00 2001 From: Edu Date: Fri, 25 Aug 2023 09:55:04 -0300 Subject: [PATCH 003/503] updated logic to handle the shouldDisplayMarker --- src/pages/home/report/ReportActionsList.js | 37 +++++++++++----------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 04f317952304..b59403cf21fb 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -70,12 +70,6 @@ const defaultProps = { const VERTICAL_OFFSET_THRESHOLD = 200; const MSG_VISIBLE_THRESHOLD = 250; -// Seems that there is an architecture issue that prevents us from using the reportID with useRef -// the useRef value gets reset when the reportID changes, so we use a global variable to keep track -let prevReportID = null; - -const currentUnreadMarker = {current: null}; - /** * Create a unique key for each action in the FlatList. * We use the reportActionID that is a string representation of a random 64-bit int, which should be @@ -113,6 +107,8 @@ function ReportActionsList({ const userActiveSince = useRef(null); const scrollingVerticalOffset = useRef(0); const readActionSkipped = useRef(false); + const prevReportID = useRef(null); + const currentUnreadMarker = useRef(null); const lastMessage = sortedReportActions.length > 0 ? sortedReportActions[0] : null; // Considering that renderItem is enclosed within a useCallback, marking it as "read" twice will retain the value as "true," preventing the useCallback from re-executing. // However, if we create and listen to an object, it will lead to a new useCallback execution. @@ -131,20 +127,20 @@ function ReportActionsList({ // If the reportID changes, we reset the userActiveSince to null, we need to do it because // the parent component is sending the previous reportID even when the user isn't active // on the report - if (userActiveSince.current && prevReportID && prevReportID !== report.reportID) { + if (userActiveSince.current && prevReportID.current && prevReportID.current !== report.reportID) { userActiveSince.current = null; } else { userActiveSince.current = DateUtils.getDBTime(); } - if (prevReportID !== report.reportID) { - currentUnreadMarker.current = null; - } - prevReportID = report.reportID; + // if (prevReportID.current !== report.reportID) { + // currentUnreadMarker.current = null; + // } + prevReportID.current = report.reportID; }, [report.reportID]); useEffect(() => { - if (!userActiveSince.current || report.reportID !== prevReportID) { + if (!userActiveSince.current || report.reportID !== prevReportID.current) { return; } const isUnreadMessage = ReportUtils.isUnread(report); @@ -166,15 +162,15 @@ function ReportActionsList({ }, [lastMessage, report.reportID, isOffline]); useEffect(() => { - if (!userActiveSince.current || report.reportID !== prevReportID) { + if (!userActiveSince.current || report.reportID !== prevReportID.current) { return; } const didManuallyMarkReportAsUnread = report.lastReadTime < DateUtils.getDBTime() && ReportUtils.isUnread(report); - if (!didManuallyMarkReportAsUnread) { setMessageManuallyMarked({read: false}); return; } + // Clearing the current unread marker so that it can be recalculated currentUnreadMarker.current = null; setMessageManuallyMarked({read: true}); @@ -235,15 +231,18 @@ function ReportActionsList({ if (!currentUnreadMarker.current) { const nextMessage = sortedReportActions[index + 1]; const isCurrentMessageUnread = isMessageUnread(reportAction, report.lastReadTime); - shouldDisplayNewMarker = isCurrentMessageUnread && !isMessageUnread(nextMessage, report.lastReadTime); + let canDisplayNewMarker = isCurrentMessageUnread && !isMessageUnread(nextMessage, report.lastReadTime); if (!messageManuallyMarked.read) { - shouldDisplayNewMarker = shouldDisplayNewMarker && reportAction.actorAccountID !== Report.getCurrentUserAccountID(); + canDisplayNewMarker = shouldDisplayNewMarker && reportAction.actorAccountID !== Report.getCurrentUserAccountID(); } - const canDisplayMarker = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true; - - if (!currentUnreadMarker.current && shouldDisplayNewMarker && canDisplayMarker) { + let isMessageInScope = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true; + if (messageManuallyMarked.read) { + isMessageInScope = true; + } + if (!currentUnreadMarker.current && canDisplayNewMarker && isMessageInScope) { currentUnreadMarker.current = reportAction.reportActionID; + shouldDisplayNewMarker = true; } } else { shouldDisplayNewMarker = reportAction.reportActionID === currentUnreadMarker.current; From 8b49c6f15873ea47b473ceb3fd64d1624edd018f Mon Sep 17 00:00:00 2001 From: Edu Date: Fri, 25 Aug 2023 10:24:37 -0300 Subject: [PATCH 004/503] updated logic --- src/pages/home/report/ReportActionsList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index b59403cf21fb..be91ad809d73 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -234,7 +234,7 @@ function ReportActionsList({ let canDisplayNewMarker = isCurrentMessageUnread && !isMessageUnread(nextMessage, report.lastReadTime); if (!messageManuallyMarked.read) { - canDisplayNewMarker = shouldDisplayNewMarker && reportAction.actorAccountID !== Report.getCurrentUserAccountID(); + canDisplayNewMarker = canDisplayNewMarker && reportAction.actorAccountID !== Report.getCurrentUserAccountID(); } let isMessageInScope = scrollingVerticalOffset.current < MSG_VISIBLE_THRESHOLD ? reportAction.created < userActiveSince.current : true; if (messageManuallyMarked.read) { From 2fb7f33e11b278954bb32d6d44dafa84bfa9552c Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 13 Sep 2023 11:03:16 +0200 Subject: [PATCH 005/503] clean up --- src/pages/home/report/ReportActionsList.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index be91ad809d73..99464b9041e3 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -133,9 +133,6 @@ function ReportActionsList({ userActiveSince.current = DateUtils.getDBTime(); } - // if (prevReportID.current !== report.reportID) { - // currentUnreadMarker.current = null; - // } prevReportID.current = report.reportID; }, [report.reportID]); From 6b6afd8326c14044681838a44c5e4b929ef77e26 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 15 Nov 2023 15:06:15 +0100 Subject: [PATCH 006/503] [TS migration] Migrate 'Onfido' component --- .../{BaseOnfidoWeb.js => BaseOnfidoWeb.tsx} | 50 ++++++++++--------- src/components/Onfido/index.desktop.js | 11 ---- .../{index.native.js => index.native.tsx} | 16 +++--- .../Onfido/{index.website.js => index.tsx} | 11 ++-- src/components/Onfido/onfidoPropTypes.js | 15 ------ src/components/Onfido/types.ts | 20 ++++++++ 6 files changed, 58 insertions(+), 65 deletions(-) rename src/components/Onfido/{BaseOnfidoWeb.js => BaseOnfidoWeb.tsx} (81%) delete mode 100644 src/components/Onfido/index.desktop.js rename src/components/Onfido/{index.native.js => index.native.tsx} (79%) rename src/components/Onfido/{index.website.js => index.tsx} (64%) delete mode 100644 src/components/Onfido/onfidoPropTypes.js create mode 100644 src/components/Onfido/types.ts diff --git a/src/components/Onfido/BaseOnfidoWeb.js b/src/components/Onfido/BaseOnfidoWeb.tsx similarity index 81% rename from src/components/Onfido/BaseOnfidoWeb.js rename to src/components/Onfido/BaseOnfidoWeb.tsx index 5c0f83902e55..79842823a975 100644 --- a/src/components/Onfido/BaseOnfidoWeb.js +++ b/src/components/Onfido/BaseOnfidoWeb.tsx @@ -1,7 +1,6 @@ -import lodashGet from 'lodash/get'; import * as OnfidoSDK from 'onfido-sdk-ui'; -import React, {forwardRef, useEffect} from 'react'; -import _ from 'underscore'; +import React, {ForwardedRef, forwardRef, useEffect} from 'react'; +import type {LocaleContextProps} from '@components/LocaleContextProvider'; import useLocalize from '@hooks/useLocalize'; import Log from '@libs/Log'; import fontFamily from '@styles/fontFamily'; @@ -10,9 +9,15 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import './index.css'; -import onfidoPropTypes from './onfidoPropTypes'; +import type {OnfidoElement, OnfidoProps} from './types'; -function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLocale, translate}) { +type LocaleProps = Pick; + +type OnfidoEvent = Event & { + detail?: Record; +}; + +function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLocale, translate}: OnfidoProps & LocaleProps) { OnfidoSDK.init({ token: sdkToken, containerId: CONST.ONFIDO.CONTAINER_ID, @@ -22,7 +27,7 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo fontFamilySubtitle: `${fontFamily.EXP_NEUE}, -apple-system, serif`, fontFamilyBody: `${fontFamily.EXP_NEUE}, -apple-system, serif`, fontSizeTitle: `${variables.fontSizeLarge}px`, - fontWeightTitle: fontWeightBold, + fontWeightTitle: Number(fontWeightBold), fontWeightSubtitle: 400, fontSizeSubtitle: `${variables.fontSizeNormal}px`, colorContentTitle: themeColors.text, @@ -47,7 +52,6 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo colorBorderLinkUnderline: themeColors.link, colorBackgroundLinkHover: themeColors.link, colorBackgroundLinkActive: themeColors.link, - authAccentColor: themeColors.link, colorBackgroundInfoPill: themeColors.link, colorBackgroundSelector: themeColors.appBG, colorBackgroundDocTypeButton: themeColors.success, @@ -59,11 +63,10 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo { type: CONST.ONFIDO.TYPE.DOCUMENT, options: { - useLiveDocumentCapture: true, forceCrossDevice: true, hideCountrySelection: true, - country: 'USA', documentTypes: { + // eslint-disable-next-line @typescript-eslint/naming-convention driving_licence: { country: 'USA', }, @@ -78,17 +81,15 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo }, }, ], - smsNumberCountryCode: CONST.ONFIDO.SMS_NUMBER_COUNTRY_CODE.US, - showCountrySelection: false, onComplete: (data) => { - if (_.isEmpty(data)) { + if (!Object.keys(data).length) { Log.warn('Onfido completed with no data'); } onSuccess(data); }, onError: (error) => { - const errorMessage = lodashGet(error, 'message', CONST.ERROR.UNKNOWN_ERROR); - const errorType = lodashGet(error, 'type'); + const errorMessage = error.message ?? CONST.ERROR.UNKNOWN_ERROR; + const errorType = error.type; Log.hmmm('Onfido error', {errorType, errorMessage}); onError(errorMessage); }, @@ -101,32 +102,33 @@ function initializeOnfido({sdkToken, onSuccess, onError, onUserExit, preferredLo }, language: { // We need to use ES_ES as locale key because the key `ES` is not a valid config key for Onfido - locale: preferredLocale === CONST.LOCALES.ES ? CONST.LOCALES.ES_ES_ONFIDO : preferredLocale, + locale: preferredLocale === CONST.LOCALES.ES ? CONST.LOCALES.ES_ES_ONFIDO : (preferredLocale as OnfidoSDK.SupportedLanguages), // Provide a custom phrase for the back button so that the first letter is capitalized, // and translate the phrase while we're at it. See the issue and documentation for more context. // https://github.com/Expensify/App/issues/17244 // https://documentation.onfido.com/sdk/web/#custom-languages phrases: { + // eslint-disable-next-line @typescript-eslint/naming-convention 'generic.back': translate('common.back'), }, }, }); } -function logOnFidoEvent(event) { +function logOnFidoEvent(event: OnfidoEvent) { Log.hmmm('Receiving Onfido analytic event', event.detail); } -const Onfido = forwardRef((props, ref) => { +function Onfido({sdkToken, onSuccess, onError, onUserExit}: OnfidoProps, ref: ForwardedRef) { const {preferredLocale, translate} = useLocalize(); useEffect(() => { initializeOnfido({ - sdkToken: props.sdkToken, - onSuccess: props.onSuccess, - onError: props.onError, - onUserExit: props.onUserExit, + sdkToken, + onSuccess, + onError, + onUserExit, preferredLocale, translate, }); @@ -143,8 +145,8 @@ const Onfido = forwardRef((props, ref) => { ref={ref} /> ); -}); +} Onfido.displayName = 'Onfido'; -Onfido.propTypes = onfidoPropTypes; -export default Onfido; + +export default forwardRef(Onfido); diff --git a/src/components/Onfido/index.desktop.js b/src/components/Onfido/index.desktop.js deleted file mode 100644 index e455eaf78d32..000000000000 --- a/src/components/Onfido/index.desktop.js +++ /dev/null @@ -1,11 +0,0 @@ -import BaseOnfidoWeb from './BaseOnfidoWeb'; -import onfidoPropTypes from './onfidoPropTypes'; - -// On desktop, we do not want to teardown onfido, because it causes a crash. -// See https://github.com/Expensify/App/issues/6082 -const Onfido = BaseOnfidoWeb; - -Onfido.propTypes = onfidoPropTypes; -Onfido.displayName = 'Onfido'; - -export default Onfido; diff --git a/src/components/Onfido/index.native.js b/src/components/Onfido/index.native.tsx similarity index 79% rename from src/components/Onfido/index.native.js rename to src/components/Onfido/index.native.tsx index ed0578187d3c..e09eeec4f322 100644 --- a/src/components/Onfido/index.native.js +++ b/src/components/Onfido/index.native.tsx @@ -1,15 +1,13 @@ import {OnfidoCaptureType, OnfidoCountryCode, OnfidoDocumentType, Onfido as OnfidoSDK} from '@onfido/react-native-sdk'; -import lodashGet from 'lodash/get'; import React, {useEffect} from 'react'; import {Alert, Linking} from 'react-native'; -import _ from 'underscore'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useLocalize from '@hooks/useLocalize'; import Log from '@libs/Log'; import CONST from '@src/CONST'; -import onfidoPropTypes from './onfidoPropTypes'; +import type {OnfidoProps} from './types'; -function Onfido({sdkToken, onUserExit, onSuccess, onError}) { +function Onfido({sdkToken, onUserExit, onSuccess, onError}: OnfidoProps) { const {translate} = useLocalize(); useEffect(() => { @@ -28,19 +26,20 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}) { }) .then(onSuccess) .catch((error) => { - const errorMessage = lodashGet(error, 'message', CONST.ERROR.UNKNOWN_ERROR); - const errorType = lodashGet(error, 'type'); + const errorMessage = error.message ?? CONST.ERROR.UNKNOWN_ERROR; + const errorType = error.type; + Log.hmmm('Onfido error on native', {errorType, errorMessage}); // If the user cancels the Onfido flow we won't log this error as it's normal. In the React Native SDK the user exiting the flow will trigger this error which we can use as // our "user exited the flow" callback. On web, this event has it's own callback passed as a config so we don't need to bother with this there. - if (_.contains([CONST.ONFIDO.ERROR.USER_CANCELLED, CONST.ONFIDO.ERROR.USER_TAPPED_BACK, CONST.ONFIDO.ERROR.USER_EXITED], errorMessage)) { + if ([CONST.ONFIDO.ERROR.USER_CANCELLED, CONST.ONFIDO.ERROR.USER_TAPPED_BACK, CONST.ONFIDO.ERROR.USER_EXITED].includes(errorMessage)) { onUserExit(); return; } // Handle user camera permission on iOS and Android - if (_.contains([CONST.ONFIDO.ERROR.USER_CAMERA_PERMISSION, CONST.ONFIDO.ERROR.USER_CAMERA_DENINED, CONST.ONFIDO.ERROR.USER_CAMERA_CONSENT_DENIED], errorMessage)) { + if ([CONST.ONFIDO.ERROR.USER_CAMERA_PERMISSION, CONST.ONFIDO.ERROR.USER_CAMERA_DENINED, CONST.ONFIDO.ERROR.USER_CAMERA_CONSENT_DENIED].includes(errorMessage)) { Alert.alert( translate('onfidoStep.cameraPermissionsNotGranted'), translate('onfidoStep.cameraRequestMessage'), @@ -71,7 +70,6 @@ function Onfido({sdkToken, onUserExit, onSuccess, onError}) { return ; } -Onfido.propTypes = onfidoPropTypes; Onfido.displayName = 'Onfido'; export default Onfido; diff --git a/src/components/Onfido/index.website.js b/src/components/Onfido/index.tsx similarity index 64% rename from src/components/Onfido/index.website.js rename to src/components/Onfido/index.tsx index 12ad1edd8fb9..139dc3cec405 100644 --- a/src/components/Onfido/index.website.js +++ b/src/components/Onfido/index.tsx @@ -1,14 +1,14 @@ -import lodashGet from 'lodash/get'; import React, {useEffect, useRef} from 'react'; import BaseOnfidoWeb from './BaseOnfidoWeb'; -import onfidoPropTypes from './onfidoPropTypes'; +import type {OnfidoElement, OnfidoProps} from './types'; -function Onfido({sdkToken, onSuccess, onError, onUserExit}) { - const baseOnfidoRef = useRef(null); +function Onfido({sdkToken, onSuccess, onError, onUserExit}: OnfidoProps) { + const baseOnfidoRef = useRef(null); useEffect( () => () => { - const onfidoOut = lodashGet(baseOnfidoRef.current, 'onfidoOut'); + const onfidoOut = baseOnfidoRef.current?.onfidoOut; + if (!onfidoOut) { return; } @@ -29,7 +29,6 @@ function Onfido({sdkToken, onSuccess, onError, onUserExit}) { ); } -Onfido.propTypes = onfidoPropTypes; Onfido.displayName = 'Onfido'; export default Onfido; diff --git a/src/components/Onfido/onfidoPropTypes.js b/src/components/Onfido/onfidoPropTypes.js deleted file mode 100644 index ff0023c70058..000000000000 --- a/src/components/Onfido/onfidoPropTypes.js +++ /dev/null @@ -1,15 +0,0 @@ -import PropTypes from 'prop-types'; - -export default { - /** Token used to initialize the Onfido SDK */ - sdkToken: PropTypes.string.isRequired, - - /** Called when the user intentionally exits the flow without completing it */ - onUserExit: PropTypes.func.isRequired, - - /** Called when the user is totally done with Onfido */ - onSuccess: PropTypes.func.isRequired, - - /** Called when Onfido throws an error */ - onError: PropTypes.func.isRequired, -}; diff --git a/src/components/Onfido/types.ts b/src/components/Onfido/types.ts new file mode 100644 index 000000000000..a4fe3d93f05e --- /dev/null +++ b/src/components/Onfido/types.ts @@ -0,0 +1,20 @@ +import {OnfidoError, OnfidoResult} from '@onfido/react-native-sdk'; +import * as OnfidoSDK from 'onfido-sdk-ui'; + +type OnfidoElement = HTMLDivElement & {onfidoOut?: OnfidoSDK.SdkHandle}; + +type OnfidoProps = { + /** Token used to initialize the Onfido SDK */ + sdkToken: string; + + /** Called when the user intentionally exits the flow without completing it */ + onUserExit: (userExitCode?: OnfidoSDK.UserExitCode) => void; + + /** Called when the user is totally done with Onfido */ + onSuccess: (data: OnfidoSDK.SdkResponse | OnfidoResult | OnfidoError) => void; + + /** Called when Onfido throws an error */ + onError: (error?: string) => void; +}; + +export type {OnfidoProps, OnfidoElement}; From f5575a773678586ea7cc49766ffe606b76f7fb4d Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Tue, 12 Dec 2023 19:41:26 +0100 Subject: [PATCH 007/503] migrate GrowlNotification and related components to TypeScript --- .../growlNotificationContainerPropTypes.js | 12 ------- .../GrowlNotificationContainer/index.js | 31 ------------------- .../index.native.js | 27 ---------------- .../index.native.tsx | 18 +++++++++++ .../GrowlNotificationContainer/index.tsx | 22 +++++++++++++ .../GrowlNotificationContainer/types.ts | 9 ++++++ .../GrowlNotification/{index.js => index.tsx} | 16 +++++----- src/components/GrowlNotification/types.ts | 9 ++++++ src/libs/Growl.ts | 7 ++--- 9 files changed, 69 insertions(+), 82 deletions(-) delete mode 100644 src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js delete mode 100644 src/components/GrowlNotification/GrowlNotificationContainer/index.js delete mode 100644 src/components/GrowlNotification/GrowlNotificationContainer/index.native.js create mode 100644 src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx create mode 100644 src/components/GrowlNotification/GrowlNotificationContainer/index.tsx create mode 100644 src/components/GrowlNotification/GrowlNotificationContainer/types.ts rename src/components/GrowlNotification/{index.js => index.tsx} (88%) create mode 100644 src/components/GrowlNotification/types.ts diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js b/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js deleted file mode 100644 index 2432d1b1748c..000000000000 --- a/src/components/GrowlNotification/GrowlNotificationContainer/growlNotificationContainerPropTypes.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; -import {Animated} from 'react-native'; - -const propTypes = { - /** GrowlNotification content */ - children: PropTypes.node.isRequired, - - /** GrowlNotification Y postion, required to show or hide with fling animation */ - translateY: PropTypes.instanceOf(Animated.Value).isRequired, -}; - -export default propTypes; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.js deleted file mode 100644 index 82672edb14c2..000000000000 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useThemeStyles from '@styles/useThemeStyles'; -import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes'; - -const propTypes = { - ...growlNotificationContainerPropTypes, - ...windowDimensionsPropTypes, -}; - -function GrowlNotificationContainer(props) { - const styles = useThemeStyles(); - return ( - - {props.children} - - ); -} - -GrowlNotificationContainer.propTypes = propTypes; -GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; - -export default withWindowDimensions(GrowlNotificationContainer); diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js deleted file mode 100644 index 457a9dce66d9..000000000000 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import {Animated} from 'react-native'; -import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; -import useStyleUtils from '@styles/useStyleUtils'; -import useThemeStyles from '@styles/useThemeStyles'; -import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes'; - -const propTypes = { - ...growlNotificationContainerPropTypes, -}; - -function GrowlNotificationContainer(props) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const insets = useSafeAreaInsets; - - return ( - - {props.children} - - ); -} - -GrowlNotificationContainer.propTypes = propTypes; -GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; - -export default GrowlNotificationContainer; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx new file mode 100644 index 000000000000..0fd7167ba492 --- /dev/null +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import {Animated} from 'react-native'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; +import useStyleUtils from '@styles/useStyleUtils'; +import useThemeStyles from '@styles/useThemeStyles'; +import GrowlNotificationContainerProps from './types'; + +function GrowlNotificationContainer({children, translateY}: GrowlNotificationContainerProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const insets = useSafeAreaInsets(); + + return {children}; +} + +GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; + +export default GrowlNotificationContainer; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx new file mode 100644 index 000000000000..9f65e269ee96 --- /dev/null +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import {Animated} from 'react-native'; +import withWindowDimensions from '@components/withWindowDimensions'; +import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; +import useThemeStyles from '@styles/useThemeStyles'; +import GrowlNotificationContainerProps from './types'; + +function GrowlNotificationContainer({children, translateY, isSmallScreenWidth}: GrowlNotificationContainerProps & WindowDimensionsProps) { + const styles = useThemeStyles(); + + return ( + + {children} + + ); +} + +GrowlNotificationContainer.displayName = 'GrowlNotificationContainer'; + +export default withWindowDimensions(GrowlNotificationContainer); diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/types.ts b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts new file mode 100644 index 000000000000..9a8ea4c76958 --- /dev/null +++ b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts @@ -0,0 +1,9 @@ +import {Animated} from 'react-native'; + +type GrowlNotificationContainerProps = { + children: React.ReactNode; + + translateY: Animated.Value; +}; + +export default GrowlNotificationContainerProps; diff --git a/src/components/GrowlNotification/index.js b/src/components/GrowlNotification/index.tsx similarity index 88% rename from src/components/GrowlNotification/index.js rename to src/components/GrowlNotification/index.tsx index faf1ec9cfa16..84a56594943e 100644 --- a/src/components/GrowlNotification/index.js +++ b/src/components/GrowlNotification/index.tsx @@ -1,6 +1,7 @@ import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {Animated, View} from 'react-native'; import {Directions, FlingGestureHandler, State} from 'react-native-gesture-handler'; +import {SvgProps} from 'react-native-svg'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Pressables from '@components/Pressable'; @@ -11,20 +12,21 @@ import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import GrowlNotificationContainer from './GrowlNotificationContainer'; +import {GrowlNotificationProps} from './types'; const INACTIVE_POSITION_Y = -255; const PressableWithoutFeedback = Pressables.PressableWithoutFeedback; -function GrowlNotification(_, ref) { +function GrowlNotification({ref}: GrowlNotificationProps) { const translateY = useRef(new Animated.Value(INACTIVE_POSITION_Y)).current; const [bodyText, setBodyText] = useState(''); const [type, setType] = useState('success'); - const [duration, setDuration] = useState(); + const [duration, setDuration] = useState(); const theme = useTheme(); const styles = useThemeStyles(); - const types = { + const types: Record; iconColor: string}> = { [CONST.GROWL.SUCCESS]: { icon: Expensicons.Checkmark, iconColor: theme.success, @@ -46,7 +48,7 @@ function GrowlNotification(_, ref) { * @param {String} type * @param {Number} duration */ - const show = useCallback((text, growlType, growlDuration) => { + const show = useCallback((text: string, growlType: string, growlDuration: number) => { setBodyText(text); setType(growlType); setDuration(growlDuration); @@ -58,13 +60,11 @@ function GrowlNotification(_, ref) { * @param {Number} val */ const fling = useCallback( - (val = INACTIVE_POSITION_Y) => { + (val = INACTIVE_POSITION_Y) => Animated.spring(translateY, { toValue: val, - duration: 80, useNativeDriver, - }).start(); - }, + }).start(), [translateY], ); diff --git a/src/components/GrowlNotification/types.ts b/src/components/GrowlNotification/types.ts new file mode 100644 index 000000000000..edee6ce6b37a --- /dev/null +++ b/src/components/GrowlNotification/types.ts @@ -0,0 +1,9 @@ +type GrowlRef = { + show?: (bodyText: string, type: string, duration: number) => void; +}; + +type GrowlNotificationProps = { + ref: React.RefObject; +}; + +export type {GrowlRef, GrowlNotificationProps}; diff --git a/src/libs/Growl.ts b/src/libs/Growl.ts index 55bcf88206e9..b55ae5512c69 100644 --- a/src/libs/Growl.ts +++ b/src/libs/Growl.ts @@ -1,10 +1,7 @@ import React from 'react'; +import {GrowlRef} from '@components/GrowlNotification/types'; import CONST from '@src/CONST'; -type GrowlRef = { - show?: (bodyText: string, type: string, duration: number) => void; -}; - const growlRef = React.createRef(); let resolveIsReadyPromise: undefined | ((value?: unknown) => void); const isReadyPromise = new Promise((resolve) => { @@ -50,4 +47,6 @@ export default { success, }; +export type {GrowlRef}; + export {growlRef, setIsReady}; From 236b050889c9a3535bd65ff603d8071c05f88687 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Tue, 12 Dec 2023 19:44:40 +0100 Subject: [PATCH 008/503] remove omitted type export --- src/libs/Growl.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libs/Growl.ts b/src/libs/Growl.ts index b55ae5512c69..59cadc7073f3 100644 --- a/src/libs/Growl.ts +++ b/src/libs/Growl.ts @@ -47,6 +47,4 @@ export default { success, }; -export type {GrowlRef}; - export {growlRef, setIsReady}; From f6828ab5c83a59082c9fc658540265c8dba54244 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 14 Dec 2023 00:58:41 +0100 Subject: [PATCH 009/503] add review nitpicks and changes --- .../GrowlNotificationContainer/index.tsx | 8 ++++---- src/components/GrowlNotification/index.tsx | 3 ++- src/components/GrowlNotification/types.ts | 7 ++++++- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx index 9f65e269ee96..07530c117f60 100644 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.tsx @@ -1,12 +1,12 @@ import React from 'react'; import {Animated} from 'react-native'; -import withWindowDimensions from '@components/withWindowDimensions'; -import {WindowDimensionsProps} from '@components/withWindowDimensions/types'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import useThemeStyles from '@styles/useThemeStyles'; import GrowlNotificationContainerProps from './types'; -function GrowlNotificationContainer({children, translateY, isSmallScreenWidth}: GrowlNotificationContainerProps & WindowDimensionsProps) { +function GrowlNotificationContainer({children, translateY}: GrowlNotificationContainerProps) { const styles = useThemeStyles(); + const {isSmallScreenWidth} = useWindowDimensions(); return ( void; }; type GrowlNotificationProps = { - ref: React.RefObject; + // eslint-disable-next-line @typescript-eslint/naming-convention + _: unknown; + + ref: ForwardedRef; }; export type {GrowlRef, GrowlNotificationProps}; From b11c5c838c7db58e91f9038e9b02c7385bf056d8 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:03:27 +0100 Subject: [PATCH 010/503] change up ts errors --- src/components/GrowlNotification/index.tsx | 8 ++++---- src/components/GrowlNotification/types.ts | 14 -------------- src/libs/Growl.ts | 7 ++++++- 3 files changed, 10 insertions(+), 19 deletions(-) delete mode 100644 src/components/GrowlNotification/types.ts diff --git a/src/components/GrowlNotification/index.tsx b/src/components/GrowlNotification/index.tsx index 780a97924354..f52a6083f4e6 100644 --- a/src/components/GrowlNotification/index.tsx +++ b/src/components/GrowlNotification/index.tsx @@ -1,4 +1,4 @@ -import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import {Animated, View} from 'react-native'; import {Directions, FlingGestureHandler, State} from 'react-native-gesture-handler'; import {SvgProps} from 'react-native-svg'; @@ -9,17 +9,17 @@ import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Growl from '@libs/Growl'; +import type {GrowlRef} from '@libs/Growl'; import useNativeDriver from '@libs/useNativeDriver'; import CONST from '@src/CONST'; import GrowlNotificationContainer from './GrowlNotificationContainer'; -import {GrowlNotificationProps} from './types'; const INACTIVE_POSITION_Y = -255; const PressableWithoutFeedback = Pressables.PressableWithoutFeedback; -// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars -function GrowlNotification({_, ref}: GrowlNotificationProps) { +// eslint-disable-next-line @typescript-eslint/naming-convention +function GrowlNotification(_: unknown, ref: ForwardedRef) { const translateY = useRef(new Animated.Value(INACTIVE_POSITION_Y)).current; const [bodyText, setBodyText] = useState(''); const [type, setType] = useState('success'); diff --git a/src/components/GrowlNotification/types.ts b/src/components/GrowlNotification/types.ts deleted file mode 100644 index a984008978ed..000000000000 --- a/src/components/GrowlNotification/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {ForwardedRef} from 'react'; - -type GrowlRef = { - show?: (bodyText: string, type: string, duration: number) => void; -}; - -type GrowlNotificationProps = { - // eslint-disable-next-line @typescript-eslint/naming-convention - _: unknown; - - ref: ForwardedRef; -}; - -export type {GrowlRef, GrowlNotificationProps}; diff --git a/src/libs/Growl.ts b/src/libs/Growl.ts index 59cadc7073f3..3812a155ba1f 100644 --- a/src/libs/Growl.ts +++ b/src/libs/Growl.ts @@ -1,7 +1,10 @@ import React from 'react'; -import {GrowlRef} from '@components/GrowlNotification/types'; import CONST from '@src/CONST'; +type GrowlRef = { + show?: (bodyText: string, type: string, duration: number) => void; +}; + const growlRef = React.createRef(); let resolveIsReadyPromise: undefined | ((value?: unknown) => void); const isReadyPromise = new Promise((resolve) => { @@ -47,4 +50,6 @@ export default { success, }; +export type {GrowlRef}; + export {growlRef, setIsReady}; From ac6b737f32def157b2efdaff3ca6fac045c24f3a Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 14 Dec 2023 13:12:20 +0100 Subject: [PATCH 011/503] add ChildrenProps to types --- .../GrowlNotification/GrowlNotificationContainer/types.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/types.ts b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts index 9a8ea4c76958..69e5d11745c6 100644 --- a/src/components/GrowlNotification/GrowlNotificationContainer/types.ts +++ b/src/components/GrowlNotification/GrowlNotificationContainer/types.ts @@ -1,8 +1,7 @@ import {Animated} from 'react-native'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; -type GrowlNotificationContainerProps = { - children: React.ReactNode; - +type GrowlNotificationContainerProps = ChildrenProps & { translateY: Animated.Value; }; From 5d024d763bde660513aaad091ccce1d39fe06662 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Tue, 19 Dec 2023 22:28:51 +0100 Subject: [PATCH 012/503] separate icon types --- src/components/GrowlNotification/index.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/GrowlNotification/index.tsx b/src/components/GrowlNotification/index.tsx index f52a6083f4e6..0ab515bf3032 100644 --- a/src/components/GrowlNotification/index.tsx +++ b/src/components/GrowlNotification/index.tsx @@ -27,7 +27,18 @@ function GrowlNotification(_: unknown, ref: ForwardedRef) { const theme = useTheme(); const styles = useThemeStyles(); - const types: Record; iconColor: string}> = { + type GrowlIconTypes = Record< + string, + { + /** Expensicon for the page */ + icon: React.FC; + + /** Color for the icon (should be from theme) */ + iconColor: string; + } + >; + + const types: GrowlIconTypes = { [CONST.GROWL.SUCCESS]: { icon: Expensicons.Checkmark, iconColor: theme.success, From d5c50b659fb3201c8ddf78aac1df7ab76590b549 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Tue, 23 Jan 2024 11:30:01 -0800 Subject: [PATCH 013/503] allow accessing the members page in a thread --- src/pages/ReportDetailsPage.js | 3 ++- src/pages/RoomMembersPage.js | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index ff9ed62c6a65..7cb5b12f250e 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -106,6 +106,7 @@ function ReportDetailsPage(props) { } // The Members page is only shown when: + // - The report is a thread in a chat report // - The report is not a user created room with participants to show i.e. DM, Group Chat, etc // - The report is a user created room and the room and the current user is a workspace member i.e. non-workspace members should not see this option. if ((!isUserCreatedPolicyRoom && participants.length) || (isUserCreatedPolicyRoom && isPolicyMember)) { @@ -116,7 +117,7 @@ function ReportDetailsPage(props) { subtitle: participants.length, isAnonymousAction: false, action: () => { - if (isUserCreatedPolicyRoom && !props.report.parentReportID) { + if (props.report.parentReportID || isUserCreatedPolicyRoom) { Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(props.report.reportID)); } else { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID)); diff --git a/src/pages/RoomMembersPage.js b/src/pages/RoomMembersPage.js index 30ffd60aa4ac..486945b24e03 100644 --- a/src/pages/RoomMembersPage.js +++ b/src/pages/RoomMembersPage.js @@ -238,7 +238,7 @@ function RoomMembersPage(props) { testID={RoomMembersPage.displayName} > Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID))} > From 545e3687617d4cdad7ceb3d0543564611b448302 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Tue, 23 Jan 2024 13:25:51 -0800 Subject: [PATCH 014/503] restrict to chat reports --- src/pages/ReportDetailsPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 7cb5b12f250e..65456b17616b 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -117,7 +117,7 @@ function ReportDetailsPage(props) { subtitle: participants.length, isAnonymousAction: false, action: () => { - if (props.report.parentReportID || isUserCreatedPolicyRoom) { + if ((props.report.type === CONST.REPORT.TYPE.CHAT && props.report.parentReportID) || isUserCreatedPolicyRoom) { Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(props.report.reportID)); } else { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID)); From c644fd04aa8ac28707e64003ed3049a9e9f957e9 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Tue, 23 Jan 2024 13:31:40 -0800 Subject: [PATCH 015/503] update condition, add comment --- src/pages/RoomMembersPage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/RoomMembersPage.js b/src/pages/RoomMembersPage.js index 486945b24e03..8d273c7d2fee 100644 --- a/src/pages/RoomMembersPage.js +++ b/src/pages/RoomMembersPage.js @@ -238,7 +238,12 @@ function RoomMembersPage(props) { testID={RoomMembersPage.displayName} > Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID))} > From a7bfccbb7fe6c9bd5eca48fe88fddfd5d5eb80a7 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Tue, 23 Jan 2024 14:17:11 -0800 Subject: [PATCH 016/503] style --- src/pages/RoomMembersPage.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/RoomMembersPage.js b/src/pages/RoomMembersPage.js index 8d273c7d2fee..df2634a1199e 100644 --- a/src/pages/RoomMembersPage.js +++ b/src/pages/RoomMembersPage.js @@ -243,7 +243,11 @@ function RoomMembersPage(props) { // - this report is a user-created policy room and the user is not a member of the policy // - this report is a default room (threads in default rooms are fine) // - this report is a policy expense chat (threads in policy expense chats are fine) - shouldShow={_.isEmpty(props.report) || (_.isEmpty(ReportUtils.getParentReport(props.report)) && ReportUtils.isUserCreatedPolicyRoom(props.report) && !isPolicyMember) || (_.isEmpty(ReportUtils.getParentReport(props.report)) && (ReportUtils.isDefaultRoom(props.report) || ReportUtils.isPolicyExpenseChat(props.report)))} + shouldShow={ + _.isEmpty(props.report) || + (_.isEmpty(ReportUtils.getParentReport(props.report)) && ReportUtils.isUserCreatedPolicyRoom(props.report) && !isPolicyMember) || + (_.isEmpty(ReportUtils.getParentReport(props.report)) && (ReportUtils.isDefaultRoom(props.report) || ReportUtils.isPolicyExpenseChat(props.report))) + } subtitleKey={_.isEmpty(props.report) ? undefined : 'roomMembersPage.notAuthorized'} onBackButtonPress={() => Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID))} > From e643deb8b949db852e8aadc34c1fdd918cb9b095 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Wed, 31 Jan 2024 03:29:04 +0200 Subject: [PATCH 017/503] Migrate 'SettingsProfile' page to TypeScript --- src/ROUTES.ts | 2 +- src/components/Form/FormProvider.tsx | 6 +- src/components/Form/types.ts | 6 +- src/components/MenuItem.tsx | 2 +- src/libs/UserUtils.ts | 4 +- src/libs/ValidationUtils.ts | 2 +- .../{StatusPage.js => StatusPage.tsx} | 73 +++---- ...DisplayNamePage.js => DisplayNamePage.tsx} | 76 +++---- ...ungeAccessPage.js => LoungeAccessPage.tsx} | 36 ++-- src/pages/settings/Profile/ProfilePage.js | 186 ------------------ src/pages/settings/Profile/ProfilePage.tsx | 158 +++++++++++++++ ...InitialPage.js => TimezoneInitialPage.tsx} | 45 ++--- ...neSelectPage.js => TimezoneSelectPage.tsx} | 75 +++---- src/types/onyx/PersonalDetails.ts | 2 +- 14 files changed, 295 insertions(+), 378 deletions(-) rename src/pages/settings/Profile/CustomStatus/{StatusPage.js => StatusPage.tsx} (77%) rename src/pages/settings/Profile/{DisplayNamePage.js => DisplayNamePage.tsx} (65%) rename src/pages/settings/Profile/{LoungeAccessPage.js => LoungeAccessPage.tsx} (58%) delete mode 100755 src/pages/settings/Profile/ProfilePage.js create mode 100755 src/pages/settings/Profile/ProfilePage.tsx rename src/pages/settings/Profile/{TimezoneInitialPage.js => TimezoneInitialPage.tsx} (55%) rename src/pages/settings/Profile/{TimezoneSelectPage.js => TimezoneSelectPage.tsx} (52%) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9c4375b84ab6..4d77c8885871 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -31,7 +31,7 @@ const ROUTES = { }, PROFILE_AVATAR: { route: 'a/:accountID/avatar', - getRoute: (accountID: string) => `a/${accountID}/avatar` as const, + getRoute: (accountID: string | number) => `a/${accountID}/avatar` as const, }, TRANSITION_BETWEEN_APPS: 'transition', diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 424fd989291a..a9d20e31c286 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -15,7 +15,7 @@ import type {Errors} from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import FormContext from './FormContext'; import FormWrapper from './FormWrapper'; -import type {BaseInputProps, FormProps, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; +import type {BaseInputProps, FormProps, FormRef, InputRefs, OnyxFormKeyWithoutDraft, OnyxFormValues, OnyxFormValuesFields, RegisterInput, ValueTypeKey} from './types'; // In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web. // 200ms delay was chosen as a result of empirical testing. @@ -63,10 +63,6 @@ type FormProviderProps = FormProvider shouldValidateOnChange?: boolean; }; -type FormRef = { - resetForm: (optionalValue: OnyxFormValues) => void; -}; - function FormProvider( { formID, diff --git a/src/components/Form/types.ts b/src/components/Form/types.ts index 447f3205ad68..580e15a5d4d6 100644 --- a/src/components/Form/types.ts +++ b/src/components/Form/types.ts @@ -86,8 +86,12 @@ type FormProps = { footerContent?: ReactNode; }; +type FormRef = { + resetForm: (optionalValue: OnyxFormValues) => void; +}; + type RegisterInput = (inputID: keyof Form, inputProps: TInputProps) => TInputProps; type InputRefs = Record>; -export type {InputWrapperProps, FormProps, RegisterInput, ValidInputs, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft}; +export type {InputWrapperProps, FormRef, FormProps, RegisterInput, ValidInputs, BaseInputProps, ValueTypeKey, OnyxFormValues, OnyxFormValuesFields, InputRefs, OnyxFormKeyWithoutDraft}; diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index 334fa9895205..1a6398c95afb 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -189,7 +189,7 @@ type MenuItemProps = (IconProps | AvatarProps | NoIcon) & { isSmallAvatarSubscriptMenu?: boolean; /** The type of brick road indicator to show. */ - brickRoadIndicator?: ValueOf; + brickRoadIndicator?: ValueOf | '' | null; /** Should render the content in HTML format */ shouldRenderAsHTML?: boolean; diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index 6ec386679a32..3d34d570d2df 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -16,7 +16,7 @@ type AvatarRange = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | type AvatarSource = IconAsset | string; -type LoginListIndicator = ValueOf | ''; +type LoginListIndicator = ValueOf | undefined; let allPersonalDetails: OnyxEntry; Onyx.connect({ @@ -69,7 +69,7 @@ function getLoginListBrickRoadIndicator(loginList: Record): Login if (hasLoginListInfo(loginList)) { return CONST.BRICK_ROAD_INDICATOR_STATUS.INFO; } - return ''; + return undefined; } /** diff --git a/src/libs/ValidationUtils.ts b/src/libs/ValidationUtils.ts index 7eff51c354df..305131d7731a 100644 --- a/src/libs/ValidationUtils.ts +++ b/src/libs/ValidationUtils.ts @@ -338,7 +338,7 @@ function isValidPersonName(value: string) { /** * Checks if the provided string includes any of the provided reserved words */ -function doesContainReservedWord(value: string, reservedWords: string[]): boolean { +function doesContainReservedWord(value: string, reservedWords: readonly string[]): boolean { const valueToCheck = value.trim().toLowerCase(); return reservedWords.some((reservedWord) => valueToCheck.includes(reservedWord.toLowerCase())); } diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.js b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx similarity index 77% rename from src/pages/settings/Profile/CustomStatus/StatusPage.js rename to src/pages/settings/Profile/CustomStatus/StatusPage.tsx index 0183e0cc8a2d..9e9d34f8ae30 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.js +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -1,10 +1,12 @@ -import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import type {ValueOf} from 'type-fest'; import EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {FormRef, OnyxFormValuesFields} from '@components/Form/types'; import HeaderPageLayout from '@components/HeaderPageLayout'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -13,13 +15,13 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; @@ -27,43 +29,45 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import type {Status} from '@src/types/onyx/PersonalDetails'; const INPUT_IDS = { EMOJI_CODE: 'emojiCode', STATUS_TEXT: 'statusText', +} as const; +type StatusPageOnyxProps = { + draftStatus: OnyxEntry; }; -const propTypes = { - ...withCurrentUserPersonalDetailsPropTypes, -}; +type StatusPageProps = StatusPageOnyxProps & WithCurrentUserPersonalDetailsProps; const initialEmoji = '💬'; -function StatusPage({draftStatus, currentUserPersonalDetails}) { +function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const formRef = useRef(null); - const [brickRoadIndicator, setBrickRoadIndicator] = useState(''); - const currentUserEmojiCode = lodashGet(currentUserPersonalDetails, 'status.emojiCode', ''); - const currentUserStatusText = lodashGet(currentUserPersonalDetails, 'status.text', ''); - const currentUserClearAfter = lodashGet(currentUserPersonalDetails, 'status.clearAfter', ''); - const draftEmojiCode = lodashGet(draftStatus, 'emojiCode'); - const draftText = lodashGet(draftStatus, 'text'); - const draftClearAfter = lodashGet(draftStatus, 'clearAfter'); - - const defaultEmoji = draftEmojiCode || currentUserEmojiCode; - const defaultText = draftText || currentUserStatusText; + const formRef = useRef(null); + const [brickRoadIndicator, setBrickRoadIndicator] = useState | '' | null>(''); + const currentUserEmojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; + const currentUserStatusText = currentUserPersonalDetails?.status?.text ?? ''; + const currentUserClearAfter = currentUserPersonalDetails?.status?.clearAfter ?? ''; + const draftEmojiCode = draftStatus?.emojiCode; + const draftText = draftStatus?.text; + const draftClearAfter = draftStatus?.clearAfter; + + const defaultEmoji = draftEmojiCode ?? currentUserEmojiCode; + const defaultText = draftText ?? currentUserStatusText; const customClearAfter = useMemo(() => { - const dataToShow = draftClearAfter || currentUserClearAfter; + const dataToShow = draftClearAfter ?? currentUserClearAfter; return DateUtils.getLocalizedTimePeriodDescription(dataToShow); }, [draftClearAfter, currentUserClearAfter]); const isValidClearAfterDate = useCallback(() => { - const clearAfterTime = draftClearAfter || currentUserClearAfter; - if (clearAfterTime === CONST.CUSTOM_STATUS_TYPES.NEVER || clearAfterTime === '') { + const clearAfterTime = draftClearAfter ?? currentUserClearAfter; + if (clearAfterTime === CONST.CUSTOM_STATUS_TYPES.NEVER ?? clearAfterTime === '') { return true; } @@ -72,15 +76,16 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { const navigateBackToPreviousScreen = useCallback(() => Navigation.goBack(ROUTES.SETTINGS_PROFILE, false, true), []); const updateStatus = useCallback( - ({emojiCode, statusText}) => { - const clearAfterTime = draftClearAfter || currentUserClearAfter || CONST.CUSTOM_STATUS_TYPES.NEVER; + (values: OnyxFormValuesFields) => { + const {emojiCode, statusText} = values; + const clearAfterTime = draftClearAfter ?? currentUserClearAfter ?? CONST.CUSTOM_STATUS_TYPES.NEVER; const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({dateTimeString: clearAfterTime}); if (!isValid && clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER) { setBrickRoadIndicator(isValidClearAfterDate() ? null : CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR); return; } User.updateCustomStatus({ - text: statusText, + text: values.statusText, emojiCode: !emojiCode && statusText ? initialEmoji : emojiCode, clearAfter: clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER ? clearAfterTime : '', }); @@ -100,7 +105,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { emojiCode: '', clearAfter: DateUtils.getEndOfToday(), }); - formRef.current.resetForm({[INPUT_IDS.EMOJI_CODE]: ''}); + formRef.current?.resetForm?.({[INPUT_IDS.EMOJI_CODE]: ''}); InteractionManager.runAfterInteractions(() => { navigateBackToPreviousScreen(); }); @@ -110,9 +115,9 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { useEffect(() => { if (!currentUserEmojiCode && !currentUserClearAfter && !draftClearAfter) { - User.updateDraftCustomStatus({clearAfter: DateUtils.getEndOfToday()}); + User.updateDraftCustomStatus({clearAfter: DateUtils.getEndOfToday()} as Status); } else { - User.updateDraftCustomStatus({clearAfter: currentUserClearAfter}); + User.updateDraftCustomStatus({clearAfter: currentUserClearAfter} as Status); } return () => User.clearDraftCustomStatus(); @@ -164,7 +169,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) { /> - {(!!currentUserEmojiCode || !!currentUserStatusText) && ( + {(!!currentUserEmojiCode ?? !!currentUserStatusText) && ( ({ draftStatus: { key: () => ONYXKEYS.CUSTOM_STATUS_DRAFT, }, - }), -)(StatusPage); + })(StatusPage), +); diff --git a/src/pages/settings/Profile/DisplayNamePage.js b/src/pages/settings/Profile/DisplayNamePage.tsx similarity index 65% rename from src/pages/settings/Profile/DisplayNamePage.js rename to src/pages/settings/Profile/DisplayNamePage.tsx index 3269fc401c01..0906ca90133d 100644 --- a/src/pages/settings/Profile/DisplayNamePage.js +++ b/src/pages/settings/Profile/DisplayNamePage.tsx @@ -1,19 +1,19 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; @@ -21,40 +21,29 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; -const propTypes = { - ...withLocalizePropTypes, - ...withCurrentUserPersonalDetailsPropTypes, - isLoadingApp: PropTypes.bool, +type DisplayNamePageOnyxProps = { + isLoadingApp: OnyxEntry; }; -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, - isLoadingApp: true, -}; +type DisplayNamePageProps = DisplayNamePageOnyxProps & WithCurrentUserPersonalDetailsProps; /** * Submit form to update user's first and last name (and display name) - * @param {Object} values - * @param {String} values.firstName - * @param {String} values.lastName */ -const updateDisplayName = (values) => { +const updateDisplayName = (values: OnyxFormValuesFields) => { PersonalDetails.updateDisplayName(values.firstName.trim(), values.lastName.trim()); }; -function DisplayNamePage(props) { +function DisplayNamePage({isLoadingApp = true, currentUserPersonalDetails}: DisplayNamePageProps) { const styles = useThemeStyles(); - const currentUserDetails = props.currentUserPersonalDetails || {}; + const {translate} = useLocalize(); + + const currentUserDetails = currentUserPersonalDetails ?? {}; - /** - * @param {Object} values - * @param {String} values.firstName - * @param {String} values.lastName - * @returns {Object} - An object containing the errors for each inputID - */ - const validate = (values) => { - const errors = {}; + const validate = (values: OnyxFormValuesFields) => { + const errors: Errors = {}; // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { @@ -73,7 +62,6 @@ function DisplayNamePage(props) { } return errors; }; - return ( Navigation.goBack(ROUTES.SETTINGS_PROFILE)} /> - {props.isLoadingApp ? ( + {isLoadingApp ? ( ) : ( - {props.translate('displayNamePage.isShownOnProfile')} + {translate('displayNamePage.isShownOnProfile')} @@ -116,10 +104,10 @@ function DisplayNamePage(props) { InputComponent={TextInput} inputID="lastName" name="lname" - label={props.translate('common.lastName')} - aria-label={props.translate('common.lastName')} + label={translate('common.lastName')} + aria-label={translate('common.lastName')} role={CONST.ROLE.PRESENTATION} - defaultValue={lodashGet(currentUserDetails, 'lastName', '')} + defaultValue={currentUserDetails?.lastName ?? ''} maxLength={CONST.DISPLAY_NAME.MAX_LENGTH} spellCheck={false} /> @@ -130,16 +118,12 @@ function DisplayNamePage(props) { ); } -DisplayNamePage.propTypes = propTypes; -DisplayNamePage.defaultProps = defaultProps; DisplayNamePage.displayName = 'DisplayNamePage'; -export default compose( - withLocalize, - withCurrentUserPersonalDetails, - withOnyx({ +export default withCurrentUserPersonalDetails( + withOnyx({ isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, }, - }), -)(DisplayNamePage); + })(DisplayNamePage), +); diff --git a/src/pages/settings/Profile/LoungeAccessPage.js b/src/pages/settings/Profile/LoungeAccessPage.tsx similarity index 58% rename from src/pages/settings/Profile/LoungeAccessPage.js rename to src/pages/settings/Profile/LoungeAccessPage.tsx index 60cb0896a4eb..7edc3c2956f6 100644 --- a/src/pages/settings/Profile/LoungeAccessPage.js +++ b/src/pages/settings/Profile/LoungeAccessPage.tsx @@ -1,35 +1,30 @@ import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; import IllustratedHeaderPageLayout from '@components/IllustratedHeaderPageLayout'; import LottieAnimations from '@components/LottieAnimations'; import Text from '@components/Text'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; -import userPropTypes from '@pages/settings/userPropTypes'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {User} from '@src/types/onyx'; -const propTypes = { - /** Current user details, which will hold whether or not they have Lounge Access */ - user: userPropTypes, - - ...withCurrentUserPersonalDetailsPropTypes, +type LoungeAccessPageOnyxProps = { + user: OnyxEntry; }; -const defaultProps = { - user: {}, - ...withCurrentUserPersonalDetailsDefaultProps, -}; +type LoungeAccessPageProps = LoungeAccessPageOnyxProps & WithCurrentUserPersonalDetailsProps; -function LoungeAccessPage(props) { +function LoungeAccessPage({user}: LoungeAccessPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - if (!props.user.hasLoungeAccess) { + if (!user?.hasLoungeAccess) { return ; } @@ -45,20 +40,17 @@ function LoungeAccessPage(props) { > {translate('loungeAccessPage.headline')} - {translate('loungeAccessPage.description')} + {translate('loungeAccessPage.description')} ); } -LoungeAccessPage.propTypes = propTypes; -LoungeAccessPage.defaultProps = defaultProps; LoungeAccessPage.displayName = 'LoungeAccessPage'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ +export default withCurrentUserPersonalDetails( + withOnyx({ user: { key: ONYXKEYS.USER, }, - }), -)(LoungeAccessPage); + })(LoungeAccessPage), +); diff --git a/src/pages/settings/Profile/ProfilePage.js b/src/pages/settings/Profile/ProfilePage.js deleted file mode 100755 index 99cc5cf7e35a..000000000000 --- a/src/pages/settings/Profile/ProfilePage.js +++ /dev/null @@ -1,186 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React, {useEffect} from 'react'; -import {ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; -import MenuItem from '@components/MenuItem'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import ScreenWrapper from '@components/ScreenWrapper'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import * as UserUtils from '@libs/UserUtils'; -import userPropTypes from '@pages/settings/userPropTypes'; -import * as App from '@userActions/App'; -import * as PersonalDetails from '@userActions/PersonalDetails'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; - -const propTypes = { - /* Onyx Props */ - - /** Login list for the user that is signed in */ - loginList: PropTypes.objectOf( - PropTypes.shape({ - /** Date login was validated, used to show brickroad info status */ - validatedDate: PropTypes.string, - - /** Field-specific server side errors keyed by microtime */ - errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), - }), - ), - - user: userPropTypes, - - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, - ...withCurrentUserPersonalDetailsPropTypes, -}; - -const defaultProps = { - loginList: {}, - user: {}, - ...withCurrentUserPersonalDetailsDefaultProps, -}; - -function ProfilePage(props) { - const styles = useThemeStyles(); - const getPronouns = () => { - let pronounsKey = lodashGet(props.currentUserPersonalDetails, 'pronouns', ''); - if (pronounsKey.startsWith(CONST.PRONOUNS.PREFIX)) { - pronounsKey = pronounsKey.slice(CONST.PRONOUNS.PREFIX.length); - } - - if (!pronounsKey) { - return props.translate('profilePage.selectYourPronouns'); - } - return props.translate(`pronouns.${pronounsKey}`); - }; - const currentUserDetails = props.currentUserPersonalDetails || {}; - const contactMethodBrickRoadIndicator = UserUtils.getLoginListBrickRoadIndicator(props.loginList); - const avatarURL = lodashGet(currentUserDetails, 'avatar', ''); - const accountID = lodashGet(currentUserDetails, 'accountID', ''); - const emojiCode = lodashGet(props, 'currentUserPersonalDetails.status.emojiCode', ''); - - const profileSettingsOptions = [ - { - description: props.translate('displayNamePage.headerTitle'), - title: lodashGet(currentUserDetails, 'displayName', ''), - pageRoute: ROUTES.SETTINGS_DISPLAY_NAME, - }, - { - description: props.translate('contacts.contactMethod'), - title: props.formatPhoneNumber(lodashGet(currentUserDetails, 'login', '')), - pageRoute: ROUTES.SETTINGS_CONTACT_METHODS.route, - brickRoadIndicator: contactMethodBrickRoadIndicator, - }, - ...[ - { - description: props.translate('statusPage.status'), - title: emojiCode ? `${emojiCode} ${lodashGet(props, 'currentUserPersonalDetails.status.text', '')}` : '', - pageRoute: ROUTES.SETTINGS_STATUS, - }, - ], - { - description: props.translate('pronounsPage.pronouns'), - title: getPronouns(), - pageRoute: ROUTES.SETTINGS_PRONOUNS, - }, - { - description: props.translate('timezonePage.timezone'), - title: `${lodashGet(currentUserDetails, 'timezone.selected', '')}`, - pageRoute: ROUTES.SETTINGS_TIMEZONE, - }, - ]; - - useEffect(() => { - App.openProfile(props.currentUserPersonalDetails); - }, [props.currentUserPersonalDetails]); - - return ( - - Navigation.goBack(ROUTES.SETTINGS)} - /> - - Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(accountID))} - previewSource={UserUtils.getFullSizeAvatar(avatarURL, accountID)} - originalFileName={currentUserDetails.originalFileName} - headerTitle={props.translate('profilePage.profileAvatar')} - style={[styles.mh5]} - fallbackIcon={lodashGet(currentUserDetails, 'fallbackIcon')} - /> - - {_.map(profileSettingsOptions, (detail, index) => ( - Navigation.navigate(detail.pageRoute)} - brickRoadIndicator={detail.brickRoadIndicator} - /> - ))} - - Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS)} - shouldShowRightIcon - /> - {props.user.hasLoungeAccess && ( - Navigation.navigate(ROUTES.SETTINGS_LOUNGE_ACCESS)} - shouldShowRightIcon - /> - )} - - - ); -} - -ProfilePage.propTypes = propTypes; -ProfilePage.defaultProps = defaultProps; -ProfilePage.displayName = 'ProfilePage'; - -export default compose( - withLocalize, - withWindowDimensions, - withCurrentUserPersonalDetails, - withOnyx({ - loginList: { - key: ONYXKEYS.LOGIN_LIST, - }, - user: { - key: ONYXKEYS.USER, - }, - }), -)(ProfilePage); diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx new file mode 100755 index 000000000000..c9a6a1e680b0 --- /dev/null +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -0,0 +1,158 @@ +import React, {useEffect} from 'react'; +import {ScrollView, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx/lib/types'; +import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScreenWrapper from '@components/ScreenWrapper'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; +import Navigation from '@libs/Navigation/Navigation'; +import * as UserUtils from '@libs/UserUtils'; +import * as App from '@userActions/App'; +import * as PersonalDetails from '@userActions/PersonalDetails'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {LoginList, PersonalDetails as PersonalDetailsType, User} from '@src/types/onyx'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type ProfilePageOnyxProps = { + loginList: OnyxEntry; + user: OnyxEntry; +}; + +type ProfilePageProps = ProfilePageOnyxProps & WithCurrentUserPersonalDetailsProps; + +function ProfilePage({loginList, user, currentUserPersonalDetails}: ProfilePageProps) { + const styles = useThemeStyles(); + + const {translate} = useLocalize(); + const {windowWidth} = useWindowDimensions(); + + const getPronouns = (): string => { + const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? ''; + return pronounsKey ? translate(`pronouns.${pronounsKey}` as TranslationPaths) : translate('profilePage.selectYourPronouns'); + }; + + const contactMethodBrickRoadIndicator = loginList ? UserUtils.getLoginListBrickRoadIndicator(loginList) : undefined; + const avatarURL = currentUserPersonalDetails?.avatar ?? ''; + const accountID = currentUserPersonalDetails?.accountID ?? ''; + const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; + + const profileSettingsOptions = [ + { + description: translate('displayNamePage.headerTitle'), + title: currentUserPersonalDetails?.displayName ?? '', + pageRoute: ROUTES.SETTINGS_DISPLAY_NAME, + }, + { + description: translate('contacts.contactMethod'), + title: LocalePhoneNumber.formatPhoneNumber(currentUserPersonalDetails?.login ?? ''), + pageRoute: ROUTES.SETTINGS_CONTACT_METHODS.route, + brickRoadIndicator: contactMethodBrickRoadIndicator, + }, + { + description: translate('statusPage.status'), + title: emojiCode ? `${emojiCode} ${currentUserPersonalDetails?.status?.text ?? ''}` : '', + pageRoute: ROUTES.SETTINGS_STATUS, + }, + { + description: translate('pronounsPage.pronouns'), + title: getPronouns(), + pageRoute: ROUTES.SETTINGS_PRONOUNS, + }, + { + description: translate('timezonePage.timezone'), + title: currentUserPersonalDetails?.timezone?.selected ?? '', + pageRoute: ROUTES.SETTINGS_TIMEZONE, + }, + ]; + + useEffect(() => { + App.openProfile(currentUserPersonalDetails as PersonalDetailsType); + }, [currentUserPersonalDetails]); + + return ( + + Navigation.goBack(ROUTES.SETTINGS)} + /> + + Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(accountID))} + previewSource={UserUtils.getFullSizeAvatar(avatarURL, accountID)} + originalFileName={currentUserPersonalDetails?.originalFileName} + headerTitle={translate('profilePage.profileAvatar')} + style={[styles.mh5]} + fallbackIcon={currentUserPersonalDetails?.fallbackIcon} + /> + + {profileSettingsOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + brickRoadIndicator={detail.brickRoadIndicator} + /> + ))} + + Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS)} + shouldShowRightIcon + /> + {user?.hasLoungeAccess && ( + Navigation.navigate(ROUTES.SETTINGS_LOUNGE_ACCESS)} + shouldShowRightIcon + /> + )} + + + ); +} + +ProfilePage.displayName = 'ProfilePage'; + +export default withCurrentUserPersonalDetails( + withOnyx({ + loginList: { + key: ONYXKEYS.LOGIN_LIST, + }, + user: { + key: ONYXKEYS.USER, + }, + })(ProfilePage), +); diff --git a/src/pages/settings/Profile/TimezoneInitialPage.js b/src/pages/settings/Profile/TimezoneInitialPage.tsx similarity index 55% rename from src/pages/settings/Profile/TimezoneInitialPage.js rename to src/pages/settings/Profile/TimezoneInitialPage.tsx index bf86e8a5a077..cc63a985b6ba 100644 --- a/src/pages/settings/Profile/TimezoneInitialPage.js +++ b/src/pages/settings/Profile/TimezoneInitialPage.tsx @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import React from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -6,62 +5,56 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; import Switch from '@components/Switch'; import Text from '@components/Text'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; +import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; -const propTypes = { - ...withLocalizePropTypes, - ...withCurrentUserPersonalDetailsPropTypes, -}; +type TimezoneInitialPageProps = WithCurrentUserPersonalDetailsProps; -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, -}; - -function TimezoneInitialPage(props) { +function TimezoneInitialPage({currentUserPersonalDetails}: TimezoneInitialPageProps) { const styles = useThemeStyles(); - const timezone = lodashGet(props.currentUserPersonalDetails, 'timezone', CONST.DEFAULT_TIME_ZONE); + const timezone: Timezone = currentUserPersonalDetails?.timezone ?? CONST.DEFAULT_TIME_ZONE; + + const {translate} = useLocalize(); /** * Updates setting for automatic timezone selection. * Note: If we are updating automatically, we'll immediately calculate the user's timezone. - * - * @param {Boolean} isAutomatic */ - const updateAutomaticTimezone = (isAutomatic) => { + const updateAutomaticTimezone = (isAutomatic: boolean) => { PersonalDetails.updateAutomaticTimezone({ automatic: isAutomatic, - selected: isAutomatic ? Intl.DateTimeFormat().resolvedOptions().timeZone : timezone.selected, + selected: isAutomatic ? (Intl.DateTimeFormat().resolvedOptions().timeZone as SelectedTimezone) : timezone.selected, }); }; return ( Navigation.goBack(ROUTES.SETTINGS_PROFILE)} /> - {props.translate('timezonePage.isShownOnProfile')} + {translate('timezonePage.isShownOnProfile')} - {props.translate('timezonePage.getLocationAutomatically')} + {translate('timezonePage.getLocationAutomatically')} Navigation.navigate(ROUTES.SETTINGS_TIMEZONE_SELECT)} @@ -71,8 +64,6 @@ function TimezoneInitialPage(props) { ); } -TimezoneInitialPage.propTypes = propTypes; -TimezoneInitialPage.defaultProps = defaultProps; TimezoneInitialPage.displayName = 'TimezoneInitialPage'; -export default compose(withLocalize, withCurrentUserPersonalDetails)(TimezoneInitialPage); +export default withCurrentUserPersonalDetails(TimezoneInitialPage); diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.tsx similarity index 52% rename from src/pages/settings/Profile/TimezoneSelectPage.js rename to src/pages/settings/Profile/TimezoneSelectPage.tsx index 8280d9b5c604..933f1dfc5f2a 100644 --- a/src/pages/settings/Profile/TimezoneSelectPage.js +++ b/src/pages/settings/Profile/TimezoneSelectPage.tsx @@ -1,10 +1,9 @@ -import lodashGet from 'lodash/get'; import React, {useState} from 'react'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useInitialValue from '@hooks/useInitialValue'; import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; @@ -12,67 +11,45 @@ import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import TIMEZONES from '@src/TIMEZONES'; +import type {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; -const propTypes = { - ...withCurrentUserPersonalDetailsPropTypes, -}; - -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, -}; +type TimezoneSelectPageProps = Pick; /** * We add the current time to the key to fix a bug where the list options don't update unless the key is updated. - * @param {String} text - * @return {string} key for list item */ -const getKey = (text) => `${text}-${new Date().getTime()}`; +const getKey = (text: string): string => `${text}-${new Date().getTime()}`; -/** - * @param {Object} currentUserPersonalDetails - * @return {Object} user's timezone data - */ -const getUserTimezone = (currentUserPersonalDetails) => lodashGet(currentUserPersonalDetails, 'timezone', CONST.DEFAULT_TIME_ZONE); +const getUserTimezone = ({currentUserPersonalDetails}: Pick) => + currentUserPersonalDetails?.timezone ?? CONST.DEFAULT_TIME_ZONE; -function TimezoneSelectPage(props) { +function TimezoneSelectPage({currentUserPersonalDetails}: TimezoneSelectPageProps) { const {translate} = useLocalize(); - const timezone = getUserTimezone(props.currentUserPersonalDetails); + const timezone = getUserTimezone({currentUserPersonalDetails}); const allTimezones = useInitialValue(() => - _.chain(TIMEZONES) - .filter((tz) => !tz.startsWith('Etc/GMT')) - .map((text) => ({ - text, - keyForList: getKey(text), - isSelected: text === timezone.selected, - })) - .value(), + TIMEZONES.filter((tz: string) => !tz.startsWith('Etc/GMT')).map((text: string) => ({ + text, + keyForList: getKey(text), + isSelected: text === timezone.selected, + })), ); const [timezoneInputText, setTimezoneInputText] = useState(''); const [timezoneOptions, setTimezoneOptions] = useState(allTimezones); - /** - * @param {Object} timezone - * @param {String} timezone.text - */ - const saveSelectedTimezone = ({text}) => { - PersonalDetails.updateSelectedTimezone(text); + const saveSelectedTimezone = ({text}: {text: string}) => { + PersonalDetails.updateSelectedTimezone(text as SelectedTimezone); }; - /** - * @param {String} searchText - */ - const filterShownTimezones = (searchText) => { + const filterShownTimezones = (searchText: string) => { setTimezoneInputText(searchText); - const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) || []; + const searchWords = searchText.toLowerCase().match(/[a-z0-9]+/g) ?? []; setTimezoneOptions( - _.filter(allTimezones, (tz) => - _.every( - searchWords, - (word) => - tz.text - .toLowerCase() - .replace(/[^a-z0-9]/g, ' ') - .indexOf(word) > -1, + allTimezones.filter((tz) => + searchWords.every((word) => + tz.text + .toLowerCase() + .replace(/[^a-z0-9]/g, ' ') + .includes(word), ), ), ); @@ -94,7 +71,7 @@ function TimezoneSelectPage(props) { onChangeText={filterShownTimezones} onSelectRow={saveSelectedTimezone} sections={[{data: timezoneOptions, indexOffset: 0, isDisabled: timezone.automatic}]} - initiallyFocusedOptionKey={_.get(_.filter(timezoneOptions, (tz) => tz.text === timezone.selected)[0], 'keyForList')} + initiallyFocusedOptionKey={timezoneOptions.find((tz) => tz.text === timezone.selected)?.keyForList} showScrollIndicator shouldShowTooltips={false} /> @@ -102,8 +79,6 @@ function TimezoneSelectPage(props) { ); } -TimezoneSelectPage.propTypes = propTypes; -TimezoneSelectPage.defaultProps = defaultProps; TimezoneSelectPage.displayName = 'TimezoneSelectPage'; export default withCurrentUserPersonalDetails(TimezoneSelectPage); diff --git a/src/types/onyx/PersonalDetails.ts b/src/types/onyx/PersonalDetails.ts index 8d02d7cf26fc..5e74743c8ac1 100644 --- a/src/types/onyx/PersonalDetails.ts +++ b/src/types/onyx/PersonalDetails.ts @@ -17,7 +17,7 @@ type Status = { emojiCode: string; /** The text of the draft status */ - text?: string; + text: string; /** The timestamp of when the status should be cleared */ clearAfter: string; // ISO 8601 format; From 1057dd6c48edbc4083d467fa9c0bb44e592bd129 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 31 Jan 2024 22:56:21 +0700 Subject: [PATCH 018/503] fix: Approved expense preview does not show GBR when submitter needs to add a bank account --- .../ReportActionItem/ReportPreview.tsx | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index b2fece085f57..beef49c0c015 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -25,12 +25,13 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as IOU from '@userActions/IOU'; +import * as store from '@userActions/ReimbursementAccount/store'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report, ReportAction, Session, Transaction, TransactionViolations} from '@src/types/onyx'; -import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {IOUMessage, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ReportActionItemImages from './ReportActionItemImages'; type ReportPreviewOnyxProps = { @@ -228,6 +229,16 @@ function ReportPreview({ return isCurrentUserManager && !isDraftExpenseReport && !isApproved && !iouSettled; }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; + const paymentType = (action.originalMessage as IOUMessage).paymentType; + const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(iouReportID) && !ReportUtils.isSettled(iouReportID); + + const shouldPromptUserToAddBankAccount = + action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED && + isSubmitterOfUnsettledReport && + !store.hasCreditBankAccount() && + paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY; + const shouldShowRBR = !iouSettled && hasErrors; + return ( @@ -256,12 +267,18 @@ function ReportPreview({ {getPreviewMessage()} - {!iouSettled && hasErrors && ( + {shouldShowRBR && ( )} + {!shouldShowRBR && shouldPromptUserToAddBankAccount && ( + + )} From 9ffd082c9f8027534012db6d2ef28d635c1801bc Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 1 Feb 2024 16:02:50 +0700 Subject: [PATCH 019/503] create util function --- .../ReportActionItem/ReportPreview.tsx | 17 +++++------------ src/libs/ReportUtils.ts | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index beef49c0c015..ae7edc3c6d71 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -1,8 +1,8 @@ import React, {useMemo} from 'react'; -import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {View} from 'react-native'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; import Button from '@components/Button'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -25,13 +25,12 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import type {ContextMenuAnchor} from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import * as IOU from '@userActions/IOU'; -import * as store from '@userActions/ReimbursementAccount/store'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Policy, Report, ReportAction, Session, Transaction, TransactionViolations} from '@src/types/onyx'; -import type {IOUMessage, PaymentMethodType} from '@src/types/onyx/OriginalMessage'; +import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ReportActionItemImages from './ReportActionItemImages'; type ReportPreviewOnyxProps = { @@ -229,14 +228,8 @@ function ReportPreview({ return isCurrentUserManager && !isDraftExpenseReport && !isApproved && !iouSettled; }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; - const paymentType = (action.originalMessage as IOUMessage).paymentType; - const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(iouReportID) && !ReportUtils.isSettled(iouReportID); - - const shouldPromptUserToAddBankAccount = - action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED && - isSubmitterOfUnsettledReport && - !store.hasCreditBankAccount() && - paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY; + + const shouldPromptUserToAddBankAccount = ReportUtils.hasAddBankAccountAction(iouReportID); const shouldShowRBR = !iouSettled && hasErrors; return ( diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 228db29aea6c..29219e7df037 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -25,6 +25,7 @@ import type {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import type IconAsset from '@src/types/utils/IconAsset'; +import * as store from './actions/ReimbursementAccount/store'; import * as CollectionUtils from './CollectionUtils'; import * as CurrencyUtils from './CurrencyUtils'; import DateUtils from './DateUtils'; @@ -4613,6 +4614,22 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry + action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED && + isSubmitterOfUnsettledReport && + !hasCreditBankAccount && + (action.originalMessage as IOUMessage)?.paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + ); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -4798,6 +4815,7 @@ export { isReportParticipant, isValidReport, isReportFieldOfTypeTitle, + hasAddBankAccountAction, }; export type { From 3b9eb00bbd396ea6e7b5dbd5e03adb96dfe8289f Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 5 Feb 2024 10:48:19 +0700 Subject: [PATCH 020/503] refactor code --- src/libs/ReportUtils.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1760635a8352..1a5883909009 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4769,11 +4769,14 @@ function hasAddBankAccountAction(iouReportID: string): boolean { const reportActions = ReportActionsUtils.getAllReportActions(iouReportID); const isSubmitterOfUnsettledReport = isCurrentUserSubmitter(iouReportID) && !isSettled(iouReportID); const hasCreditBankAccount = store.hasCreditBankAccount(); + + if(!isSubmitterOfUnsettledReport || hasCreditBankAccount){ + return false; + } + return !!Object.values(reportActions).find( (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED && - isSubmitterOfUnsettledReport && - !hasCreditBankAccount && (action.originalMessage as IOUMessage)?.paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY, ); } From 652298dd7c4c8712934d600ff80ea4e3b571506b Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 5 Feb 2024 10:53:06 +0700 Subject: [PATCH 021/503] lint fix --- src/libs/ReportUtils.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 1a5883909009..92e24694a56b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4770,14 +4770,12 @@ function hasAddBankAccountAction(iouReportID: string): boolean { const isSubmitterOfUnsettledReport = isCurrentUserSubmitter(iouReportID) && !isSettled(iouReportID); const hasCreditBankAccount = store.hasCreditBankAccount(); - if(!isSubmitterOfUnsettledReport || hasCreditBankAccount){ + if (!isSubmitterOfUnsettledReport || hasCreditBankAccount) { return false; } - + return !!Object.values(reportActions).find( - (action) => - action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED && - (action.originalMessage as IOUMessage)?.paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY, + (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED && (action.originalMessage as IOUMessage)?.paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY, ); } From 67089268d0000f500bd4f70166c320addc50aca2 Mon Sep 17 00:00:00 2001 From: Jasper Huang Date: Mon, 12 Feb 2024 19:56:36 -0800 Subject: [PATCH 022/503] simplify condition --- src/pages/ReportDetailsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReportDetailsPage.tsx b/src/pages/ReportDetailsPage.tsx index 3c2ad805ea23..1a263ad537a4 100644 --- a/src/pages/ReportDetailsPage.tsx +++ b/src/pages/ReportDetailsPage.tsx @@ -119,7 +119,7 @@ function ReportDetailsPage({policies, report, session, personalDetails}: ReportD subtitle: participants.length, isAnonymousAction: false, action: () => { - if ((report?.type === CONST.REPORT.TYPE.CHAT && report?.parentReportID) ?? isUserCreatedPolicyRoom) { + if (isUserCreatedPolicyRoom || ReportUtils.isChatThread(report)) { Navigation.navigate(ROUTES.ROOM_MEMBERS.getRoute(report?.reportID ?? '')); } else { Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report?.reportID ?? '')); From 42aa582deb02445448f4a01f48c3e08232f167dd Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Wed, 14 Feb 2024 03:12:23 +0200 Subject: [PATCH 023/503] Updating profile page --- src/pages/settings/Profile/ProfilePage.tsx | 175 +++++++++++++-------- 1 file changed, 109 insertions(+), 66 deletions(-) diff --git a/src/pages/settings/Profile/ProfilePage.tsx b/src/pages/settings/Profile/ProfilePage.tsx index c9a6a1e680b0..7a0c63fb102b 100755 --- a/src/pages/settings/Profile/ProfilePage.tsx +++ b/src/pages/settings/Profile/ProfilePage.tsx @@ -1,42 +1,61 @@ import React, {useEffect} from 'react'; import {ScrollView, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; -import AvatarWithImagePicker from '@components/AvatarWithImagePicker'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import * as Expensicons from '@components/Icon/Expensicons'; -import MenuItem from '@components/MenuItem'; +import * as Illustrations from '@components/Icon/Illustrations'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import ScreenWrapper from '@components/ScreenWrapper'; +import Section from '@components/Section'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; +import usePrivatePersonalDetails from '@hooks/usePrivatePersonalDetails'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as UserUtils from '@libs/UserUtils'; import * as App from '@userActions/App'; -import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {LoginList, PersonalDetails as PersonalDetailsType, User} from '@src/types/onyx'; -import type IconAsset from '@src/types/utils/IconAsset'; +import type {LoginList, PersonalDetails, PrivatePersonalDetails} from '@src/types/onyx'; type ProfilePageOnyxProps = { loginList: OnyxEntry; - user: OnyxEntry; + /** User's private personal details */ + privatePersonalDetails: OnyxEntry; }; type ProfilePageProps = ProfilePageOnyxProps & WithCurrentUserPersonalDetailsProps; -function ProfilePage({loginList, user, currentUserPersonalDetails}: ProfilePageProps) { +function ProfilePage({ + loginList, + privatePersonalDetails = { + legalFirstName: '', + legalLastName: '', + dob: '', + address: { + street: '', + city: '', + state: '', + zip: '', + country: '', + }, + }, + currentUserPersonalDetails, +}: ProfilePageProps) { + const theme = useTheme(); const styles = useThemeStyles(); - + const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const {windowWidth} = useWindowDimensions(); + const {isSmallScreenWidth} = useWindowDimensions(); const getPronouns = (): string => { const pronounsKey = currentUserPersonalDetails?.pronouns?.replace(CONST.PRONOUNS.PREFIX, '') ?? ''; @@ -44,11 +63,13 @@ function ProfilePage({loginList, user, currentUserPersonalDetails}: ProfilePageP }; const contactMethodBrickRoadIndicator = loginList ? UserUtils.getLoginListBrickRoadIndicator(loginList) : undefined; - const avatarURL = currentUserPersonalDetails?.avatar ?? ''; - const accountID = currentUserPersonalDetails?.accountID ?? ''; const emojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; + usePrivatePersonalDetails(); + const privateDetails = privatePersonalDetails ?? {}; + const legalName = `${privateDetails.legalFirstName ?? ''} ${privateDetails.legalLastName ?? ''}`.trim(); + const isLoadingPersonalDetails = privatePersonalDetails?.isLoading ?? true; - const profileSettingsOptions = [ + const publicOptions = [ { description: translate('displayNamePage.headerTitle'), title: currentUserPersonalDetails?.displayName ?? '', @@ -78,67 +99,89 @@ function ProfilePage({loginList, user, currentUserPersonalDetails}: ProfilePageP ]; useEffect(() => { - App.openProfile(currentUserPersonalDetails as PersonalDetailsType); + App.openProfile(currentUserPersonalDetails as PersonalDetails); }, [currentUserPersonalDetails]); + const privateOptions = [ + { + description: translate('privatePersonalDetails.legalName'), + title: legalName, + pageRoute: ROUTES.SETTINGS_LEGAL_NAME, + }, + { + description: translate('common.dob'), + title: privateDetails.dob ?? '', + pageRoute: ROUTES.SETTINGS_DATE_OF_BIRTH, + }, + { + description: translate('privatePersonalDetails.address'), + title: PersonalDetailsUtils.getFormattedAddress(privateDetails), + pageRoute: ROUTES.SETTINGS_ADDRESS, + }, + ]; + return ( Navigation.goBack(ROUTES.SETTINGS)} + onBackButtonPress={() => Navigation.goBack()} + shouldShowBackButton={isSmallScreenWidth} + icon={Illustrations.Profile} /> - - Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(accountID))} - previewSource={UserUtils.getFullSizeAvatar(avatarURL, accountID)} - originalFileName={currentUserPersonalDetails?.originalFileName} - headerTitle={translate('profilePage.profileAvatar')} - style={[styles.mh5]} - fallbackIcon={currentUserPersonalDetails?.fallbackIcon} - /> - - {profileSettingsOptions.map((detail, index) => ( - Navigation.navigate(detail.pageRoute)} - brickRoadIndicator={detail.brickRoadIndicator} - /> - ))} + + +
+ {publicOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + brickRoadIndicator={detail.brickRoadIndicator} + /> + ))} +
+
+ {isLoadingPersonalDetails ? ( + + ) : ( + <> + {privateOptions.map((detail, index) => ( + Navigation.navigate(detail.pageRoute)} + /> + ))} + + )} +
- Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS)} - shouldShowRightIcon - /> - {user?.hasLoungeAccess && ( - Navigation.navigate(ROUTES.SETTINGS_LOUNGE_ACCESS)} - shouldShowRightIcon - /> - )}
); @@ -151,8 +194,8 @@ export default withCurrentUserPersonalDetails( loginList: { key: ONYXKEYS.LOGIN_LIST, }, - user: { - key: ONYXKEYS.USER, + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, }, })(ProfilePage), ); From ae6d226fbdbddf7a12f1495adb1d1876bdbcce75 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Wed, 14 Feb 2024 05:39:58 +0200 Subject: [PATCH 024/503] migrating the status page --- src/ONYXKEYS.ts | 2 +- .../Profile/CustomStatus/StatusPage.tsx | 20 +++++++++---------- .../settings/Profile/DisplayNamePage.tsx | 5 ++--- src/types/form/SettingsStatusSetForm.ts | 13 +++++++++++- 4 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 0735bc53e56c..f007246c6763 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -403,7 +403,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.MONEY_REQUEST_DATE_FORM]: FormTypes.Form; [ONYXKEYS.FORMS.NEW_CONTACT_METHOD_FORM]: FormTypes.Form; [ONYXKEYS.FORMS.WAYPOINT_FORM]: FormTypes.Form; - [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.Form; + [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_FORM]: FormTypes.SettingsStatusSetForm; [ONYXKEYS.FORMS.SETTINGS_STATUS_CLEAR_DATE_FORM]: FormTypes.Form; [ONYXKEYS.FORMS.SETTINGS_STATUS_SET_CLEAR_AFTER_FORM]: FormTypes.Form; [ONYXKEYS.FORMS.PRIVATE_NOTES_FORM]: FormTypes.PrivateNotesForm; diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx index 629f174c8a2b..61e93324bb16 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -1,12 +1,12 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {InteractionManager, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; import type {ValueOf} from 'type-fest'; import EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {FormRef, OnyxFormValuesFields} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; import HeaderPageLayout from '@components/HeaderPageLayout'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -26,15 +26,13 @@ import DateUtils from '@libs/DateUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/SettingsStatusSetForm'; import type {Status} from '@src/types/onyx/PersonalDetails'; -const INPUT_IDS = { - EMOJI_CODE: 'emojiCode', - STATUS_TEXT: 'statusText', -} as const; type StatusPageOnyxProps = { draftStatus: OnyxEntry; }; @@ -48,7 +46,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const formRef = useRef(null); + const formRef = useRef(null); const [brickRoadIndicator, setBrickRoadIndicator] = useState | '' | null>(''); const currentUserEmojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const currentUserStatusText = currentUserPersonalDetails?.status?.text ?? ''; @@ -76,7 +74,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) const navigateBackToPreviousScreen = useCallback(() => Navigation.goBack(), []); const updateStatus = useCallback( - (values: OnyxFormValuesFields) => { + (values: FormOnyxValues) => { const {emojiCode, statusText} = values; const clearAfterTime = draftClearAfter ?? currentUserClearAfter ?? CONST.CUSTOM_STATUS_TYPES.NEVER; const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({dateTimeString: clearAfterTime}); @@ -124,9 +122,9 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const validateForm = useCallback(() => { + const validateForm = useCallback((): FormInputErrors => { if (brickRoadIndicator) { - return {clearAfter: ''}; + return {clearAfter: '' as TranslationPaths}; } return {}; }, [brickRoadIndicator]); @@ -169,7 +167,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) /> ; @@ -42,8 +41,8 @@ function DisplayNamePage({isLoadingApp = true, currentUserPersonalDetails}: Disp const currentUserDetails = currentUserPersonalDetails ?? {}; - const validate = (values: FormOnyxValues): FormInputErrors => { - const errors: Errors = {}; + const validate = (values: FormOnyxValues) => { + const errors: FormInputErrors = {}; // First we validate the first name field if (!ValidationUtils.isValidDisplayName(values.firstName)) { diff --git a/src/types/form/SettingsStatusSetForm.ts b/src/types/form/SettingsStatusSetForm.ts index 9aeec26c4887..dcb0211ece6f 100644 --- a/src/types/form/SettingsStatusSetForm.ts +++ b/src/types/form/SettingsStatusSetForm.ts @@ -1,6 +1,17 @@ import type Form from './Form'; -type SettingsStatusSetForm = Form; +const INPUT_IDS = { + EMOJI_CODE: 'emojiCode', + STATUS_TEXT: 'statusText', + clearAfter: 'clearAfter', +} as const; + +type SettingsStatusSetForm = Form<{ + [INPUT_IDS.EMOJI_CODE]: string; + [INPUT_IDS.STATUS_TEXT]: string; + [INPUT_IDS.clearAfter]?: string; +}>; // eslint-disable-next-line import/prefer-default-export export type {SettingsStatusSetForm}; +export default INPUT_IDS; From 58bb076836e979123feb3471f7b88ce087cc3db4 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 14 Feb 2024 21:35:48 +0700 Subject: [PATCH 025/503] lint fix --- src/components/ReportActionItem/ReportPreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index 1726e0f5879d..043c73dab90d 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -233,7 +233,7 @@ function ReportPreview({ const shouldPromptUserToAddBankAccount = ReportUtils.hasAddBankAccountAction(iouReportID); const shouldShowRBR = !iouSettled && hasErrors; - + /* Show subtitle if at least one of the money requests is not being smart scanned, and either: - There is more than one money request – in this case, the "X requests, Y scanning" subtitle is shown; From 40cd54db77e7a89c46163a4ba0074fe5c7c114dc Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 14 Feb 2024 15:39:01 +0100 Subject: [PATCH 026/503] Migrate ReimbursementAccount to ts --- .../RestartBankAccountSetupParams.ts | 6 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../deleteFromBankAccountList.ts | 16 ++++ .../actions/ReimbursementAccount/errors.ts | 44 ++++++++++ .../actions/ReimbursementAccount/index.ts | 63 ++++++++++++++ .../ReimbursementAccount/navigation.ts | 24 ++++++ .../resetFreePlanBankAccount.ts | 84 +++++++++++++++++++ .../actions/ReimbursementAccount/store.ts | 69 +++++++++++++++ 9 files changed, 309 insertions(+) create mode 100644 src/libs/API/parameters/RestartBankAccountSetupParams.ts create mode 100644 src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts create mode 100644 src/libs/actions/ReimbursementAccount/errors.ts create mode 100644 src/libs/actions/ReimbursementAccount/index.ts create mode 100644 src/libs/actions/ReimbursementAccount/navigation.ts create mode 100644 src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts create mode 100644 src/libs/actions/ReimbursementAccount/store.ts diff --git a/src/libs/API/parameters/RestartBankAccountSetupParams.ts b/src/libs/API/parameters/RestartBankAccountSetupParams.ts new file mode 100644 index 000000000000..b338eac0dea1 --- /dev/null +++ b/src/libs/API/parameters/RestartBankAccountSetupParams.ts @@ -0,0 +1,6 @@ +type RestartBankAccountSetupParams = { + bankAccountID: number; + ownerEmail: string; +}; + +export default RestartBankAccountSetupParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 482c5e0336c4..90b27d825580 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -2,6 +2,7 @@ export type {default as ActivatePhysicalExpensifyCardParams} from './ActivatePhy export type {default as AddNewContactMethodParams} from './AddNewContactMethodParams'; export type {default as AddPaymentCardParams} from './AddPaymentCardParams'; export type {default as AddPersonalBankAccountParams} from './AddPersonalBankAccountParams'; +export type {default as RestartBankAccountSetupParams} from './RestartBankAccountSetupParams'; export type {default as AddSchoolPrincipalParams} from './AddSchoolPrincipalParams'; export type {default as AuthenticatePusherParams} from './AuthenticatePusherParams'; export type {default as BankAccountHandlePlaidErrorParams} from './BankAccountHandlePlaidErrorParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index f5d99d8cf40e..4f811b85e709 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -76,6 +76,7 @@ const WRITE_COMMANDS = { ADD_ATTACHMENT: 'AddAttachment', CONNECT_BANK_ACCOUNT_WITH_PLAID: 'ConnectBankAccountWithPlaid', ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount', + RESTART_BANK_ACCOUNT_SETUP: 'RestartBankAccountSetup', OPT_IN_TO_PUSH_NOTIFICATIONS: 'OptInToPushNotifications', OPT_OUT_OF_PUSH_NOTIFICATIONS: 'OptOutOfPushNotifications', RECONNECT_TO_REPORT: 'ReconnectToReport', @@ -213,6 +214,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ADD_ATTACHMENT]: Parameters.AddCommentOrAttachementParams; [WRITE_COMMANDS.CONNECT_BANK_ACCOUNT_WITH_PLAID]: Parameters.ConnectBankAccountWithPlaidParams; [WRITE_COMMANDS.ADD_PERSONAL_BANK_ACCOUNT]: Parameters.AddPersonalBankAccountParams; + [WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP]: Parameters.RestartBankAccountSetupParams; [WRITE_COMMANDS.OPT_IN_TO_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; [WRITE_COMMANDS.OPT_OUT_OF_PUSH_NOTIFICATIONS]: Parameters.OptInOutToPushNotificationsParams; [WRITE_COMMANDS.RECONNECT_TO_REPORT]: Parameters.ReconnectToReportParams; diff --git a/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts b/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts new file mode 100644 index 000000000000..d9a2dd130d62 --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.ts @@ -0,0 +1,16 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import * as store from './store'; + +/** + * Deletes a bank account from bankAccountList + */ +function deleteFromBankAccountList(bankAccountID: number) { + // We should delete the bankAccountID key from the bankAccountList object before setting it in Onyx + const bankAccountList = store.getBankAccountList(); + delete bankAccountList?.[bankAccountID]; + + Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); +} + +export default deleteFromBankAccountList; diff --git a/src/libs/actions/ReimbursementAccount/errors.ts b/src/libs/actions/ReimbursementAccount/errors.ts new file mode 100644 index 000000000000..c65da17690bb --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/errors.ts @@ -0,0 +1,44 @@ +import Onyx from 'react-native-onyx'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; + +/** + * Set the current fields with errors. + */ +function setPersonalBankAccountFormValidationErrorFields(errors: Errors) { + // We set 'errors' to null first because we don't have a way yet to replace a specific property without merging it + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors: null}); + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors}); +} + +/** + * Set the current fields with errors. + + */ +function setBankAccountFormValidationErrors(errors: Errors) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors}); +} + +/** + * Clear validation messages from reimbursement account + */ +function resetReimbursementAccount() { + setBankAccountFormValidationErrors({}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { + errors: null, + pendingAction: null, + }); +} + +/** + * Set the current error message. + */ +function showBankAccountFormValidationError(error: string | null) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { + errors: ErrorUtils.getMicroSecondOnyxError(error), + }); +} + +export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, showBankAccountFormValidationError, resetReimbursementAccount}; diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts new file mode 100644 index 000000000000..5c9bf1c822d1 --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -0,0 +1,63 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReimbursementAccountForm} from '@src/types/form'; +import type {BankAccountSubStep} from '@src/types/onyx/ReimbursementAccount'; +import deleteFromBankAccountList from './deleteFromBankAccountList'; +import resetFreePlanBankAccount from './resetFreePlanBankAccount'; + +export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute} from './navigation'; +export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, resetReimbursementAccount, showBankAccountFormValidationError} from './errors'; + +/** + * Set the current sub step in first step of adding withdrawal bank account: + * - `null` if we want to go back to the view where the user selects between connecting via Plaid or connecting manually + * - CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL to ask them to enter their accountNumber and routingNumber + * - CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID to ask them to login to their bank via Plaid + * + * @param subStep + * @returns + */ +function setBankAccountSubStep(subStep: BankAccountSubStep) { + return Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {subStep}}); +} + +function hideBankAccountErrors() { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: '', errors: null}); +} + +function setWorkspaceIDForReimbursementAccount(workspaceID: string) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, workspaceID); +} + +/** + * @param bankAccountData + */ +function updateReimbursementAccountDraft(bankAccountData: ReimbursementAccountForm) { + Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); +} + +/** + * Triggers a modal to open allowing the user to reset their bank account + */ +function requestResetFreePlanBankAccount() { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {shouldShowResetModal: true}); +} + +/** + * Hides modal allowing the user to reset their bank account + */ +function cancelResetFreePlanBankAccount() { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {shouldShowResetModal: false}); +} + +export { + resetFreePlanBankAccount, + setBankAccountSubStep, + hideBankAccountErrors, + setWorkspaceIDForReimbursementAccount, + updateReimbursementAccountDraft, + requestResetFreePlanBankAccount, + cancelResetFreePlanBankAccount, + deleteFromBankAccountList, +}; diff --git a/src/libs/actions/ReimbursementAccount/navigation.ts b/src/libs/actions/ReimbursementAccount/navigation.ts new file mode 100644 index 000000000000..2c3eb7cf0384 --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/navigation.ts @@ -0,0 +1,24 @@ +import Onyx from 'react-native-onyx'; +import Navigation from '@libs/Navigation/Navigation'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {BankAccountStep} from '@src/types/onyx/ReimbursementAccount'; + +/** + * Navigate to a specific step in the VBA flow + */ +function goToWithdrawalAccountSetupStep(stepID: BankAccountStep) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {currentStep: stepID}}); +} + +/** + * Navigate to the correct bank account route based on the bank account state and type + * + * @param policyID - The policy ID associated with the bank account. + * @param [backTo=''] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. + */ +function navigateToBankAccountRoute(policyID: string, backTo?: string) { + Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID, backTo)); +} + +export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute}; diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts new file mode 100644 index 000000000000..3cc34db0846f --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.ts @@ -0,0 +1,84 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import {WRITE_COMMANDS} from '@libs/API/types'; +import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; +import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; + +/** + * Reset user's reimbursement account. This will delete the bank account. + */ +function resetFreePlanBankAccount(bankAccountID: number, session: OnyxEntry) { + if (!bankAccountID) { + throw new Error('Missing bankAccountID when attempting to reset free plan bank account'); + } + if (!session?.email) { + throw new Error('Missing credentials when attempting to reset free plan bank account'); + } + + API.write( + WRITE_COMMANDS.RESTART_BANK_ACCOUNT_SETUP, + { + bankAccountID, + ownerEmail: session.email, + }, + { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + shouldShowResetModal: false, + isLoading: true, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + achData: null, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.ONFIDO_TOKEN, + value: '', + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.ONFIDO_APPLICANT_ID, + value: '', + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.PLAID_DATA, + value: PlaidDataProps.plaidDataDefaultProps, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.PLAID_LINK_TOKEN, + value: '', + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: ReimbursementAccountProps.reimbursementAccountDefaultProps, + }, + { + onyxMethod: Onyx.METHOD.SET, + key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, + value: {}, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: {isLoading: false, pendingAction: null}, + }, + ], + }, + ); +} + +export default resetFreePlanBankAccount; diff --git a/src/libs/actions/ReimbursementAccount/store.ts b/src/libs/actions/ReimbursementAccount/store.ts new file mode 100644 index 000000000000..bdceb4e2ad5d --- /dev/null +++ b/src/libs/actions/ReimbursementAccount/store.ts @@ -0,0 +1,69 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import BankAccount from '@libs/models/BankAccount'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {ACHData} from '@src/types/onyx/ReimbursementAccount'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; + +/** Reimbursement account actively being set up */ +let reimbursementAccountInSetup: ACHData | EmptyObject = {}; +Onyx.connect({ + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + callback: (val) => { + reimbursementAccountInSetup = val?.achData ?? {}; + }, +}); + +let reimbursementAccountWorkspaceID: OnyxEntry = null; +Onyx.connect({ + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, + callback: (val) => { + reimbursementAccountWorkspaceID = val; + }, +}); + +let bankAccountList: OnyxEntry = null; +Onyx.connect({ + key: ONYXKEYS.BANK_ACCOUNT_LIST, + callback: (val) => { + bankAccountList = val; + }, +}); + +let credentials: OnyxEntry = null; +Onyx.connect({ + key: ONYXKEYS.CREDENTIALS, + callback: (val) => { + credentials = val; + }, +}); + +function getReimbursementAccountInSetup() { + return reimbursementAccountInSetup; +} + +function getBankAccountList() { + return bankAccountList; +} + +function hasCreditBankAccount() { + if (!bankAccountList) { + return false; + } + + Object.entries(bankAccountList).some(([, bankAccountJSON]) => { + const bankAccount = new BankAccount(bankAccountJSON); + return bankAccount.isDefaultCredit(); + }); +} + +function getCredentials() { + return credentials; +} + +function getReimbursementAccountWorkspaceID() { + return reimbursementAccountWorkspaceID; +} + +export {getReimbursementAccountInSetup, getBankAccountList, getCredentials, getReimbursementAccountWorkspaceID, hasCreditBankAccount}; From 6a886fefb7d026fb767ec41172b226e1dfad4842 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 14 Feb 2024 15:39:51 +0100 Subject: [PATCH 027/503] Remove old js implementations --- .../deleteFromBankAccountList.js | 18 ---- .../actions/ReimbursementAccount/errors.js | 47 ----------- .../actions/ReimbursementAccount/index.js | 61 -------------- .../ReimbursementAccount/navigation.js | 25 ------ .../resetFreePlanBankAccount.js | 83 ------------------- .../actions/ReimbursementAccount/store.js | 63 -------------- 6 files changed, 297 deletions(-) delete mode 100644 src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.js delete mode 100644 src/libs/actions/ReimbursementAccount/errors.js delete mode 100644 src/libs/actions/ReimbursementAccount/index.js delete mode 100644 src/libs/actions/ReimbursementAccount/navigation.js delete mode 100644 src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js delete mode 100644 src/libs/actions/ReimbursementAccount/store.js diff --git a/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.js b/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.js deleted file mode 100644 index 6161066c1c69..000000000000 --- a/src/libs/actions/ReimbursementAccount/deleteFromBankAccountList.js +++ /dev/null @@ -1,18 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as store from './store'; - -/** - * Deletes a bank account from bankAccountList - * - * @param {Number} bankAccountID - */ -function deleteFromBankAccountList(bankAccountID) { - // We should delete the bankAccountID key from the bankAccountList object before setting it in Onyx - const bankAccountList = store.getBankAccountList(); - delete bankAccountList[bankAccountID]; - - Onyx.merge(ONYXKEYS.BANK_ACCOUNT_LIST, bankAccountList); -} - -export default deleteFromBankAccountList; diff --git a/src/libs/actions/ReimbursementAccount/errors.js b/src/libs/actions/ReimbursementAccount/errors.js deleted file mode 100644 index fd2eaf852bce..000000000000 --- a/src/libs/actions/ReimbursementAccount/errors.js +++ /dev/null @@ -1,47 +0,0 @@ -import Onyx from 'react-native-onyx'; -import * as ErrorUtils from '@libs/ErrorUtils'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** - * Set the current fields with errors. - * @param {Object} errorFields - */ -function setPersonalBankAccountFormValidationErrorFields(errorFields) { - // We set 'errorFields' to null first because we don't have a way yet to replace a specific property without merging it - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields: null}); - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields}); -} - -/** - * Set the current fields with errors. - * - * @param {Object} errorFields - */ -function setBankAccountFormValidationErrors(errorFields) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields: null}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields}); -} - -/** - * Clear validation messages from reimbursement account - */ -function resetReimbursementAccount() { - setBankAccountFormValidationErrors({}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { - errors: null, - pendingAction: null, - }); -} - -/** - * Set the current error message. - * - * @param {String} error - */ -function showBankAccountFormValidationError(error) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, { - errors: ErrorUtils.getMicroSecondOnyxError(error), - }); -} - -export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, showBankAccountFormValidationError, resetReimbursementAccount}; diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js deleted file mode 100644 index 12b5b940a0f2..000000000000 --- a/src/libs/actions/ReimbursementAccount/index.js +++ /dev/null @@ -1,61 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import deleteFromBankAccountList from './deleteFromBankAccountList'; -import resetFreePlanBankAccount from './resetFreePlanBankAccount'; - -export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute} from './navigation'; -export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidationErrorFields, resetReimbursementAccount, showBankAccountFormValidationError} from './errors'; - -/** - * Set the current sub step in first step of adding withdrawal bank account: - * - `null` if we want to go back to the view where the user selects between connecting via Plaid or connecting manually - * - CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL to ask them to enter their accountNumber and routingNumber - * - CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID to ask them to login to their bank via Plaid - * - * @param {String | null} subStep - * @returns {Promise} - */ -function setBankAccountSubStep(subStep) { - return Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {subStep}}); -} - -function hideBankAccountErrors() { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: '', errors: null}); -} - -function setWorkspaceIDForReimbursementAccount(workspaceID) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, workspaceID); -} - -/** - * @param {Object} bankAccountData - */ -function updateReimbursementAccountDraft(bankAccountData) { - Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); -} - -/** - * Triggers a modal to open allowing the user to reset their bank account - */ -function requestResetFreePlanBankAccount() { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {shouldShowResetModal: true}); -} - -/** - * Hides modal allowing the user to reset their bank account - */ -function cancelResetFreePlanBankAccount() { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {shouldShowResetModal: false}); -} - -export { - resetFreePlanBankAccount, - setBankAccountSubStep, - hideBankAccountErrors, - setWorkspaceIDForReimbursementAccount, - updateReimbursementAccountDraft, - requestResetFreePlanBankAccount, - cancelResetFreePlanBankAccount, - deleteFromBankAccountList, -}; diff --git a/src/libs/actions/ReimbursementAccount/navigation.js b/src/libs/actions/ReimbursementAccount/navigation.js deleted file mode 100644 index 6c82561c16ee..000000000000 --- a/src/libs/actions/ReimbursementAccount/navigation.js +++ /dev/null @@ -1,25 +0,0 @@ -import Onyx from 'react-native-onyx'; -import Navigation from '@libs/Navigation/Navigation'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; - -/** - * Navigate to a specific step in the VBA flow - * - * @param {String} stepID - */ -function goToWithdrawalAccountSetupStep(stepID) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {currentStep: stepID}}); -} - -/** - * Navigate to the correct bank account route based on the bank account state and type - * - * @param {string} policyID - The policy ID associated with the bank account. - * @param {string} [backTo=''] - An optional return path. If provided, it will be URL-encoded and appended to the resulting URL. - */ -function navigateToBankAccountRoute(policyID, backTo) { - Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID, backTo)); -} - -export {goToWithdrawalAccountSetupStep, navigateToBankAccountRoute}; diff --git a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js deleted file mode 100644 index 962800fb2e55..000000000000 --- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js +++ /dev/null @@ -1,83 +0,0 @@ -import Onyx from 'react-native-onyx'; -import * as API from '@libs/API'; -import * as PlaidDataProps from '@pages/ReimbursementAccount/plaidDataPropTypes'; -import * as ReimbursementAccountProps from '@pages/ReimbursementAccount/reimbursementAccountPropTypes'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** - * Reset user's reimbursement account. This will delete the bank account. - * @param {Number} bankAccountID - * @param {Object} session - */ -function resetFreePlanBankAccount(bankAccountID, session) { - if (!bankAccountID) { - throw new Error('Missing bankAccountID when attempting to reset free plan bank account'); - } - if (!session.email) { - throw new Error('Missing credentials when attempting to reset free plan bank account'); - } - - API.write( - 'RestartBankAccountSetup', - { - bankAccountID, - ownerEmail: session.email, - }, - { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: { - shouldShowResetModal: false, - isLoading: true, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - achData: null, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.ONFIDO_TOKEN, - value: '', - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.ONFIDO_APPLICANT_ID, - value: '', - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.PLAID_DATA, - value: PlaidDataProps.plaidDataDefaultProps, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.PLAID_LINK_TOKEN, - value: '', - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: ReimbursementAccountProps.reimbursementAccountDefaultProps, - }, - { - onyxMethod: Onyx.METHOD.SET, - key: ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, - value: {}, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - value: {isLoading: false, pendingAction: null}, - }, - ], - }, - ); -} - -export default resetFreePlanBankAccount; diff --git a/src/libs/actions/ReimbursementAccount/store.js b/src/libs/actions/ReimbursementAccount/store.js deleted file mode 100644 index 4b8549b60b2e..000000000000 --- a/src/libs/actions/ReimbursementAccount/store.js +++ /dev/null @@ -1,63 +0,0 @@ -import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import BankAccount from '@libs/models/BankAccount'; -import ONYXKEYS from '@src/ONYXKEYS'; - -/** Reimbursement account actively being set up */ -let reimbursementAccountInSetup = {}; -Onyx.connect({ - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, - callback: (val) => { - reimbursementAccountInSetup = lodashGet(val, 'achData', {}); - }, -}); - -let reimbursementAccountWorkspaceID = null; -Onyx.connect({ - key: ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, - callback: (val) => { - reimbursementAccountWorkspaceID = val; - }, -}); - -let bankAccountList = null; -Onyx.connect({ - key: ONYXKEYS.BANK_ACCOUNT_LIST, - callback: (val) => { - bankAccountList = val; - }, -}); - -let credentials; -Onyx.connect({ - key: ONYXKEYS.CREDENTIALS, - callback: (val) => { - credentials = val || {}; - }, -}); - -function getReimbursementAccountInSetup() { - return reimbursementAccountInSetup; -} - -function getBankAccountList() { - return bankAccountList; -} - -function hasCreditBankAccount() { - return _.some(bankAccountList, (bankAccountJSON) => { - const bankAccount = new BankAccount(bankAccountJSON); - return bankAccount.isDefaultCredit(); - }); -} - -function getCredentials() { - return credentials; -} - -function getReimbursementAccountWorkspaceID() { - return reimbursementAccountWorkspaceID; -} - -export {getReimbursementAccountInSetup, getBankAccountList, getCredentials, getReimbursementAccountWorkspaceID, hasCreditBankAccount}; From 8b6fb5e1e3639f3532c1183e953f416417c6151c Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 14 Feb 2024 15:58:46 +0100 Subject: [PATCH 028/503] Add ErrorFields to PersonalBankAccount type --- src/libs/actions/ReimbursementAccount/errors.ts | 14 +++++++------- src/types/onyx/PersonalBankAccount.ts | 3 +++ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/libs/actions/ReimbursementAccount/errors.ts b/src/libs/actions/ReimbursementAccount/errors.ts index c65da17690bb..f85426f8d4fe 100644 --- a/src/libs/actions/ReimbursementAccount/errors.ts +++ b/src/libs/actions/ReimbursementAccount/errors.ts @@ -1,24 +1,24 @@ import Onyx from 'react-native-onyx'; import * as ErrorUtils from '@libs/ErrorUtils'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Errors} from '@src/types/onyx/OnyxCommon'; +import type {ErrorFields} from '@src/types/onyx/OnyxCommon'; /** * Set the current fields with errors. */ -function setPersonalBankAccountFormValidationErrorFields(errors: Errors) { +function setPersonalBankAccountFormValidationErrorFields(errorFields: ErrorFields) { // We set 'errors' to null first because we don't have a way yet to replace a specific property without merging it - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors: null}); - Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errors}); + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields: null}); + Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {errorFields}); } /** * Set the current fields with errors. */ -function setBankAccountFormValidationErrors(errors: Errors) { - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors: null}); - Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errors}); +function setBankAccountFormValidationErrors(errorFields: ErrorFields) { + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields: null}); + Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {errorFields}); } /** diff --git a/src/types/onyx/PersonalBankAccount.ts b/src/types/onyx/PersonalBankAccount.ts index 3714cc9f314b..3e52a3cf59f3 100644 --- a/src/types/onyx/PersonalBankAccount.ts +++ b/src/types/onyx/PersonalBankAccount.ts @@ -5,6 +5,9 @@ type PersonalBankAccount = { /** An error message to display to the user */ errors?: OnyxCommon.Errors; + /** Error objects keyed by field name containing errors keyed by microtime */ + errorFields?: OnyxCommon.ErrorFields; + /** Whether we should show the view that the bank account was successfully added */ shouldShowSuccess?: boolean; From 53fb383b51d8382c2cfc3e8fc9068adbddd82c07 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Wed, 14 Feb 2024 16:03:55 +0100 Subject: [PATCH 029/503] Adjust types --- src/libs/actions/ReimbursementAccount/index.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/ReimbursementAccount/index.ts b/src/libs/actions/ReimbursementAccount/index.ts index 5c9bf1c822d1..416c5e956189 100644 --- a/src/libs/actions/ReimbursementAccount/index.ts +++ b/src/libs/actions/ReimbursementAccount/index.ts @@ -13,11 +13,8 @@ export {setBankAccountFormValidationErrors, setPersonalBankAccountFormValidation * - `null` if we want to go back to the view where the user selects between connecting via Plaid or connecting manually * - CONST.BANK_ACCOUNT.SETUP_TYPE.MANUAL to ask them to enter their accountNumber and routingNumber * - CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID to ask them to login to their bank via Plaid - * - * @param subStep - * @returns */ -function setBankAccountSubStep(subStep: BankAccountSubStep) { +function setBankAccountSubStep(subStep: BankAccountSubStep | null) { return Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {achData: {subStep}}); } @@ -25,13 +22,10 @@ function hideBankAccountErrors() { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {error: '', errors: null}); } -function setWorkspaceIDForReimbursementAccount(workspaceID: string) { +function setWorkspaceIDForReimbursementAccount(workspaceID: string | null) { Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_WORKSPACE_ID, workspaceID); } -/** - * @param bankAccountData - */ function updateReimbursementAccountDraft(bankAccountData: ReimbursementAccountForm) { Onyx.merge(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM_DRAFT, bankAccountData); Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined}); From dcc5adff694c41c31a48acfbe042bea0e72dac17 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Wed, 14 Feb 2024 16:07:13 +0000 Subject: [PATCH 030/503] refactor(typescript): migrate settings preferences --- src/libs/LocaleUtils.ts | 17 ++++++ .../{LanguagePage.js => LanguagePage.tsx} | 26 ++++----- ...PreferencesPage.js => PreferencesPage.tsx} | 39 ++++++-------- ...iorityModePage.js => PriorityModePage.tsx} | 54 ++++++++----------- .../{ThemePage.js => ThemePage.tsx} | 25 ++++----- 5 files changed, 76 insertions(+), 85 deletions(-) create mode 100644 src/libs/LocaleUtils.ts rename src/pages/settings/Preferences/{LanguagePage.js => LanguagePage.tsx} (53%) rename src/pages/settings/Preferences/{PreferencesPage.js => PreferencesPage.tsx} (87%) rename src/pages/settings/Preferences/{PriorityModePage.js => PriorityModePage.tsx} (50%) rename src/pages/settings/Preferences/{ThemePage.js => ThemePage.tsx} (71%) diff --git a/src/libs/LocaleUtils.ts b/src/libs/LocaleUtils.ts new file mode 100644 index 000000000000..2bcbb946c7c0 --- /dev/null +++ b/src/libs/LocaleUtils.ts @@ -0,0 +1,17 @@ +import type {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; + +const getLanguageFromLocale = (locale: ValueOf): (typeof CONST.LANGUAGES)[number] => { + switch (locale) { + case CONST.LOCALES.ES_ES: + case CONST.LOCALES.ES_ES_ONFIDO: + case CONST.LOCALES.ES: + return CONST.LOCALES.ES; + case CONST.LOCALES.EN: + return CONST.LOCALES.EN; + default: + return CONST.LOCALES.DEFAULT; + } +}; + +export default {getLanguageFromLocale}; diff --git a/src/pages/settings/Preferences/LanguagePage.js b/src/pages/settings/Preferences/LanguagePage.tsx similarity index 53% rename from src/pages/settings/Preferences/LanguagePage.js rename to src/pages/settings/Preferences/LanguagePage.tsx index ce93e94222b5..68ceeb0a1d81 100644 --- a/src/pages/settings/Preferences/LanguagePage.js +++ b/src/pages/settings/Preferences/LanguagePage.tsx @@ -1,27 +1,20 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; -const propTypes = { - ...withLocalizePropTypes, +function LanguagePage() { + const {translate, preferredLocale} = useLocalize(); - /** The preferred language of the App */ - preferredLocale: PropTypes.string.isRequired, -}; - -function LanguagePage(props) { - const localesToLanguages = _.map(CONST.LANGUAGES, (language) => ({ + const localesToLanguages = CONST.LANGUAGES.map((language) => ({ value: language, - text: props.translate(`languagePage.languages.${language}.label`), + text: translate(`languagePage.languages.${language}.label`), keyForList: language, - isSelected: props.preferredLocale === language, + isSelected: preferredLocale === language, })); return ( @@ -30,19 +23,18 @@ function LanguagePage(props) { testID={LanguagePage.displayName} > Navigation.goBack()} /> App.setLocaleAndNavigate(language.value)} - initiallyFocusedOptionKey={_.find(localesToLanguages, (locale) => locale.isSelected).keyForList} + initiallyFocusedOptionKey={localesToLanguages.find((locale) => locale.isSelected)?.keyForList} />
); } LanguagePage.displayName = 'LanguagePage'; -LanguagePage.propTypes = propTypes; -export default withLocalize(LanguagePage); +export default LanguagePage; diff --git a/src/pages/settings/Preferences/PreferencesPage.js b/src/pages/settings/Preferences/PreferencesPage.tsx similarity index 87% rename from src/pages/settings/Preferences/PreferencesPage.js rename to src/pages/settings/Preferences/PreferencesPage.tsx index 5ac78f6d20c6..4b93b330e6e9 100755 --- a/src/pages/settings/Preferences/PreferencesPage.js +++ b/src/pages/settings/Preferences/PreferencesPage.tsx @@ -1,8 +1,8 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React from 'react'; import {ScrollView, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Illustrations from '@components/Icon/Illustrations'; import LottieAnimations from '@components/LottieAnimations'; @@ -16,33 +16,28 @@ import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import LocaleUtils from '@libs/LocaleUtils'; import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {User as UserType} from '@src/types/onyx'; -const propTypes = { +type PreferencesPageOnyxProps = { /** The chat priority mode */ - priorityMode: PropTypes.string, + priorityMode: OnyxEntry>; /** The app's color theme */ - preferredTheme: PropTypes.string, + preferredTheme: OnyxEntry>; /** The details about the user that is signed in */ - user: PropTypes.shape({ - /** Whether or not the user is subscribed to news updates */ - isSubscribedToNewsletter: PropTypes.bool, - }), + user: OnyxEntry; }; -const defaultProps = { - priorityMode: CONST.PRIORITY_MODE.DEFAULT, - preferredTheme: CONST.DEFAULT_THEME, - user: {}, -}; +type PreferencesPageProps = PreferencesPageOnyxProps; -function PreferencesPage(props) { +function PreferencesPage({priorityMode, preferredTheme, user}: PreferencesPageProps) { const styles = useThemeStyles(); const {isProduction} = useEnvironment(); const {translate, preferredLocale} = useLocalize(); @@ -85,7 +80,7 @@ function PreferencesPage(props) { @@ -97,28 +92,28 @@ function PreferencesPage(props) {
Navigation.navigate(ROUTES.SETTINGS_PRIORITY_MODE)} wrapperStyle={styles.sectionMenuItemTopDescription} /> Navigation.navigate(ROUTES.SETTINGS_LANGUAGE)} wrapperStyle={styles.sectionMenuItemTopDescription} /> Navigation.navigate(ROUTES.SETTINGS_THEME)} wrapperStyle={styles.sectionMenuItemTopDescription} @@ -144,11 +139,9 @@ function PreferencesPage(props) { ); } -PreferencesPage.propTypes = propTypes; -PreferencesPage.defaultProps = defaultProps; PreferencesPage.displayName = 'PreferencesPage'; -export default withOnyx({ +export default withOnyx({ priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, }, diff --git a/src/pages/settings/Preferences/PriorityModePage.js b/src/pages/settings/Preferences/PriorityModePage.tsx similarity index 50% rename from src/pages/settings/Preferences/PriorityModePage.js rename to src/pages/settings/Preferences/PriorityModePage.tsx index 983e3cb26746..e6c94c73021e 100644 --- a/src/pages/settings/Preferences/PriorityModePage.js +++ b/src/pages/settings/Preferences/PriorityModePage.tsx @@ -1,48 +1,45 @@ -import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _, {compose} from 'underscore'; +import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const propTypes = { +type PriorityModePageOnyxProps = { /** The chat priority mode */ - priorityMode: PropTypes.string, - - ...withLocalizePropTypes, + priorityMode: OnyxEntry>; }; -const defaultProps = { - priorityMode: CONST.PRIORITY_MODE.DEFAULT, -}; +type PriorityModePageProps = PriorityModePageOnyxProps; -function PriorityModePage(props) { +function PriorityModePage({priorityMode}: PriorityModePageProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); - const priorityModes = _.map(_.values(CONST.PRIORITY_MODE), (mode) => ({ + const priorityModes = Object.values(CONST.PRIORITY_MODE).map((mode) => ({ value: mode, - text: props.translate(`priorityModePage.priorityModes.${mode}.label`), - alternateText: props.translate(`priorityModePage.priorityModes.${mode}.description`), + text: translate(`priorityModePage.priorityModes.${mode}.label`), + alternateText: translate(`priorityModePage.priorityModes.${mode}.description`), keyForList: mode, - isSelected: props.priorityMode === mode, + isSelected: priorityMode === mode, })); const updateMode = useCallback( - (mode) => { - if (mode.value === props.priorityMode) { + (mode: (typeof priorityModes)[number]) => { + if (mode.value === priorityMode) { Navigation.goBack(); return; } User.updateChatPriorityMode(mode.value); }, - [props.priorityMode], + [priorityMode], ); return ( @@ -51,28 +48,23 @@ function PriorityModePage(props) { testID={PriorityModePage.displayName} > Navigation.goBack()} /> - {props.translate('priorityModePage.explainerText')} + {translate('priorityModePage.explainerText')} mode.isSelected).keyForList} + initiallyFocusedOptionKey={priorityModes.find((mode) => mode.isSelected)?.keyForList} />
); } PriorityModePage.displayName = 'PriorityModePage'; -PriorityModePage.propTypes = propTypes; -PriorityModePage.defaultProps = defaultProps; -export default compose( - withLocalize, - withOnyx({ - priorityMode: { - key: ONYXKEYS.NVP_PRIORITY_MODE, - }, - }), -)(PriorityModePage); +export default withOnyx({ + priorityMode: { + key: ONYXKEYS.NVP_PRIORITY_MODE, + }, +})(PriorityModePage); diff --git a/src/pages/settings/Preferences/ThemePage.js b/src/pages/settings/Preferences/ThemePage.tsx similarity index 71% rename from src/pages/settings/Preferences/ThemePage.js rename to src/pages/settings/Preferences/ThemePage.tsx index 4907056be761..4d89e600770b 100644 --- a/src/pages/settings/Preferences/ThemePage.js +++ b/src/pages/settings/Preferences/ThemePage.tsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {ValueOf} from 'type-fest'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -13,23 +13,22 @@ import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const propTypes = { +type ThemePageOnyxProps = { /** The theme of the app */ - preferredTheme: PropTypes.string, + preferredTheme: OnyxEntry>; }; -const defaultProps = { - preferredTheme: CONST.THEME.DEFAULT, -}; +type ThemePageProps = ThemePageOnyxProps; -function ThemePage(props) { +function ThemePage({preferredTheme}: ThemePageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const localesToThemes = _.map(_.values(_.omit(CONST.THEME, 'DEFAULT', 'FALLBACK')), (theme) => ({ + const {DEFAULT, FALLBACK, ...themes} = CONST.THEME; + const localesToThemes = Object.values(themes).map((theme) => ({ value: theme, text: translate(`themePage.themes.${theme}.label`), keyForList: theme, - isSelected: (props.preferredTheme || CONST.THEME.DEFAULT) === theme, + isSelected: (preferredTheme ?? CONST.THEME.DEFAULT) === theme, })); return ( @@ -49,17 +48,15 @@ function ThemePage(props) { User.updateTheme(theme.value)} - initiallyFocusedOptionKey={_.find(localesToThemes, (theme) => theme.isSelected).keyForList} + initiallyFocusedOptionKey={localesToThemes.find((theme) => theme.isSelected)?.keyForList} /> ); } ThemePage.displayName = 'ThemePage'; -ThemePage.propTypes = propTypes; -ThemePage.defaultProps = defaultProps; -export default withOnyx({ +export default withOnyx({ preferredTheme: { key: ONYXKEYS.PREFERRED_THEME, }, From 0c056bed123c0eff882e120b692b04428fa5cf40 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 12:59:10 +0100 Subject: [PATCH 031/503] ref: move SignInOrAvatarWithOptionalStatus to TS --- ...js => SignInOrAvatarWithOptionalStatus.tsx} | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) rename src/pages/home/sidebar/{SignInOrAvatarWithOptionalStatus.js => SignInOrAvatarWithOptionalStatus.tsx} (61%) diff --git a/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.js b/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx similarity index 61% rename from src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.js rename to src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx index 0ea6195cd713..2a9356d78232 100644 --- a/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.js +++ b/src/pages/home/sidebar/SignInOrAvatarWithOptionalStatus.tsx @@ -1,6 +1,3 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React from 'react'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import * as Session from '@userActions/Session'; @@ -8,18 +5,13 @@ import AvatarWithOptionalStatus from './AvatarWithOptionalStatus'; import PressableAvatarWithIndicator from './PressableAvatarWithIndicator'; import SignInButton from './SignInButton'; -const propTypes = { - /** Whether the create menu is open or not */ - isCreateMenuOpen: PropTypes.bool, +type SignInOrAvatarWithOptionalStatusProps = { + isCreateMenuOpen?: boolean; }; -const defaultProps = { - isCreateMenuOpen: false, -}; - -function SignInOrAvatarWithOptionalStatus({isCreateMenuOpen}) { +function SignInOrAvatarWithOptionalStatus({isCreateMenuOpen = false}: SignInOrAvatarWithOptionalStatusProps) { const currentUserPersonalDetails = useCurrentUserPersonalDetails(); - const emojiStatus = lodashGet(currentUserPersonalDetails, 'status.emojiCode', ''); + const emojiStatus = currentUserPersonalDetails.status?.emojiCode ?? ''; if (Session.isAnonymousUser()) { return ; @@ -35,7 +27,5 @@ function SignInOrAvatarWithOptionalStatus({isCreateMenuOpen}) { return ; } -SignInOrAvatarWithOptionalStatus.propTypes = propTypes; -SignInOrAvatarWithOptionalStatus.defaultProps = defaultProps; SignInOrAvatarWithOptionalStatus.displayName = 'SignInOrAvatarWithOptionalStatus'; export default SignInOrAvatarWithOptionalStatus; From 6bd5633ec4631b4a1ef76b03c5a7e576d7d15ca8 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 13:04:18 +0100 Subject: [PATCH 032/503] ref: move SignInButton to TS --- src/pages/home/sidebar/{SignInButton.js => SignInButton.tsx} | 1 - 1 file changed, 1 deletion(-) rename src/pages/home/sidebar/{SignInButton.js => SignInButton.tsx} (95%) diff --git a/src/pages/home/sidebar/SignInButton.js b/src/pages/home/sidebar/SignInButton.tsx similarity index 95% rename from src/pages/home/sidebar/SignInButton.js rename to src/pages/home/sidebar/SignInButton.tsx index f89deb6f65b2..1dc65bfd5050 100644 --- a/src/pages/home/sidebar/SignInButton.js +++ b/src/pages/home/sidebar/SignInButton.tsx @@ -1,4 +1,3 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ import React from 'react'; import {View} from 'react-native'; import Button from '@components/Button'; From 5ed0bc2c9fc59b181d3067e88b7c10c36e0ed497 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 13:05:47 +0100 Subject: [PATCH 033/503] ref: removed SidebarNavigationContext --- src/pages/home/sidebar/SidebarNavigationContext.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/pages/home/sidebar/SidebarNavigationContext.js diff --git a/src/pages/home/sidebar/SidebarNavigationContext.js b/src/pages/home/sidebar/SidebarNavigationContext.js deleted file mode 100644 index e69de29bb2d1..000000000000 From 5443274f38ae358c00e9013f8100f7195ebe77e5 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 16:19:53 +0100 Subject: [PATCH 034/503] ref: move SidebarLinksData to TS --- src/libs/ReportUtils.ts | 2 +- src/libs/SidebarUtils.ts | 14 +- ...debarLinksData.js => SidebarLinksData.tsx} | 296 +++++++----------- 3 files changed, 127 insertions(+), 185 deletions(-) rename src/pages/home/sidebar/{SidebarLinksData.js => SidebarLinksData.tsx} (53%) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index b5da21c0f67e..40581f46e76a 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3824,7 +3824,7 @@ function shouldReportBeInOptionList({ report: OnyxEntry; currentReportId: string; isInGSDMode: boolean; - betas: Beta[]; + betas: OnyxEntry; policies: OnyxCollection; excludeEmptyChats: boolean; doesReportHaveViolations: boolean; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 5fe646c5ad13..7b757d7fc5b8 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -79,10 +79,10 @@ let hasInitialReportActions = false; */ function getOrderedReportIDs( currentReportId: string | null, - allReports: Record, - betas: Beta[], - policies: Record, - priorityMode: ValueOf, + allReports: OnyxEntry>, + betas: OnyxEntry, + policies: OnyxEntry>, + priorityMode: OnyxEntry>, allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', @@ -110,7 +110,7 @@ function getOrderedReportIDs( const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; - const allReportsDictValues = Object.values(allReports); + const allReportsDictValues = Object.values(allReports ?? {}); // Filter out all the reports that shouldn't be displayed let reportsToDisplay = allReportsDictValues.filter((report) => { @@ -118,7 +118,7 @@ function getOrderedReportIDs( const parentReportActions = allReportActions?.[parentReportActionsKey]; const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID); const doesReportHaveViolations = - betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); + betas?.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ report, currentReportId: currentReportId ?? '', @@ -126,7 +126,7 @@ function getOrderedReportIDs( betas, policies, excludeEmptyChats: true, - doesReportHaveViolations, + doesReportHaveViolations: !!doesReportHaveViolations, }); }); diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.tsx similarity index 53% rename from src/pages/home/sidebar/SidebarLinksData.js rename to src/pages/home/sidebar/SidebarLinksData.tsx index 3bd538e8beab..b128fcaf33ec 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -1,154 +1,114 @@ +import {useIsFocused} from '@react-navigation/native'; import {deepEqual} from 'fast-equals'; -import lodashGet from 'lodash/get'; -import lodashMap from 'lodash/map'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import withCurrentReportID from '@components/withCurrentReportID'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import withNavigationFocus from '@components/withNavigationFocus'; +import type {EdgeInsets} from 'react-native-safe-area-context'; +import type {ValueOf} from 'type-fest'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; +import useCurrentReportID from '@hooks/useCurrentReportID'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; 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 {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import SidebarLinks, {basePropTypes} from './SidebarLinks'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Message} from '@src/types/onyx/ReportAction'; +import SidebarLinks from './SidebarLinks'; -const propTypes = { - ...basePropTypes, - - /* Onyx Props */ - /** List of reports */ - chatReports: PropTypes.objectOf(reportPropTypes), - - /** All report actions for all reports */ - - /** Object of report actions for this report */ - allReportActions: PropTypes.objectOf( - PropTypes.arrayOf( - PropTypes.shape({ - error: PropTypes.string, - message: PropTypes.arrayOf( - PropTypes.shape({ - moderationDecision: PropTypes.shape({ - decision: PropTypes.string, - }), - }), - ), - }), - ), - ), - - /** Whether the reports are loading. When false it means they are ready to be used. */ - isLoadingApp: PropTypes.bool, - - /** The chat priority mode */ - priorityMode: PropTypes.string, - - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - - network: networkPropTypes.isRequired, - - /** The policies which the user has access to */ - // eslint-disable-next-line react/forbid-prop-types - policies: PropTypes.object, - - // eslint-disable-next-line react/forbid-prop-types - policyMembers: PropTypes.object, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), - /** All of the transaction violations */ - transactionViolations: PropTypes.shape({ - violations: PropTypes.arrayOf( - PropTypes.shape({ - /** The transaction ID */ - transactionID: PropTypes.number, - - /** The transaction violation type */ - type: PropTypes.string, - - /** The transaction violation message */ - message: PropTypes.string, - - /** The transaction violation data */ - data: PropTypes.shape({ - /** The transaction violation data field */ - field: PropTypes.string, - - /** The transaction violation data value */ - value: PropTypes.string, - }), - }), - ), - }), +type SidebarLinksDataOnyxProps = { + chatReports: OnyxEntry< + Pick< + OnyxTypes.Report, + | 'reportID' + | 'participantAccountIDs' + | 'hasDraft' + | 'isPinned' + | 'isHidden' + | 'notificationPreference' + | 'errorFields' + | 'lastMessageText' + | 'lastVisibleActionCreated' + | 'iouReportID' + | 'total' + | 'nonReimbursableTotal' + | 'hasOutstandingChildRequest' + | 'isWaitingOnBankAccount' + | 'statusNum' + | 'stateNum' + | 'chatType' + | 'type' + | 'policyID' + | 'visibility' + | 'lastReadTime' + | 'reportName' + | 'policyName' + | 'oldPolicyName' + | 'ownerAccountID' + | 'currency' + | 'managerID' + | 'parentReportActionID' + | 'parentReportID' + | 'isDeletedParentAction' + > & {isUnreadWithMention: boolean} + >; + isLoadingApp: OnyxEntry; + priorityMode: OnyxEntry>; + betas: OnyxEntry; + allReportActions: OnyxEntry>>; + policies: OnyxEntry>; + policyMembers: OnyxCollection; + transactionViolations: OnyxCollection; }; -const defaultProps = { - chatReports: {}, - allReportActions: {}, - isLoadingApp: true, - priorityMode: CONST.PRIORITY_MODE.DEFAULT, - betas: [], - policies: {}, - policyMembers: {}, - session: { - accountID: '', - }, - transactionViolations: {}, +type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { + onLinkClick: (reportID: number) => void; + insets: EdgeInsets; }; function SidebarLinksData({ - isFocused, allReportActions, betas, chatReports, - currentReportID, insets, - isLoadingApp, + isLoadingApp = true, onLinkClick, policies, - priorityMode, - network, + priorityMode = CONST.PRIORITY_MODE.DEFAULT, policyMembers, - session: {accountID}, + // session: {accountID}, transactionViolations, -}) { +}: SidebarLinksDataProps) { + const {currentReportID} = useCurrentReportID() ?? {}; + const {accountID} = useCurrentUserPersonalDetails(); + const network = useNetwork(); + const isFocused = useIsFocused(); const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); const prevPriorityMode = usePrevious(priorityMode); - const policyMemberAccountIDs = getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, accountID); // eslint-disable-next-line react-hooks/exhaustive-deps - useEffect(() => Policy.openWorkspace(activeWorkspaceID, policyMemberAccountIDs), [activeWorkspaceID]); + useEffect(() => Policy.openWorkspace(activeWorkspaceID ?? '', policyMemberAccountIDs), [activeWorkspaceID]); - const reportIDsRef = useRef(null); + const reportIDsRef = useRef(null); const isLoading = isLoadingApp; const optionListItems = useMemo(() => { const reportIDs = SidebarUtils.getOrderedReportIDs( null, - chatReports, + chatReports as OnyxEntry>, betas, - policies, + policies as OnyxEntry>, priorityMode, - allReportActions, + allReportActions as OnyxEntry>, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -161,7 +121,7 @@ function SidebarLinksData({ // 1. We need to update existing reports only once while loading because they are updated several times during loading and causes this regression: https://github.com/Expensify/App/issues/24596#issuecomment-1681679531 // 2. If the user is offline, we need to update the reports unconditionally, since the loading of report data might be stuck in this case. // 3. Changing priority mode to Most Recent will call OpenApp. If there is an existing reports and the priority mode is updated, we want to immediately update the list instead of waiting the OpenApp request to complete - if (!isLoading || !reportIDsRef.current || network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { + if (!isLoading || !reportIDsRef.current || !!network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { reportIDsRef.current = reportIDs; } return reportIDsRef.current || []; @@ -173,14 +133,14 @@ function SidebarLinksData({ // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. const optionListItemsWithCurrentReport = useMemo(() => { - if (currentReportID && !_.contains(optionListItems, currentReportID)) { + if (currentReportID && !optionListItems?.includes(currentReportID)) { return SidebarUtils.getOrderedReportIDs( currentReportID, - chatReports, + chatReports as OnyxEntry>, betas, - policies, + policies as OnyxEntry>, priorityMode, - allReportActions, + allReportActions as OnyxEntry>, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -191,7 +151,7 @@ function SidebarLinksData({ const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; - const isActiveReport = useCallback((reportID) => currentReportIDRef.current === reportID, []); + const isActiveReport = useCallback((reportID: string) => currentReportIDRef.current === reportID, []); return ( +const chatReportSelector = (report: OnyxEntry) => report && { reportID: report.reportID, participantAccountIDs: report.participantAccountIDs, @@ -233,7 +190,7 @@ const chatReportSelector = (report) => isHidden: report.isHidden, notificationPreference: report.notificationPreference, errorFields: { - addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, + addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, }, lastMessageText: report.lastMessageText, lastVisibleActionCreated: report.lastVisibleActionCreated, @@ -264,78 +221,63 @@ const chatReportSelector = (report) => isUnreadWithMention: ReportUtils.isUnreadWithMention(report), }; -/** - * @param {Object} [reportActions] - * @returns {Object|undefined} - */ -const reportActionsSelector = (reportActions) => +const reportActionsSelector = (reportActions: OnyxEntry) => reportActions && - lodashMap(reportActions, (reportAction) => { - const {reportActionID, parentReportActionID, actionName, errors = []} = reportAction; - const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); + Object.values(reportActions).map((reportAction) => { + const {reportActionID, actionName, errors} = reportAction; + const decision = reportAction.message?.[0].moderationDecision?.decision; return { reportActionID, - parentReportActionID, actionName, errors, message: [ { moderationDecision: {decision}, }, - ], + ] as Message[], }; }); -/** - * @param {Object} [policy] - * @returns {Object|undefined} - */ -const policySelector = (policy) => +const policySelector = (policy: OnyxEntry) => policy && { type: policy.type, name: policy.name, avatar: policy.avatar, }; -export default compose( - withCurrentReportID, - withCurrentUserPersonalDetails, - withNavigationFocus, - withNetwork(), - withOnyx({ - chatReports: { - key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, - initialValue: {}, - }, - isLoadingApp: { - key: ONYXKEYS.IS_LOADING_APP, - }, - priorityMode: { - key: ONYXKEYS.NVP_PRIORITY_MODE, - initialValue: CONST.PRIORITY_MODE.DEFAULT, - }, - betas: { - key: ONYXKEYS.BETAS, - initialValue: [], - }, - allReportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - selector: reportActionsSelector, - initialValue: {}, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector, - initialValue: {}, - }, - policyMembers: { - key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - initialValue: {}, - }, - }), -)(SidebarLinksData); +export default withOnyx({ + chatReports: { + key: ONYXKEYS.COLLECTION.REPORT, + selector: chatReportSelector, + initialValue: {}, + }, + isLoadingApp: { + key: ONYXKEYS.IS_LOADING_APP, + }, + priorityMode: { + key: ONYXKEYS.NVP_PRIORITY_MODE, + initialValue: CONST.PRIORITY_MODE.DEFAULT, + }, + betas: { + key: ONYXKEYS.BETAS, + initialValue: [], + }, + allReportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + selector: reportActionsSelector, + initialValue: {}, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + selector: policySelector, + initialValue: {}, + }, + policyMembers: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + initialValue: {}, + }, +})(SidebarLinksData); From bc5cd4df25c3dbd9e8a07e14a8abc44d61ec32b0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 16:46:36 +0100 Subject: [PATCH 035/503] ref: move SidebarLinks to TS --- src/components/LHNOptionsList/types.ts | 2 +- .../{SidebarLinks.js => SidebarLinks.tsx} | 64 +++++++++---------- src/pages/home/sidebar/SidebarLinksData.tsx | 4 +- 3 files changed, 32 insertions(+), 38 deletions(-) rename src/pages/home/sidebar/{SidebarLinks.js => SidebarLinks.tsx} (78%) diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 58bea97f04c9..f3d6bde9d41c 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -45,7 +45,7 @@ type CustomLHNOptionsListProps = { contentContainerStyles?: StyleProp; /** Sections for the section list */ - data: string[]; + data: string[] | null; /** Callback to fire when a row is selected */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.tsx similarity index 78% rename from src/pages/home/sidebar/SidebarLinks.js rename to src/pages/home/sidebar/SidebarLinks.tsx index 9431bae68d8a..52165c148727 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.tsx @@ -1,10 +1,9 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, StyleSheet, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {EdgeInsets} from 'react-native-safe-area-context'; +import type {ValueOf} from 'type-fest'; import Breadcrumbs from '@components/Breadcrumbs'; import LHNOptionsList from '@components/LHNOptionsList/LHNOptionsList'; import OptionsListSkeletonView from '@components/OptionsListSkeletonView'; @@ -16,37 +15,33 @@ import KeyboardShortcut from '@libs/KeyboardShortcut'; import Navigation from '@libs/Navigation/Navigation'; import onyxSubscribe from '@libs/onyxSubscribe'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; -import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {Modal, Policy, Report} from '@src/types/onyx'; -const basePropTypes = { - /** Toggles the navigation menu open and closed */ - onLinkClick: PropTypes.func.isRequired, - - /** Safe area insets required for mobile devices margins */ - insets: safeAreaInsetPropTypes.isRequired, +type SidebarLinksOnyxProps = { + activePolicy: OnyxEntry; }; -const propTypes = { - ...basePropTypes, - - optionListItems: PropTypes.arrayOf(PropTypes.string).isRequired, - - isLoading: PropTypes.bool.isRequired, - - // eslint-disable-next-line react/require-default-props - priorityMode: PropTypes.oneOf(_.values(CONST.PRIORITY_MODE)), - - isActiveReport: PropTypes.func.isRequired, +type SidebarLinksProps = SidebarLinksOnyxProps & { + onLinkClick: () => void; + insets: EdgeInsets; + optionListItems: string[] | null; + isLoading: OnyxEntry; + priorityMode?: OnyxEntry>; + isActiveReport: (reportID: string) => boolean; + isCreateMenuOpen?: boolean; + + // eslint-disable-next-line react/no-unused-prop-types -- its used in withOnyx + activeWorkspaceID: string | undefined; }; -function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen, activePolicy}) { +function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priorityMode = CONST.PRIORITY_MODE.DEFAULT, isActiveReport, isCreateMenuOpen, activePolicy}: SidebarLinksProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const modal = useRef({}); + const modal = useRef({}); const {translate, updateLocale} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -67,7 +62,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority const unsubscribeOnyxModal = onyxSubscribe({ key: ONYXKEYS.MODAL, callback: (modalArg) => { - if (_.isNull(modalArg) || typeof modalArg !== 'object') { + if (modalArg === null || typeof modalArg !== 'object') { return; } modal.current = modalArg; @@ -105,18 +100,19 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority /** * Show Report page with selected report id - * - * @param {Object} option - * @param {String} option.reportID */ const showReportPage = useCallback( - (option) => { + (option: Report) => { // Prevent opening Report page when clicking LHN row quickly after clicking FAB icon // or when clicking the active LHN row on large screens // or when continuously clicking different LHNs, only apply to small screen // since getTopmostReportId always returns on other devices const reportActionID = Navigation.getTopmostReportActionId(); - if (isCreateMenuOpen || (option.reportID === Navigation.getTopmostReportId() && !reportActionID) || (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID)) { + if ( + !!isCreateMenuOpen || + (option.reportID === Navigation.getTopmostReportId() && !reportActionID) || + (isSmallScreenWidth && isActiveReport(option.reportID) && !reportActionID) + ) { return; } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); @@ -137,7 +133,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority activePolicy ? { type: CONST.BREADCRUMB_TYPE.STRONG, - text: lodashGet(activePolicy, 'name', ''), + text: activePolicy.name ?? '', } : { type: CONST.BREADCRUMB_TYPE.ROOT, @@ -158,7 +154,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority optionMode={viewMode} onFirstItemRendered={App.setSidebarLoaded} /> - {isLoading && optionListItems.length === 0 && ( + {isLoading && optionListItems?.length === 0 && ( @@ -168,12 +164,10 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority ); } -SidebarLinks.propTypes = propTypes; SidebarLinks.displayName = 'SidebarLinks'; -export default withOnyx({ +export default withOnyx({ activePolicy: { key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`, }, })(SidebarLinks); -export {basePropTypes}; diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index b128fcaf33ec..ddd232e99275 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -69,7 +69,7 @@ type SidebarLinksDataOnyxProps = { }; type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { - onLinkClick: (reportID: number) => void; + onLinkClick: () => void; insets: EdgeInsets; }; @@ -101,7 +101,7 @@ function SidebarLinksData({ const reportIDsRef = useRef(null); const isLoading = isLoadingApp; - const optionListItems = useMemo(() => { + const optionListItems: string[] | null = useMemo(() => { const reportIDs = SidebarUtils.getOrderedReportIDs( null, chatReports as OnyxEntry>, From 3fb3715847c5c82958147940cdcb68073456aedf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 16:54:54 +0100 Subject: [PATCH 036/503] ref: move PressableAvatarWithIndicator to TS --- ...or.js => PressableAvatarWithIndicator.tsx} | 52 ++++++------------- 1 file changed, 17 insertions(+), 35 deletions(-) rename src/pages/home/sidebar/{PressableAvatarWithIndicator.js => PressableAvatarWithIndicator.tsx} (56%) diff --git a/src/pages/home/sidebar/PressableAvatarWithIndicator.js b/src/pages/home/sidebar/PressableAvatarWithIndicator.tsx similarity index 56% rename from src/pages/home/sidebar/PressableAvatarWithIndicator.js rename to src/pages/home/sidebar/PressableAvatarWithIndicator.tsx index 63c5936e957b..e07b6e856823 100644 --- a/src/pages/home/sidebar/PressableAvatarWithIndicator.js +++ b/src/pages/home/sidebar/PressableAvatarWithIndicator.tsx @@ -1,44 +1,30 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import AvatarWithIndicator from '@components/AvatarWithIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as UserUtils from '@libs/UserUtils'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -const propTypes = { - /** Whether the create menu is open or not */ - isCreateMenuOpen: PropTypes.bool, - - /** The personal details of the person who is logged in */ - currentUserPersonalDetails: personalDetailsPropType, - +type PressableAvatarWithIndicatorOnyxProps = { /** Indicates whether the app is loading initial data */ - isLoading: PropTypes.bool, + isLoading: OnyxEntry; }; -const defaultProps = { - isCreateMenuOpen: false, - currentUserPersonalDetails: { - pendingFields: {avatar: ''}, - accountID: '', - avatar: '', - }, - isLoading: true, +type PressableAvatarWithIndicatorProps = PressableAvatarWithIndicatorOnyxProps & { + /** Whether the create menu is open or not */ + isCreateMenuOpen: boolean; }; -function PressableAvatarWithIndicator({isCreateMenuOpen, currentUserPersonalDetails, isLoading}) { +function PressableAvatarWithIndicator({isCreateMenuOpen = false, isLoading = true}: PressableAvatarWithIndicatorProps) { const {translate} = useLocalize(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const showSettingsPage = useCallback(() => { if (isCreateMenuOpen) { @@ -55,26 +41,22 @@ function PressableAvatarWithIndicator({isCreateMenuOpen, currentUserPersonalDeta role={CONST.ROLE.BUTTON} onPress={showSettingsPage} > - + ); } -PressableAvatarWithIndicator.propTypes = propTypes; -PressableAvatarWithIndicator.defaultProps = defaultProps; PressableAvatarWithIndicator.displayName = 'PressableAvatarWithIndicator'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ - isLoading: { - key: ONYXKEYS.IS_LOADING_APP, - }, - }), -)(PressableAvatarWithIndicator); + +export default withOnyx({ + isLoading: { + key: ONYXKEYS.IS_LOADING_APP, + }, +})(PressableAvatarWithIndicator); From 5fa814c327d1e9767afa896e3304487885039547 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 16:57:26 +0100 Subject: [PATCH 037/503] ref: move AvatarWithOptionalStatus to TS --- ...alStatus.js => AvatarWithOptionalStatus.tsx} | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) rename src/pages/home/sidebar/{AvatarWithOptionalStatus.js => AvatarWithOptionalStatus.tsx} (81%) diff --git a/src/pages/home/sidebar/AvatarWithOptionalStatus.js b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx similarity index 81% rename from src/pages/home/sidebar/AvatarWithOptionalStatus.js rename to src/pages/home/sidebar/AvatarWithOptionalStatus.tsx index e1ff3982a0cc..5597d46c29bc 100644 --- a/src/pages/home/sidebar/AvatarWithOptionalStatus.js +++ b/src/pages/home/sidebar/AvatarWithOptionalStatus.tsx @@ -1,5 +1,3 @@ -/* eslint-disable rulesdir/onyx-props-must-have-default */ -import PropTypes from 'prop-types'; import React, {useCallback} from 'react'; import {View} from 'react-native'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -12,20 +10,15 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import PressableAvatarWithIndicator from './PressableAvatarWithIndicator'; -const propTypes = { +type AvatarWithOptionalStatusProps = { /** Whether the create menu is open or not */ - isCreateMenuOpen: PropTypes.bool, + isCreateMenuOpen: boolean; /** Emoji status */ - emojiStatus: PropTypes.string, + emojiStatus: string; }; -const defaultProps = { - isCreateMenuOpen: false, - emojiStatus: '', -}; - -function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) { +function AvatarWithOptionalStatus({emojiStatus = '', isCreateMenuOpen = false}: AvatarWithOptionalStatusProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -61,7 +54,5 @@ function AvatarWithOptionalStatus({emojiStatus, isCreateMenuOpen}) { ); } -AvatarWithOptionalStatus.propTypes = propTypes; -AvatarWithOptionalStatus.defaultProps = defaultProps; AvatarWithOptionalStatus.displayName = 'AvatarWithOptionalStatus'; export default AvatarWithOptionalStatus; From 6733581a39b700a147c1708f606cdc6fede9b1ee Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 16 Feb 2024 17:02:16 +0100 Subject: [PATCH 038/503] ref: move BaseSidebarScreen to TS --- src/pages/home/sidebar/SidebarLinks.tsx | 2 +- src/pages/home/sidebar/SidebarLinksData.tsx | 2 +- .../{BaseSidebarScreen.js => BaseSidebarScreen.tsx} | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) rename src/pages/home/sidebar/SidebarScreen/{BaseSidebarScreen.js => BaseSidebarScreen.tsx} (94%) diff --git a/src/pages/home/sidebar/SidebarLinks.tsx b/src/pages/home/sidebar/SidebarLinks.tsx index 52165c148727..f01ed07ea476 100644 --- a/src/pages/home/sidebar/SidebarLinks.tsx +++ b/src/pages/home/sidebar/SidebarLinks.tsx @@ -27,7 +27,7 @@ type SidebarLinksOnyxProps = { type SidebarLinksProps = SidebarLinksOnyxProps & { onLinkClick: () => void; - insets: EdgeInsets; + insets: EdgeInsets | undefined; optionListItems: string[] | null; isLoading: OnyxEntry; priorityMode?: OnyxEntry>; diff --git a/src/pages/home/sidebar/SidebarLinksData.tsx b/src/pages/home/sidebar/SidebarLinksData.tsx index ddd232e99275..ab84f69e824b 100644 --- a/src/pages/home/sidebar/SidebarLinksData.tsx +++ b/src/pages/home/sidebar/SidebarLinksData.tsx @@ -70,7 +70,7 @@ type SidebarLinksDataOnyxProps = { type SidebarLinksDataProps = SidebarLinksDataOnyxProps & { onLinkClick: () => void; - insets: EdgeInsets; + insets: EdgeInsets | undefined; }; function SidebarLinksData({ diff --git a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx similarity index 94% rename from src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js rename to src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx index 9188a859d175..314b3921cc0b 100644 --- a/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.js +++ b/src/pages/home/sidebar/SidebarScreen/BaseSidebarScreen.tsx @@ -17,7 +17,7 @@ const startTimer = () => { Performance.markStart(CONST.TIMING.SWITCH_REPORT); }; -function BaseSidebarScreen(props) { +function BaseSidebarScreen() { const styles = useThemeStyles(); useEffect(() => { Performance.markStart(CONST.TIMING.SIDEBAR_LOADED); @@ -37,7 +37,6 @@ function BaseSidebarScreen(props) { )} From f8080a2a165945814a07599e50989574c0d937e0 Mon Sep 17 00:00:00 2001 From: Rushat Gabhane Date: Fri, 16 Feb 2024 21:57:11 +0530 Subject: [PATCH 039/503] add bottom margin to info box --- docs/_sass/_main.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/_sass/_main.scss b/docs/_sass/_main.scss index ea18acef7c23..ec0f76801bc7 100644 --- a/docs/_sass/_main.scss +++ b/docs/_sass/_main.scss @@ -508,6 +508,7 @@ button { .info { padding: 12px; + margin-bottom: 20px; border-radius: 8px; background-color: $color-highlightBG; color: $color-text; From add3ffa070a77cd3c821ea7a99d628b7d965ad19 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Sun, 18 Feb 2024 00:35:04 +0200 Subject: [PATCH 040/503] converting statuspage to ts --- src/components/Form/FormProvider.tsx | 4 ++-- src/components/TextInput/BaseTextInput/types.ts | 5 ++++- src/pages/settings/Profile/CustomStatus/StatusPage.tsx | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/Form/FormProvider.tsx b/src/components/Form/FormProvider.tsx index 594d07f887d6..1fbc95e74a15 100644 --- a/src/components/Form/FormProvider.tsx +++ b/src/components/Form/FormProvider.tsx @@ -1,5 +1,5 @@ import lodashIsEqual from 'lodash/isEqual'; -import type {ForwardedRef, MutableRefObject, ReactNode} from 'react'; +import type {ForwardedRef, MutableRefObject, ReactNode, RefAttributes} from 'react'; import React, {createRef, forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type {NativeSyntheticEvent, StyleProp, TextInputSubmitEditingEventData, ViewStyle} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; @@ -389,4 +389,4 @@ export default withOnyx({ // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-explicit-any key: (props) => `${props.formID}Draft` as any, }, -})(forwardRef(FormProvider)) as (props: Omit, keyof FormProviderOnyxProps>) => ReactNode; +})(forwardRef(FormProvider)) as (props: Omit & RefAttributes, keyof FormProviderOnyxProps>) => ReactNode; diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index ce0f0e126252..66d29e2705a5 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,4 +1,4 @@ -import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, Role, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; import type {AnimatedTextInputRef} from '@components/RNTextInput'; import type {MaybePhraseKey} from '@libs/Localize'; import type IconAsset from '@src/types/utils/IconAsset'; @@ -108,6 +108,9 @@ type CustomBaseTextInputProps = { /** Type of autocomplete */ autoCompleteType?: string; + + /** Keyboard type */ + role?: Role; }; type BaseTextInputRef = HTMLFormElement | AnimatedTextInputRef; diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx index 61e93324bb16..dc9709126064 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -6,7 +6,7 @@ import type {ValueOf} from 'type-fest'; import EmojiPickerButtonDropdown from '@components/EmojiPicker/EmojiPickerButtonDropdown'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; -import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import type {FormInputErrors, FormOnyxValues, FormRef} from '@components/Form/types'; import HeaderPageLayout from '@components/HeaderPageLayout'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -46,7 +46,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const formRef = useRef(null); + const formRef = useRef(null); const [brickRoadIndicator, setBrickRoadIndicator] = useState | '' | null>(''); const currentUserEmojiCode = currentUserPersonalDetails?.status?.emojiCode ?? ''; const currentUserStatusText = currentUserPersonalDetails?.status?.text ?? ''; @@ -160,6 +160,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) Date: Sun, 18 Feb 2024 01:00:58 +0200 Subject: [PATCH 041/503] migrating pronouns --- .../{PronounsPage.js => PronounsPage.tsx} | 82 +++++++++---------- 1 file changed, 37 insertions(+), 45 deletions(-) rename src/pages/settings/Profile/{PronounsPage.js => PronounsPage.tsx} (57%) diff --git a/src/pages/settings/Profile/PronounsPage.js b/src/pages/settings/Profile/PronounsPage.tsx similarity index 57% rename from src/pages/settings/Profile/PronounsPage.js rename to src/pages/settings/Profile/PronounsPage.tsx index 5bb528373e8f..19578c7dd198 100644 --- a/src/pages/settings/Profile/PronounsPage.js +++ b/src/pages/settings/Profile/PronounsPage.tsx @@ -1,48 +1,46 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import Text from '@components/Text'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetails from '@userActions/PersonalDetails'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -const propTypes = { - ...withCurrentUserPersonalDetailsPropTypes, +type PronounsListType = (typeof CONST.PRONOUNS_LIST)[number]; - /** Indicates whether the app is loading initial data */ - isLoadingApp: PropTypes.bool, +type PronounEntry = { + text: string; + value: string; + keyForList: PronounsListType; + isSelected: boolean; }; -const defaultProps = { - ...withCurrentUserPersonalDetailsDefaultProps, - isLoadingApp: true, +type PronounsPageOnyxProps = { + isLoadingApp: OnyxEntry; }; +type PronounsPageProps = PronounsPageOnyxProps & WithCurrentUserPersonalDetailsProps; -function PronounsPage({currentUserPersonalDetails, isLoadingApp}) { +function PronounsPage({currentUserPersonalDetails, isLoadingApp = true}: PronounsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const currentPronouns = lodashGet(currentUserPersonalDetails, 'pronouns', ''); + const currentPronouns = currentUserPersonalDetails?.pronouns ?? ''; const currentPronounsKey = currentPronouns.substring(CONST.PRONOUNS.PREFIX.length); const [searchValue, setSearchValue] = useState(''); useEffect(() => { - if (isLoadingApp && _.isUndefined(currentUserPersonalDetails.pronouns)) { + if (isLoadingApp && !currentUserPersonalDetails.pronouns) { return; } - const currentPronounsText = _.chain(CONST.PRONOUNS_LIST) - .find((_value) => _value === currentPronounsKey) - .value(); + const currentPronounsText = CONST.PRONOUNS_LIST.find((value) => value === currentPronounsKey); setSearchValue(currentPronounsText ? translate(`pronouns.${currentPronounsText}`) : ''); @@ -50,34 +48,31 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoadingApp]); - const filteredPronounsList = useMemo(() => { - const pronouns = _.chain(CONST.PRONOUNS_LIST) - .map((value) => { - const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${value}`; - const isCurrentPronouns = fullPronounKey === currentPronouns; - - return { - text: translate(`pronouns.${value}`), - value: fullPronounKey, - keyForList: value, - isSelected: isCurrentPronouns, - }; - }) - .sortBy((pronoun) => pronoun.text.toLowerCase()) - .value(); + const filteredPronounsList = useMemo((): PronounEntry[] => { + const pronouns = CONST.PRONOUNS_LIST.map((value) => { + const fullPronounKey = `${CONST.PRONOUNS.PREFIX}${value}`; + const isCurrentPronouns = fullPronounKey === currentPronouns; + + return { + text: translate(`pronouns.${value}`), + value: fullPronounKey, + keyForList: value, + isSelected: isCurrentPronouns, + }; + }).sort((a, b) => a.text.toLowerCase().localeCompare(b.text.toLowerCase())); const trimmedSearch = searchValue.trim(); if (trimmedSearch.length === 0) { return []; } - return _.filter(pronouns, (pronoun) => pronoun.text.toLowerCase().indexOf(trimmedSearch.toLowerCase()) >= 0); + return pronouns.filter((pronoun) => pronoun.text.toLowerCase().indexOf(trimmedSearch.toLowerCase()) >= 0); }, [searchValue, currentPronouns, translate]); - const headerMessage = searchValue.trim() && filteredPronounsList.length === 0 ? translate('common.noResultsFound') : ''; + const headerMessage = searchValue.trim() && filteredPronounsList?.length === 0 ? translate('common.noResultsFound') : ''; - const updatePronouns = (selectedPronouns) => { - PersonalDetails.updatePronouns(selectedPronouns.keyForList === currentPronounsKey ? '' : lodashGet(selectedPronouns, 'value', '')); + const updatePronouns = (selectedPronouns: PronounEntry) => { + PersonalDetails.updatePronouns(selectedPronouns.keyForList === currentPronounsKey ? '' : selectedPronouns?.value ?? ''); }; return ( @@ -85,7 +80,7 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) { includeSafeAreaPaddingBottom={false} testID={PronounsPage.displayName} > - {isLoadingApp && _.isUndefined(currentUserPersonalDetails.pronouns) ? ( + {isLoadingApp && currentUserPersonalDetails.pronouns ? ( ) : ( <> @@ -110,15 +105,12 @@ function PronounsPage({currentUserPersonalDetails, isLoadingApp}) { ); } -PronounsPage.propTypes = propTypes; -PronounsPage.defaultProps = defaultProps; PronounsPage.displayName = 'PronounsPage'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ +export default withCurrentUserPersonalDetails( + withOnyx({ isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, }, - }), -)(PronounsPage); + })(PronounsPage), +); From fa93bf376407e010771f2f0fa8036c6f7f6149f8 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Sun, 18 Feb 2024 01:04:02 +0200 Subject: [PATCH 042/503] minor edit --- src/pages/settings/Profile/CustomStatus/StatusPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx index dc9709126064..ee400fb9a828 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -83,7 +83,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) return; } User.updateCustomStatus({ - text: values.statusText, + text: statusText, emojiCode: !emojiCode && statusText ? initialEmoji : emojiCode, clearAfter: clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER ? clearAfterTime : '', }); From 038be0d072d480a822911d5577b4c77321f8c053 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Sun, 18 Feb 2024 01:11:56 +0200 Subject: [PATCH 043/503] Minor --- src/pages/settings/Profile/LoungeAccessPage.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Profile/LoungeAccessPage.tsx b/src/pages/settings/Profile/LoungeAccessPage.tsx index 81df868c5565..ca3cd32c4398 100644 --- a/src/pages/settings/Profile/LoungeAccessPage.tsx +++ b/src/pages/settings/Profile/LoungeAccessPage.tsx @@ -1,6 +1,6 @@ import React from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {OnyxEntry} from 'react-native-onyx/lib/types'; import IllustratedHeaderPageLayout from '@components/IllustratedHeaderPageLayout'; import LottieAnimations from '@components/LottieAnimations'; import Text from '@components/Text'; @@ -11,7 +11,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type {User} from '@src/types/onyx'; type LoungeAccessPageOnyxProps = { @@ -31,7 +30,7 @@ function LoungeAccessPage({user}: LoungeAccessPageProps) { return ( Navigation.goBack(ROUTES)} + onBackButtonPress={() => Navigation.goBack()} illustration={LottieAnimations.ExpensifyLounge} testID={LoungeAccessPage.displayName} > From 81c69a09d0eaa07b5d03257651ca84138da44589 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Sun, 18 Feb 2024 01:56:28 +0200 Subject: [PATCH 044/503] Minor edit --- src/pages/settings/Profile/CustomStatus/StatusPage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx index ee400fb9a828..d7a8abad470b 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -74,8 +74,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) const navigateBackToPreviousScreen = useCallback(() => Navigation.goBack(), []); const updateStatus = useCallback( - (values: FormOnyxValues) => { - const {emojiCode, statusText} = values; + ({emojiCode, statusText}: FormOnyxValues) => { const clearAfterTime = draftClearAfter ?? currentUserClearAfter ?? CONST.CUSTOM_STATUS_TYPES.NEVER; const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({dateTimeString: clearAfterTime}); if (!isValid && clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER) { From 5f96f129d21a76c25b482bddfb04e0bcd3649ca3 Mon Sep 17 00:00:00 2001 From: Abdelrahman Khattab Date: Sun, 18 Feb 2024 02:04:48 +0200 Subject: [PATCH 045/503] move clearDraftCustomStatus inside runAfterInteractions --- src/pages/settings/Profile/CustomStatus/StatusPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx index d7a8abad470b..7bc440c97ae5 100644 --- a/src/pages/settings/Profile/CustomStatus/StatusPage.tsx +++ b/src/pages/settings/Profile/CustomStatus/StatusPage.tsx @@ -87,8 +87,8 @@ function StatusPage({draftStatus, currentUserPersonalDetails}: StatusPageProps) clearAfter: clearAfterTime !== CONST.CUSTOM_STATUS_TYPES.NEVER ? clearAfterTime : '', }); - User.clearDraftCustomStatus(); InteractionManager.runAfterInteractions(() => { + User.clearDraftCustomStatus(); navigateBackToPreviousScreen(); }); }, From bc076b5e6a38acf273ea46721ecad6c18dc39aa9 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Sun, 18 Feb 2024 23:02:11 +0530 Subject: [PATCH 046/503] Increase editing space for recovery code --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 4afba77b77b5..7cbf05dba783 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -324,7 +324,7 @@ function BaseValidateCodeForm(props) { accessibilityLabel={props.translate('recoveryCodeForm.recoveryCode')} value={recoveryCode} onChangeText={(text) => onTextInput(text, 'recoveryCode')} - maxLength={CONST.RECOVERY_CODE_LENGTH} + maxLength={CONST.FORM_CHARACTER_LIMIT} label={props.translate('recoveryCodeForm.recoveryCode')} errorText={formError.recoveryCode || ''} hasError={hasError} From 04fb21000447ec284980392bd55ff7fade561a9a Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Sun, 18 Feb 2024 23:16:23 +0530 Subject: [PATCH 047/503] Add logic to trim text on input --- src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js index 7cbf05dba783..2bc90efaceb5 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js @@ -179,7 +179,7 @@ function BaseValidateCodeForm(props) { setInput = setRecoveryCode; } - setInput(text); + setInput(text.trim()); setFormError((prevError) => ({...prevError, [key]: ''})); if (props.account.errors) { From ee483b5dcb3c9bf592a9f0abb7157a2c6319cc22 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 19 Feb 2024 18:24:37 +0700 Subject: [PATCH 048/503] fix: refactor --- .../ReportActionItem/ReportPreview.tsx | 11 ++++-- src/libs/ReportUtils.ts | 35 ++++++++++++------- src/pages/home/report/ReportActionItem.js | 13 ++----- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/components/ReportActionItem/ReportPreview.tsx b/src/components/ReportActionItem/ReportPreview.tsx index dd7018c50b9b..6e1dfccd52ab 100644 --- a/src/components/ReportActionItem/ReportPreview.tsx +++ b/src/components/ReportActionItem/ReportPreview.tsx @@ -30,7 +30,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Policy, Report, ReportAction, Session, Transaction, TransactionViolations} from '@src/types/onyx'; +import type {Policy, Report, ReportAction, Session, Transaction, TransactionViolations, UserWallet} from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import ReportActionItemImages from './ReportActionItemImages'; @@ -52,6 +52,9 @@ type ReportPreviewOnyxProps = { /** All of the transaction violations */ transactionViolations: OnyxCollection; + + /** The user's wallet account */ + userWallet: OnyxEntry; }; type ReportPreviewProps = ReportPreviewOnyxProps & { @@ -99,6 +102,7 @@ function ReportPreview({ isHovered = false, isWhisper = false, checkIfContextMenuActive = () => {}, + userWallet, }: ReportPreviewProps) { const theme = useTheme(); const styles = useThemeStyles(); @@ -231,7 +235,7 @@ function ReportPreview({ }, [isPaidGroupPolicy, isCurrentUserManager, isDraftExpenseReport, isApproved, isOnInstantSubmitPolicy, isOnSubmitAndClosePolicy, iouSettled]); const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton; - const shouldPromptUserToAddBankAccount = ReportUtils.hasAddBankAccountAction(iouReportID); + const shouldPromptUserToAddBankAccount = ReportUtils.hasMissingPaymentMethod(userWallet, iouReportID); const shouldShowRBR = !iouSettled && hasErrors; /* @@ -371,4 +375,7 @@ export default withOnyx({ transactionViolations: { key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, }, + userWallet: { + key: ONYXKEYS.USER_WALLET, + }, })(ReportPreview); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 91dbee035516..ae7edf773938 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -28,6 +28,7 @@ import type { Session, Transaction, TransactionViolation, + UserWallet, } from '@src/types/onyx'; import type {Participant} from '@src/types/onyx/IOU'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; @@ -421,6 +422,8 @@ type AncestorIDs = { reportActionsIDs: string[]; }; +type MissingPaymentMethod = 'bankAccount' | 'wallet'; + let currentUserEmail: string | undefined; let currentUserAccountID: number | undefined; let isAnonymousUser = false; @@ -5009,20 +5012,27 @@ function canBeAutoReimbursed(report: OnyxEntry, policy: OnyxEntry, reportId: string, reportAction: ReportAction): MissingPaymentMethod | undefined { + const isSubmitterOfUnsettledReport = isCurrentUserSubmitter(reportId) && !isSettled(reportId); + if (!isSubmitterOfUnsettledReport || reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { + return undefined; + } + const paymentType = reportAction.originalMessage?.paymentType; + if (paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY) { + return isEmpty(userWallet) || userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER ? 'wallet' : undefined; } - return !!Object.values(reportActions).find( - (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED && action.originalMessage?.paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY, - ); + return !store.hasCreditBankAccount() ? 'bankAccount' : undefined; +} + +/** + * Checks if report chat contains add bank account action + */ +function hasMissingPaymentMethod(userWallet: OnyxEntry, iouReportID: string): boolean { + const reportActions = ReportActionsUtils.getAllReportActions(iouReportID); + return Object.values(reportActions).some((action) => getIndicatedMissingPaymentMethod(userWallet, iouReportID, action) !== undefined); } export { @@ -5216,7 +5226,7 @@ export { isValidReport, getReportDescriptionText, isReportFieldOfTypeTitle, - hasAddBankAccountAction, + hasMissingPaymentMethod, isIOUReportUsingReport, hasUpdatedTotal, isReportFieldDisabled, @@ -5225,6 +5235,7 @@ export { getAllAncestorReportActionIDs, canEditPolicyDescription, getPolicyDescriptionText, + getIndicatedMissingPaymentMethod, }; export type { diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 39a5fcaa4ee0..d8232150b6a1 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -54,7 +54,6 @@ import {ReactionListContext} from '@pages/home/ReportScreenContext'; import reportPropTypes from '@pages/reportPropTypes'; import * as BankAccounts from '@userActions/BankAccounts'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; -import * as store from '@userActions/ReimbursementAccount/store'; import * as Report from '@userActions/Report'; import * as ReportActions from '@userActions/ReportActions'; import * as Session from '@userActions/Session'; @@ -416,19 +415,13 @@ function ReportActionItem(props) { const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); - const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); - const shouldShowAddCreditBankAccountButton = isSubmitterOfUnsettledReport && !store.hasCreditBankAccount() && paymentType !== CONST.IOU.PAYMENT_TYPE.EXPENSIFY; - const shouldShowEnableWalletButton = - isSubmitterOfUnsettledReport && - (_.isEmpty(props.userWallet) || props.userWallet.tierName === CONST.WALLET.TIER_NAME.SILVER) && - paymentType === CONST.IOU.PAYMENT_TYPE.EXPENSIFY; - + const missingPaymentMethod = ReportUtils.getIndicatedMissingPaymentMethod(props.userWallet, props.report.reportID, props.action); children = ( <> - {shouldShowAddCreditBankAccountButton && ( + {missingPaymentMethod === 'bankAccount' && (