From dded6349ee50d371ed5c920a4c3334700ad1d537 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 19 Dec 2023 21:55:13 +0530 Subject: [PATCH 001/152] test map when image is not loaded in request view --- .../ReportActionItem/MoneyRequestPreview.js | 10 +++++++++- src/components/ReportActionItem/MoneyRequestView.js | 13 ++++++++++++- src/libs/actions/IOU.js | 3 +++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 12c6d0629370..1d798130dfd5 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -38,6 +38,7 @@ import CONST from '@src/CONST'; import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; import ReportActionItemImages from './ReportActionItemImages'; +import ConfirmedRoute from '@components/ConfirmedRoute'; const propTypes = { /** The active IOUReport, used for Onyx subscription */ @@ -169,6 +170,8 @@ function MoneyRequestPreview(props) { const hasPendingWaypoints = lodashGet(props.transaction, 'pendingFields.waypoints', null); + const showMapAsImage = isDistanceRequest && hasPendingWaypoints; + const getSettledMessage = () => { if (isExpensifyCardTransaction) { return translate('common.done'); @@ -257,7 +260,12 @@ function MoneyRequestPreview(props) { !props.onPreviewPressed ? [styles.moneyRequestPreviewBox, ...props.containerStyles] : {}, ]} > - {hasReceipt && ( + {showMapAsImage && ( + + + + )} + {!showMapAsImage && hasReceipt && ( - {hasReceipt && ( + {showMapAsImage && ( + + + + + + )} + {!showMapAsImage && hasReceipt && ( Date: Tue, 2 Jan 2024 21:31:37 +0100 Subject: [PATCH 002/152] fix: adding backTo param to ReportParticipantsPage.js --- src/pages/ReportParticipantsPage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index e04ffbb352fc..1e84bd3dcbbb 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -121,7 +121,12 @@ function ReportParticipantsPage(props) { }, ]} onSelectRow={(option) => { - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID)); + Navigation.navigate( + ROUTES.PROFILE.getRoute( + option.accountID, + ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID) + ) + ); }} hideSectionHeaders showTitleTooltip From ef26aaa1d61d8b25ae43ed2deb9cbc0c589d74b5 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 9 Jan 2024 17:49:16 +0530 Subject: [PATCH 003/152] test changes --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- src/components/ReportActionItem/MoneyRequestView.js | 2 +- src/libs/TransactionUtils.ts | 7 +++++-- src/libs/actions/IOU.js | 4 +++- src/types/onyx/Transaction.ts | 6 ++++-- 5 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 0663441c3834..9bba3aed205a 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -38,8 +38,8 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; -import ReportActionItemImages from './ReportActionItemImages'; import ConfirmedRoute from '@components/ConfirmedRoute'; +import ReportActionItemImages from './ReportActionItemImages'; const propTypes = { /** The active IOUReport, used for Onyx subscription */ diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index eb658a7f8a1b..fc70b47d7101 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -42,8 +42,8 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import ReportActionItemImage from './ReportActionItemImage'; import ConfirmedRoute from '@components/ConfirmedRoute'; +import ReportActionItemImage from './ReportActionItemImage'; const violationNames = lodashValues(CONST.VIOLATIONS); diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index c34a6753c1d5..b8fad803d957 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -1,5 +1,6 @@ import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; @@ -7,8 +8,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types/onyx'; import type {PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type PolicyTaxRate from '@src/types/onyx/PolicyTaxRates'; -import type {Comment, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import type {Comment, PendingFieldsCollection, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; +import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; @@ -98,6 +99,7 @@ function buildOptimisticTransaction( category = '', tag = '', billable = false, + pendingFields: PendingFieldsCollection | null = null, ): Transaction { // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) @@ -112,6 +114,7 @@ function buildOptimisticTransaction( } return { + ...(isNotEmptyObject(pendingFields) ? {pendingFields} : {}), transactionID, amount, currency, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 4e1a45101732..f4ff83e995c8 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -721,6 +721,8 @@ function getMoneyRequestInformation( receiptObject.state = receipt.state || CONST.IOU.RECEIPT_STATE.SCANREADY; filename = receipt.name; } + const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( ReportUtils.isExpenseReport(iouReport) ? -amount : amount, currency, @@ -736,6 +738,7 @@ function getMoneyRequestInformation( category, tag, billable, + isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : null, ); const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); @@ -747,7 +750,6 @@ function getMoneyRequestInformation( // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 // to remind me to do this. - const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; if (existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { optimisticTransaction = OnyxUtils.fastMerge(existingTransaction, optimisticTransaction); // pendingFields: { diff --git a/src/types/onyx/Transaction.ts b/src/types/onyx/Transaction.ts index 8b7e26280305..a6b65f7cba38 100644 --- a/src/types/onyx/Transaction.ts +++ b/src/types/onyx/Transaction.ts @@ -49,6 +49,8 @@ type Route = { type Routes = Record; +type PendingFieldsCollection = Partial<{[K in keyof Transaction | keyof Comment]: ValueOf}>; + type Transaction = { amount: number; billable: boolean; @@ -76,7 +78,7 @@ type Transaction = { routes?: Routes; transactionID: string; tag: string; - pendingFields?: Partial<{[K in keyof Transaction | keyof Comment]: ValueOf}>; + pendingFields?: PendingFieldsCollection; /** Card Transactions */ @@ -97,4 +99,4 @@ type Transaction = { }; export default Transaction; -export type {WaypointCollection, Comment, Receipt, Waypoint}; +export type {WaypointCollection, Comment, Receipt, Waypoint, PendingFieldsCollection}; From 54a63d643b9ad602ace915090c3c18cae28af5b0 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 9 Jan 2024 18:07:40 +0530 Subject: [PATCH 004/152] fix lint --- src/components/ReportActionItem/MoneyRequestPreview.js | 2 +- src/components/ReportActionItem/MoneyRequestView.js | 2 +- src/libs/TransactionUtils.ts | 2 +- src/libs/actions/IOU.js | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 9bba3aed205a..84ed317be294 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -5,6 +5,7 @@ import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import ConfirmedRoute from '@components/ConfirmedRoute'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestSkeletonView from '@components/MoneyRequestSkeletonView'; @@ -38,7 +39,6 @@ import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import * as Localize from '@src/libs/Localize'; import ONYXKEYS from '@src/ONYXKEYS'; -import ConfirmedRoute from '@components/ConfirmedRoute'; import ReportActionItemImages from './ReportActionItemImages'; const propTypes = { diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index fc70b47d7101..04b8d35e3448 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -5,6 +5,7 @@ import React, {useCallback} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import categoryPropTypes from '@components/categoryPropTypes'; +import ConfirmedRoute from '@components/ConfirmedRoute'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -42,7 +43,6 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import ConfirmedRoute from '@components/ConfirmedRoute'; import ReportActionItemImage from './ReportActionItemImage'; const violationNames = lodashValues(CONST.VIOLATIONS); diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index b8fad803d957..4155fe426f5a 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -1,6 +1,5 @@ import lodashHas from 'lodash/has'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; @@ -9,6 +8,7 @@ import type {RecentWaypoint, Report, ReportAction, Transaction} from '@src/types import type {PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type PolicyTaxRate from '@src/types/onyx/PolicyTaxRates'; import type {Comment, PendingFieldsCollection, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f4ff83e995c8..f004b44feca2 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -722,7 +722,7 @@ function getMoneyRequestInformation( filename = receipt.name; } const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; - const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( ReportUtils.isExpenseReport(iouReport) ? -amount : amount, currency, @@ -752,9 +752,9 @@ function getMoneyRequestInformation( // to remind me to do this. if (existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { optimisticTransaction = OnyxUtils.fastMerge(existingTransaction, optimisticTransaction); -// pendingFields: { -// waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, -// } + // pendingFields: { + // waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + // } } // STEP 4: Build optimistic reportActions. We need: From 46141e379c495127f8287f3ca15a771c4eba63df Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 9 Jan 2024 18:09:37 +0530 Subject: [PATCH 005/152] fix lint --- src/libs/actions/IOU.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index f004b44feca2..d68b33e6b5ef 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -750,11 +750,8 @@ function getMoneyRequestInformation( // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 // to remind me to do this. - if (existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { + if (isDistanceRequest) { optimisticTransaction = OnyxUtils.fastMerge(existingTransaction, optimisticTransaction); - // pendingFields: { - // waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - // } } // STEP 4: Build optimistic reportActions. We need: From b7045ec7091f7aba9d94939ea654c3ba5cc59b06 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 12 Jan 2024 16:05:21 -0500 Subject: [PATCH 006/152] fix: adding backTo param to ReportParticipantsPage.js --- src/pages/ReportParticipantsPage.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 7dbc1c7036c4..9e480d2f0516 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -121,7 +121,12 @@ function ReportParticipantsPage(props) { }, ]} onSelectRow={(option) => { - Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID)); + Navigation.navigate( + ROUTES.PROFILE.getRoute( + option.accountID, + ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID) + ) + ); }} hideSectionHeaders showTitleTooltip From e7b85131efe677ac38f177262f3c51b512643b82 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sat, 13 Jan 2024 04:50:32 +0100 Subject: [PATCH 007/152] fmt: prettier --- src/pages/ReportParticipantsPage.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 9e480d2f0516..3bc2783db981 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -121,12 +121,7 @@ function ReportParticipantsPage(props) { }, ]} onSelectRow={(option) => { - Navigation.navigate( - ROUTES.PROFILE.getRoute( - option.accountID, - ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID) - ) - ); + Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID))); }} hideSectionHeaders showTitleTooltip From d1b5b355b1986240abe3e76c60977324efc24a1a Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Thu, 18 Jan 2024 23:55:12 +0100 Subject: [PATCH 008/152] fix: added onBackButtonPress prop --- src/pages/ReportParticipantsPage.js | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 9e480d2f0516..80253c1f1793 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -99,6 +99,7 @@ function ReportParticipantsPage(props) { {({safeAreaPaddingBottomStyle}) => ( Navigation.goBack(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID))} title={props.translate( ReportUtils.isChatRoom(props.report) || ReportUtils.isPolicyExpenseChat(props.report) || @@ -121,12 +122,7 @@ function ReportParticipantsPage(props) { }, ]} onSelectRow={(option) => { - Navigation.navigate( - ROUTES.PROFILE.getRoute( - option.accountID, - ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID) - ) - ); + Navigation.navigate(ROUTES.PROFILE.getRoute(option.accountID, ROUTES.REPORT_PARTICIPANTS.getRoute(props.report.reportID))); }} hideSectionHeaders showTitleTooltip From be5367fc5e9078c60afeefa7477889ada34a6838 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:13:45 +0530 Subject: [PATCH 009/152] ts fixes --- src/libs/TransactionUtils.ts | 6 +++--- tests/ui/UnreadIndicatorsTest.js | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 2e8207db860a..813bb128cb84 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -9,7 +9,7 @@ import type {PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; import type PolicyTaxRate from '@src/types/onyx/PolicyTaxRates'; import type {Comment, PendingFieldsCollection, Receipt, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; -import {isNotEmptyObject} from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; @@ -100,7 +100,7 @@ function buildOptimisticTransaction( category = '', tag = '', billable = false, - pendingFields: PendingFieldsCollection | null = null, + pendingFields: PendingFieldsCollection | undefined = undefined, ): Transaction { // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) @@ -115,7 +115,7 @@ function buildOptimisticTransaction( } return { - ...(isNotEmptyObject(pendingFields) ? {pendingFields} : {}), + ...(!isEmptyObject(pendingFields) ? {pendingFields} : {}), transactionID, amount, currency, diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index e4d4d877f66b..88576f3dc89b 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -24,12 +24,16 @@ import appSetup from '../../src/setup'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; +import PendingMapView from '../../src/components/MapView/PendingMapView'; // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); +jest.mock('../../src/components/ConfirmedRoute.tsx', () => ( + +)); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From e255ec88eb5f77c8e5b0a77a05ec4693afd66199 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:25:05 +0530 Subject: [PATCH 010/152] test fix --- tests/ui/UnreadIndicatorsTest.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 88576f3dc89b..01a3d735104b 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -24,16 +24,13 @@ import appSetup from '../../src/setup'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct'; -import PendingMapView from '../../src/components/MapView/PendingMapView'; // We need a large timeout here as we are lazy loading React Navigation screens and this test is running against the entire mounted App jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); -jest.mock('../../src/components/ConfirmedRoute.tsx', () => ( - -)); +jest.mock('../../src/components/ConfirmedRoute.tsx'); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From 17cb86a947002e25768b4cd2db2eac268cb4a569 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:29:58 +0530 Subject: [PATCH 011/152] test fix attempt 2 --- tests/ui/UnreadIndicatorsTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 01a3d735104b..2241711497ff 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -30,7 +30,7 @@ jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); -jest.mock('../../src/components/ConfirmedRoute.tsx'); +jest.mock('../../src/components/ConfirmedRoute.tsx', (props) => props.children); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From c7165ddb967bf655bfb8843d50d051bbd5b72e53 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:45:19 +0530 Subject: [PATCH 012/152] test fix attempt 3 --- tests/ui/UnreadIndicatorsTest.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 2241711497ff..363c899d4328 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -30,7 +30,10 @@ jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); -jest.mock('../../src/components/ConfirmedRoute.tsx', (props) => props.children); +jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { + const Comp = (props) => props.children; + return Comp; +}); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From b0068d381f1e212f131fe04a7ca3107a5bf2b274 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 20:50:55 +0530 Subject: [PATCH 013/152] perf-test fix --- tests/perf-test/ReportActionsList.perf-test.js | 5 +++++ tests/perf-test/ReportScreen.perf-test.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tests/perf-test/ReportActionsList.perf-test.js b/tests/perf-test/ReportActionsList.perf-test.js index 8e3312cfa4c7..515447a7a2ed 100644 --- a/tests/perf-test/ReportActionsList.perf-test.js +++ b/tests/perf-test/ReportActionsList.perf-test.js @@ -44,6 +44,11 @@ jest.mock('@react-navigation/native', () => { }; }); +jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { + const Comp = (props) => props.children; + return Comp; +}); + beforeAll(() => Onyx.init({ keys: ONYXKEYS, diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index d58f71fa7ab4..97ea04ff55fb 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -29,6 +29,11 @@ jest.mock('react-native-reanimated', () => ({ useAnimatedRef: jest.fn, })); +jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { + const Comp = (props) => props.children; + return Comp; +}); + jest.mock('../../src/components/withNavigationFocus', () => (Component) => { function WithNavigationFocus(props) { return ( From 365d37967684c85c0c059744637748346c4f82a6 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 23:06:12 +0530 Subject: [PATCH 014/152] perf-test fix attempt 2 --- src/components/__mocks__/ConfirmedRoute.tsx | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/components/__mocks__/ConfirmedRoute.tsx diff --git a/src/components/__mocks__/ConfirmedRoute.tsx b/src/components/__mocks__/ConfirmedRoute.tsx new file mode 100644 index 000000000000..a759a2c1e193 --- /dev/null +++ b/src/components/__mocks__/ConfirmedRoute.tsx @@ -0,0 +1,8 @@ +import {View} from "react-native"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any +function ConfirmedRoute(props: any){ + return +} + +export default ConfirmedRoute; \ No newline at end of file From fb5a70ef1a038cffa40e8c001c020b489f04c5e3 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Mon, 22 Jan 2024 23:30:26 +0530 Subject: [PATCH 015/152] perf-test fix attempt 3 --- jest.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jest.config.js b/jest.config.js index de7ed4b1f974..b5335f07482f 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,7 @@ module.exports = { `/?(*.)+(spec|test).${testFileExtension}`, ], transform: { - '^.+\\.jsx?$': 'babel-jest', + '^.+\\.[jt]sx?$': 'babel-jest', '^.+\\.svg?$': 'jest-transformer-svg', }, transformIgnorePatterns: ['/node_modules/(?!react-native)/'], From adb33f65224f4c82df7a375fdb0628e71a768010 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 23 Jan 2024 07:25:37 +0530 Subject: [PATCH 016/152] perf-test fix attempt 4 --- tests/perf-test/ReportActionsList.perf-test.js | 5 +---- tests/perf-test/ReportScreen.perf-test.js | 5 +---- tests/ui/UnreadIndicatorsTest.js | 5 +---- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/perf-test/ReportActionsList.perf-test.js b/tests/perf-test/ReportActionsList.perf-test.js index 515447a7a2ed..c81c4aa51df8 100644 --- a/tests/perf-test/ReportActionsList.perf-test.js +++ b/tests/perf-test/ReportActionsList.perf-test.js @@ -44,10 +44,7 @@ jest.mock('@react-navigation/native', () => { }; }); -jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { - const Comp = (props) => props.children; - return Comp; -}); +jest.mock('../../src/components/ConfirmedRoute.tsx'); beforeAll(() => Onyx.init({ diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index 97ea04ff55fb..faa72fd3a367 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -29,10 +29,7 @@ jest.mock('react-native-reanimated', () => ({ useAnimatedRef: jest.fn, })); -jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { - const Comp = (props) => props.children; - return Comp; -}); +jest.mock('../../src/components/ConfirmedRoute.tsx'); jest.mock('../../src/components/withNavigationFocus', () => (Component) => { function WithNavigationFocus(props) { diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 363c899d4328..01a3d735104b 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -30,10 +30,7 @@ jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); -jest.doMock('../../src/components/ConfirmedRoute.tsx', () => { - const Comp = (props) => props.children; - return Comp; -}); +jest.mock('../../src/components/ConfirmedRoute.tsx'); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ From 94c115adec14ae658d098d2d4e0a794e7da411fd Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 23 Jan 2024 08:16:55 +0530 Subject: [PATCH 017/152] fix lint --- src/components/__mocks__/ConfirmedRoute.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/__mocks__/ConfirmedRoute.tsx b/src/components/__mocks__/ConfirmedRoute.tsx index a759a2c1e193..3c78e764ebea 100644 --- a/src/components/__mocks__/ConfirmedRoute.tsx +++ b/src/components/__mocks__/ConfirmedRoute.tsx @@ -1,8 +1,8 @@ -import {View} from "react-native"; +import {View} from 'react-native'; // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any -function ConfirmedRoute(props: any){ - return +function ConfirmedRoute(props: any) { + return ; } -export default ConfirmedRoute; \ No newline at end of file +export default ConfirmedRoute; From 16bd5cdba5e545a808601004be02b71cb7c94104 Mon Sep 17 00:00:00 2001 From: MrRefactor Date: Mon, 29 Jan 2024 16:40:22 +0100 Subject: [PATCH 018/152] Fix always scrollable suggestion list --- .../BaseAutoCompleteSuggestions.tsx | 7 ++++++- src/styles/utils/index.ts | 16 +++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 5da9c6981603..72ef6ef2f061 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -10,6 +10,8 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as Browser from '@libs/Browser'; +import getPlatform from '@libs/getPlatform'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import viewForwardedRef from '@src/types/utils/viewForwardedRef'; @@ -47,6 +49,9 @@ function BaseAutoCompleteSuggestions( const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); const scrollRef = useRef>(null); + const platform = getPlatform(); + const isMobileSafari = Browser.isMobileSafari(); + /** * Render a suggestion menu item component. */ @@ -67,7 +72,7 @@ function BaseAutoCompleteSuggestions( ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; - const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); + const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value, platform, isMobileSafari)); const estimatedListSize = useMemo( () => ({ height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length, diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 8b040dd8d72c..30b1245ebb5a 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -4,6 +4,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {EdgeInsets} from 'react-native-safe-area-context'; import type {ValueOf} from 'type-fest'; import * as Browser from '@libs/Browser'; +import type Platform from '@libs/getPlatform/types'; import * as UserUtils from '@libs/UserUtils'; // eslint-disable-next-line no-restricted-imports import {defaultTheme} from '@styles/theme'; @@ -792,17 +793,26 @@ function getBaseAutoCompleteSuggestionContainerStyle({left, bottom, width}: GetB /** * Gets the correct position for auto complete suggestion container */ -function getAutoCompleteSuggestionContainerStyle(itemsHeight: number): ViewStyle { +function getAutoCompleteSuggestionContainerStyle(itemsHeight: number, platform: Platform, isMobileSafari: boolean): ViewStyle { 'worklet'; + // This if condition is reverting the workaround for broken scroll on all platforms but native android and iOS safari, where the issue with + // scrolling char behind suggestion list is occuring. Rewerting the fix resolves the issue with always scrollable list on other platforms. const borderWidth = 2; - const height = itemsHeight + 2 * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING; + let height = itemsHeight + 2 * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING; + let top = -(height + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING); + + if (platform === 'android' || isMobileSafari) { + top += borderWidth; + } else { + height += borderWidth; + } // The suggester is positioned absolutely within the component that includes the input and RecipientLocalTime view (for non-expanded mode only). To position it correctly, // we need to shift it by the suggester's height plus its padding and, if applicable, the height of the RecipientLocalTime view. return { overflow: 'hidden', - top: -(height + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + borderWidth), + top, height, minHeight: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, }; From dd639d421c6a7db84a69fe02858e3acb8a502cb4 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 30 Jan 2024 01:20:15 +0700 Subject: [PATCH 019/152] fix Inconsistent validation error message for required field in home address --- src/components/CountrySelector.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx index 589530cd7879..398809d79a2c 100644 --- a/src/components/CountrySelector.tsx +++ b/src/components/CountrySelector.tsx @@ -1,4 +1,5 @@ -import React, {forwardRef, useEffect} from 'react'; +import {useIsFocused} from '@react-navigation/native'; +import React, {forwardRef, useEffect, useRef} from 'react'; import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; @@ -24,13 +25,22 @@ type CountrySelectorProps = { inputID: string; }; -function CountrySelector({errorText = '', value: countryCode, onInputChange}: CountrySelectorProps, ref: ForwardedRef) { +function CountrySelector({errorText = '', value: countryCode, onInputChange, ...rest}: CountrySelectorProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); const title = countryCode ? translate(`allCountries.${countryCode}`) : ''; const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null; + const didOpenContrySelector = useRef(false); + const isFocus = useIsFocused(); + useEffect(() => { + if (isFocus && didOpenContrySelector.current) { + didOpenContrySelector.current = false; + rest.onBlur && rest.onBlur(); + } + }, [isFocus, rest]); + useEffect(() => { // This will cause the form to revalidate and remove any error related to country name onInputChange(countryCode); @@ -47,6 +57,8 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange}: Co description={translate('common.country')} onPress={() => { const activeRoute = Navigation.getActiveRouteWithoutParams(); + rest.onPress && rest.onPress(); + didOpenContrySelector.current = true; Navigation.navigate(ROUTES.SETTINGS_PERSONAL_DETAILS_ADDRESS_COUNTRY.getRoute(countryCode ?? '', activeRoute)); }} /> From e47f246fe44d188a2238dd8e295707e2404bbe78 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 30 Jan 2024 15:11:59 +0100 Subject: [PATCH 020/152] migrate BaseValidateCodeForm to TypeScript --- src/components/MagicCodeInput.tsx | 1 + src/components/withToggleVisibilityView.tsx | 6 +- src/pages/signin/ChangeExpensifyLoginLink.js | 62 ----- src/pages/signin/ChangeExpensifyLoginLink.tsx | 45 ++++ ...teCodeForm.js => BaseValidateCodeForm.tsx} | 251 ++++++++---------- 5 files changed, 159 insertions(+), 206 deletions(-) delete mode 100755 src/pages/signin/ChangeExpensifyLoginLink.js create mode 100755 src/pages/signin/ChangeExpensifyLoginLink.tsx rename src/pages/signin/ValidateCodeForm/{BaseValidateCodeForm.js => BaseValidateCodeForm.tsx} (58%) diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 4a6d87b48e38..67b2756b72d8 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -428,3 +428,4 @@ function MagicCodeInput( MagicCodeInput.displayName = 'MagicCodeInput'; export default forwardRef(MagicCodeInput); +export type {MagicCodeInputHandle}; diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx index 86513f1bd0dc..9da862ecdebe 100644 --- a/src/components/withToggleVisibilityView.tsx +++ b/src/components/withToggleVisibilityView.tsx @@ -5,12 +5,12 @@ import type {SetOptional} from 'type-fest'; import useThemeStyles from '@hooks/useThemeStyles'; import getComponentDisplayName from '@libs/getComponentDisplayName'; -type ToggleVisibilityViewProps = { +type WithToggleVisibilityViewProps = { /** Whether the content is visible. */ isVisible: boolean; }; -export default function withToggleVisibilityView( +export default function withToggleVisibilityView( WrappedComponent: ComponentType>, ): (props: TProps & RefAttributes) => ReactElement | null { function WithToggleVisibilityView({isVisible = false, ...rest}: SetOptional, ref: ForwardedRef) { @@ -30,3 +30,5 @@ export default function withToggleVisibilityView - {!_.isEmpty(props.credentials.login) && {props.translate('loginForm.notYou', {user: props.formatPhoneNumber(props.credentials.login)})}} - - - {props.translate('common.goBack')} - {'.'} - - - - ); -} - -ChangeExpensifyLoginLink.propTypes = propTypes; -ChangeExpensifyLoginLink.defaultProps = defaultProps; -ChangeExpensifyLoginLink.displayName = 'ChangeExpensifyLoginLink'; - -export default compose( - withLocalize, - withOnyx({ - credentials: {key: ONYXKEYS.CREDENTIALS}, - }), -)(ChangeExpensifyLoginLink); diff --git a/src/pages/signin/ChangeExpensifyLoginLink.tsx b/src/pages/signin/ChangeExpensifyLoginLink.tsx new file mode 100755 index 000000000000..d5e8e21b2f4c --- /dev/null +++ b/src/pages/signin/ChangeExpensifyLoginLink.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Credentials} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type ChangeExpensifyLoginLinkOnyxProps = { + /** The credentials of the person logging in */ + credentials: OnyxEntry; +}; + +type ChangeExpensifyLoginLinkProps = ChangeExpensifyLoginLinkOnyxProps & { + onPress: () => void; +}; + +function ChangeExpensifyLoginLink({credentials, onPress}: ChangeExpensifyLoginLinkProps) { + const styles = useThemeStyles(); + const {translate, formatPhoneNumber} = useLocalize(); + return ( + + {!isEmptyObject(credentials?.login) && {translate('loginForm.notYou', {user: credentials?.login ? formatPhoneNumber(credentials.login) : ''})}} + + {translate('common.goBack')}. + + + ); +} + +ChangeExpensifyLoginLink.displayName = 'ChangeExpensifyLoginLink'; + +export default withOnyx({ + credentials: {key: ONYXKEYS.CREDENTIALS}, +})(ChangeExpensifyLoginLink); diff --git a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx similarity index 58% rename from src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js rename to src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx index fd5e9b952612..c8331f3a3f46 100755 --- a/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.js +++ b/src/pages/signin/ValidateCodeForm/BaseValidateCodeForm.tsx @@ -1,140 +1,117 @@ import {useIsFocused} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useRef, useState} from 'react'; +import type {TextStyle} from 'react-native'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import Button from '@components/Button'; import FormHelpMessage from '@components/FormHelpMessage'; +import type {MagicCodeInputHandle} from '@components/MagicCodeInput'; import MagicCodeInput from '@components/MagicCodeInput'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import type {WithToggleVisibilityViewProps} from '@components/withToggleVisibilityView'; import withToggleVisibilityView from '@components/withToggleVisibilityView'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import compose from '@libs/compose'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; import ChangeExpensifyLoginLink from '@pages/signin/ChangeExpensifyLoginLink'; import Terms from '@pages/signin/Terms'; -import * as Session from '@userActions/Session'; +import * as SessionActions from '@userActions/Session'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {Account, Credentials, Session} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; -const propTypes = { - /* Onyx Props */ - +type BaseValidateCodeFormOnyxProps = { /** The details about the account that the user is signing in with */ - account: PropTypes.shape({ - /** Whether or not two-factor authentication is required */ - requiresTwoFactorAuth: PropTypes.bool, - - /** Whether or not a sign on form is loading (being submitted) */ - isLoading: PropTypes.bool, - - /** Whether or not the user has SAML enabled on their account */ - isSAMLEnabled: PropTypes.bool, - - /** Whether or not SAML is required on the account */ - isSAMLRequired: PropTypes.bool, - }), - - /** The credentials of the person signing in */ - credentials: PropTypes.shape({ - /** The login of the person signing in */ - login: PropTypes.string, - }), - - /** Session of currently logged in user */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - }), + account: OnyxEntry; - /** Information about the network */ - network: networkPropTypes.isRequired, + /** The credentials of the person logging in */ + credentials: OnyxEntry; - /** Specifies autocomplete hints for the system, so it can provide autofill */ - autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code']).isRequired, + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; - /** Determines if user is switched to using recovery code instead of 2fa code */ - isUsingRecoveryCode: PropTypes.bool.isRequired, +type BaseValidateCodeFormProps = WithToggleVisibilityViewProps & + BaseValidateCodeFormOnyxProps & { + /** Specifies autocomplete hints for the system, so it can provide autofill */ + autoComplete: 'sms-otp' | 'one-time-code'; - /** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */ - setIsUsingRecoveryCode: PropTypes.func.isRequired, + /** Determines if user is switched to using recovery code instead of 2fa code */ + isUsingRecoveryCode: boolean; - ...withLocalizePropTypes, -}; + /** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */ + setIsUsingRecoveryCode: (isUsingRecoveryCode: boolean) => void; + }; -const defaultProps = { - account: {}, - credentials: {}, - session: { - authToken: null, - }, -}; +type ValidateCodeFormVariant = 'validateCode' | 'twoFactorAuthCode' | 'recoveryCode'; -function BaseValidateCodeForm(props) { - const theme = useTheme(); +function BaseValidateCodeForm({account, credentials, session, autoComplete, isUsingRecoveryCode, setIsUsingRecoveryCode, isVisible}: BaseValidateCodeFormProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); const isFocused = useIsFocused(); - const [formError, setFormError] = useState({}); - const [validateCode, setValidateCode] = useState(props.credentials.validateCode || ''); + const {isOffline} = useNetwork(); + const [formError, setFormError] = useState>>({}); + const [validateCode, setValidateCode] = useState(credentials?.validateCode ?? ''); const [twoFactorAuthCode, setTwoFactorAuthCode] = useState(''); const [timeRemaining, setTimeRemaining] = useState(30); const [recoveryCode, setRecoveryCode] = useState(''); - const [needToClearError, setNeedToClearError] = useState(props.account.errors); + const [needToClearError, setNeedToClearError] = useState(account?.errors); - const prevRequiresTwoFactorAuth = usePrevious(props.account.requiresTwoFactorAuth); - const prevValidateCode = usePrevious(props.credentials.validateCode); + const prevRequiresTwoFactorAuth = usePrevious(account?.requiresTwoFactorAuth); + const prevValidateCode = usePrevious(credentials?.validateCode); - const inputValidateCodeRef = useRef(); - const input2FARef = useRef(); - const timerRef = useRef(); + const inputValidateCodeRef = useRef(); + const input2FARef = useRef(); + const timerRef = useRef(); - const hasError = Boolean(props.account) && !_.isEmpty(props.account.errors) && !needToClearError; - const isLoadingResendValidationForm = props.account.loadingForm === CONST.FORMS.RESEND_VALIDATE_CODE_FORM; - const shouldDisableResendValidateCode = props.network.isOffline || props.account.isLoading; + const hasError = Boolean(account) && !isEmptyObject(account?.errors) && !needToClearError; + const isLoadingResendValidationForm = account?.loadingForm === CONST.FORMS.RESEND_VALIDATE_CODE_FORM; + const shouldDisableResendValidateCode = isOffline ?? account?.isLoading; const isValidateCodeFormSubmitting = - props.account.isLoading && props.account.loadingForm === (props.account.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM); + account?.isLoading && account?.loadingForm === (account?.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM); useEffect(() => { - if (!(inputValidateCodeRef.current && hasError && (props.session.autoAuthState === CONST.AUTO_AUTH_STATE.FAILED || props.account.isLoading))) { + if (!(inputValidateCodeRef.current && hasError && (session?.autoAuthState === CONST.AUTO_AUTH_STATE.FAILED || account?.isLoading))) { return; } inputValidateCodeRef.current.blur(); - }, [props.account.isLoading, props.session.autoAuthState, hasError]); + }, [account?.isLoading, session?.autoAuthState, hasError]); useEffect(() => { - if (!inputValidateCodeRef.current || !canFocusInputOnScreenFocus() || !props.isVisible || !isFocused) { + if (!inputValidateCodeRef.current || !canFocusInputOnScreenFocus() || !isVisible || !isFocused) { return; } inputValidateCodeRef.current.focus(); - }, [props.isVisible, isFocused]); + }, [isVisible, isFocused]); useEffect(() => { - if (prevValidateCode || !props.credentials.validateCode) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (prevValidateCode || !credentials?.validateCode) { return; } - setValidateCode(props.credentials.validateCode); - }, [props.credentials.validateCode, prevValidateCode]); + setValidateCode(credentials.validateCode); + }, [credentials?.validateCode, prevValidateCode]); useEffect(() => { - if (!input2FARef.current || prevRequiresTwoFactorAuth || !props.account.requiresTwoFactorAuth) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (!input2FARef.current || prevRequiresTwoFactorAuth || !account?.requiresTwoFactorAuth) { return; } input2FARef.current.focus(); - }, [props.account.requiresTwoFactorAuth, prevRequiresTwoFactorAuth]); + }, [account?.requiresTwoFactorAuth, prevRequiresTwoFactorAuth]); useEffect(() => { if (!inputValidateCodeRef.current || validateCode.length > 0) { @@ -163,27 +140,22 @@ function BaseValidateCodeForm(props) { /** * Handle text input and clear formError upon text change - * - * @param {String} text - * @param {String} key */ - const onTextInput = (text, key) => { - let setInput; + const onTextInput = (text: string, key: ValidateCodeFormVariant) => { if (key === 'validateCode') { - setInput = setValidateCode; + setValidateCode(text); } if (key === 'twoFactorAuthCode') { - setInput = setTwoFactorAuthCode; + setTwoFactorAuthCode(text); } if (key === 'recoveryCode') { - setInput = setRecoveryCode; + setRecoveryCode(text); } - setInput(text); setFormError((prevError) => ({...prevError, [key]: ''})); - if (props.account.errors) { - Session.clearAccountMessages(); + if (account?.errors) { + SessionActions.clearAccountMessages(); } }; @@ -191,8 +163,8 @@ function BaseValidateCodeForm(props) { * Trigger the reset validate code flow and ensure the 2FA input field is reset to avoid it being permanently hidden */ const resendValidateCode = () => { - User.resendValidateCode(props.credentials.login); - inputValidateCodeRef.current.clear(); + User.resendValidateCode(credentials?.login ?? ''); + inputValidateCodeRef.current?.clear(); // Give feedback to the user to let them know the email was sent so that they don't spam the button. setTimeRemaining(30); }; @@ -204,7 +176,7 @@ function BaseValidateCodeForm(props) { setTwoFactorAuthCode(''); setFormError({}); setValidateCode(''); - props.setIsUsingRecoveryCode(false); + setIsUsingRecoveryCode(false); setRecoveryCode(''); }; @@ -213,7 +185,7 @@ function BaseValidateCodeForm(props) { */ const clearSignInData = () => { clearLocalSignInData(); - Session.clearSignInData(); + SessionActions.clearSignInData(); }; useEffect(() => { @@ -221,26 +193,26 @@ function BaseValidateCodeForm(props) { return; } - if (props.account.errors) { - Session.clearAccountMessages(); + if (account?.errors) { + SessionActions.clearAccountMessages(); return; } setNeedToClearError(false); - }, [props.account.errors, needToClearError]); + }, [account?.errors, needToClearError]); /** * Switches between 2fa and recovery code, clears inputs and errors */ const switchBetween2faAndRecoveryCode = () => { - props.setIsUsingRecoveryCode(!props.isUsingRecoveryCode); + setIsUsingRecoveryCode(!isUsingRecoveryCode); setRecoveryCode(''); setTwoFactorAuthCode(''); - setFormError((prevError) => ({...prevError, recoveryCode: '', twoFactorAuthCode: ''})); + setFormError((prevError) => ({...prevError, recoveryCode: undefined, twoFactorAuthCode: undefined})); - if (props.account.errors) { - Session.clearAccountMessages(); + if (account?.errors) { + SessionActions.clearAccountMessages(); } }; @@ -258,10 +230,10 @@ function BaseValidateCodeForm(props) { * Check that all the form fields are valid, then trigger the submit callback */ const validateAndSubmitForm = useCallback(() => { - if (props.account.isLoading) { + if (account?.isLoading) { return; } - const requiresTwoFactorAuth = props.account.requiresTwoFactorAuth; + const requiresTwoFactorAuth = account?.requiresTwoFactorAuth; if (requiresTwoFactorAuth) { if (input2FARef.current) { input2FARef.current.blur(); @@ -269,7 +241,7 @@ function BaseValidateCodeForm(props) { /** * User could be using either recovery code or 2fa code */ - if (!props.isUsingRecoveryCode) { + if (!isUsingRecoveryCode) { if (!twoFactorAuthCode.trim()) { setFormError({twoFactorAuthCode: 'validateCodeForm.error.pleaseFillTwoFactorAuth'}); return; @@ -303,30 +275,30 @@ function BaseValidateCodeForm(props) { } setFormError({}); - const recoveryCodeOr2faCode = props.isUsingRecoveryCode ? recoveryCode : twoFactorAuthCode; + const recoveryCodeOr2faCode = isUsingRecoveryCode ? recoveryCode : twoFactorAuthCode; - const accountID = lodashGet(props.credentials, 'accountID'); + const accountID = credentials?.accountID; if (accountID) { - Session.signInWithValidateCode(accountID, validateCode, recoveryCodeOr2faCode); + SessionActions.signInWithValidateCode(accountID, validateCode, recoveryCodeOr2faCode); } else { - Session.signIn(validateCode, recoveryCodeOr2faCode); + SessionActions.signIn(validateCode, recoveryCodeOr2faCode); } - }, [props.account, props.credentials, twoFactorAuthCode, validateCode, props.isUsingRecoveryCode, recoveryCode]); + }, [account, credentials, twoFactorAuthCode, validateCode, isUsingRecoveryCode, recoveryCode]); return ( <> {/* At this point, if we know the account requires 2FA we already successfully authenticated */} - {props.account.requiresTwoFactorAuth ? ( + {account?.requiresTwoFactorAuth ? ( - {props.isUsingRecoveryCode ? ( + {isUsingRecoveryCode ? ( onTextInput(text, 'recoveryCode')} maxLength={CONST.RECOVERY_CODE_LENGTH} - label={props.translate('recoveryCodeForm.recoveryCode')} - errorText={formError.recoveryCode ? props.translate(formError.recoveryCode) : ''} + label={translate('recoveryCodeForm.recoveryCode')} + errorText={formError.recoveryCode ? translate(formError.recoveryCode) : ''} hasError={hasError} onSubmitEditing={validateAndSubmitForm} autoFocus @@ -334,70 +306,70 @@ function BaseValidateCodeForm(props) { ) : ( { + input2FARef.current = magicCodeInput; + }} name="twoFactorAuthCode" value={twoFactorAuthCode} onChangeText={(text) => onTextInput(text, 'twoFactorAuthCode')} onFulfill={validateAndSubmitForm} maxLength={CONST.TFA_CODE_LENGTH} - errorText={formError.twoFactorAuthCode ? props.translate(formError.twoFactorAuthCode) : ''} + errorText={formError.twoFactorAuthCode ? translate(formError.twoFactorAuthCode) : ''} hasError={hasError} autoFocus key="twoFactorAuthCode" /> )} - {hasError && } + {hasError && } - {props.isUsingRecoveryCode ? props.translate('recoveryCodeForm.use2fa') : props.translate('recoveryCodeForm.useRecoveryCode')} + {isUsingRecoveryCode ? translate('recoveryCodeForm.use2fa') : translate('recoveryCodeForm.useRecoveryCode')} ) : ( { + inputValidateCodeRef.current = magicCodeInput; + }} name="validateCode" value={validateCode} onChangeText={(text) => onTextInput(text, 'validateCode')} onFulfill={validateAndSubmitForm} - errorText={formError.validateCode ? props.translate(formError.validateCode) : ''} + errorText={formError.validateCode ? translate(formError.validateCode) : ''} hasError={hasError} autoFocus key="validateCode" testID="validateCode" /> - {hasError && } + {hasError && !isEmptyObject(account) && } - {timeRemaining > 0 && !props.network.isOffline ? ( + {timeRemaining > 0 && !isOffline ? ( - {props.translate('validateCodeForm.requestNewCode')} + {translate('validateCodeForm.requestNewCode')} 00:{String(timeRemaining).padStart(2, '0')} ) : ( - - {hasError ? props.translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : props.translate('validateCodeForm.magicCodeNotReceived')} + + {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate('validateCodeForm.magicCodeNotReceived')} )} @@ -406,10 +378,10 @@ function BaseValidateCodeForm(props) { )}