diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index d7a94c2a4337..32d3919efbe4 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -40,9 +40,22 @@ When creating RHP flows, you have to remember a couple things: An example of adding `Settings_Workspaces` page: -1. Add path to `ROUTES.js`: https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/ROUTES.js#L36 +1. Add path to `ROUTES.ts`: https://github.com/Expensify/App/blob/main/src/ROUTES.ts + +```ts +export const ROUTES = { + // static route + SETTINGS_WORKSPACES: 'settings/workspaces', + // dynamic route + SETTINGS_WORKSPACES: { + route: 'settings/:accountID', + getRoute: (accountID: number) => `settings/${accountID}` as const, + }, +}; + +``` -2. Add `Settings_Workspaces` page to proper RHP flow in `linkingConfig.js`: https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/libs/Navigation/linkingConfig.js#L40-L42 +2. Add `Settings_Workspaces` page to proper RHP flow in `linkingConfig.ts`: https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/libs/Navigation/linkingConfig.js#L40-L42 3. Add your page to proper navigator (it should be aligned with where you've put it in the previous step) https://github.com/Expensify/App/blob/3531af22dcadaa94ed11eccf370517dca0b8c305/src/libs/Navigation/AppNavigator/ModalStackNavigators.js#L334-L338 diff --git a/scripts/start-android.sh b/scripts/start-android.sh old mode 100644 new mode 100755 diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 26589a3db0e0..a96b229ef4c1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1,30 +1,28 @@ -import {ValueOf} from 'type-fest'; +import {IsEqual, ValueOf} from 'type-fest'; import CONST from './CONST'; -/** - * This is a file containing constants for all the routes we want to be able to go to - */ +// This is a file containing constants for all the routes we want to be able to go to /** * Builds a URL with an encoded URI component for the `backTo` param which can be added to the end of URLs */ -function getUrlWithBackToParam(url: string, backTo?: string): string { - const backToParam = backTo ? `${url.includes('?') ? '&' : '?'}backTo=${encodeURIComponent(backTo)}` : ''; - return url + backToParam; +function getUrlWithBackToParam(url: TUrl, backTo?: string): `${TUrl}` | `${TUrl}?backTo=${string}` | `${TUrl}&backTo=${string}` { + const backToParam = backTo ? (`${url.includes('?') ? '&' : '?'}backTo=${encodeURIComponent(backTo)}` as const) : ''; + return `${url}${backToParam}` as const; } -export default { +const ROUTES = { HOME: '', /** This is a utility route used to go to the user's concierge chat, or the sign-in page if the user's not authenticated */ CONCIERGE: 'concierge', FLAG_COMMENT: { route: 'flag/:reportID/:reportActionID', - getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}`, + getRoute: (reportID: string, reportActionID: string) => `flag/${reportID}/${reportActionID}` as const, }, SEARCH: 'search', DETAILS: { route: 'details', - getRoute: (login: string) => `details?login=${encodeURIComponent(login)}`, + getRoute: (login: string) => `details?login=${encodeURIComponent(login)}` as const, }, PROFILE: { route: 'a/:accountID', @@ -35,7 +33,7 @@ export default { VALIDATE_LOGIN: 'v/:accountID/:validateCode', GET_ASSISTANCE: { route: 'get-assistance/:taskID', - getRoute: (taskID: string) => `get-assistance/${taskID}`, + getRoute: (taskID: string) => `get-assistance/${taskID}` as const, }, UNLINK_LOGIN: 'u/:accountID/:validateCode', APPLE_SIGN_IN: 'sign-in-with-apple', @@ -54,7 +52,7 @@ export default { BANK_ACCOUNT_PERSONAL: 'bank-account/personal', BANK_ACCOUNT_WITH_STEP_TO_OPEN: { route: 'bank-account/:stepToOpen?', - getRoute: (stepToOpen = '', policyID = '', backTo?: string): string => getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}`, backTo), + getRoute: (stepToOpen = '', policyID = '', backTo?: string) => getUrlWithBackToParam(`bank-account/${stepToOpen}?policyID=${policyID}`, backTo), }, SETTINGS: 'settings', @@ -77,44 +75,44 @@ export default { SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARD: { route: '/settings/wallet/card/:domain', - getRoute: (domain: string) => `/settings/wallet/card/${domain}`, + getRoute: (domain: string) => `/settings/wallet/card/${domain}` as const, }, SETTINGS_REPORT_FRAUD: { route: '/settings/wallet/card/:domain/report-virtual-fraud', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-virtual-fraud`, + getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-virtual-fraud` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: { route: '/settings/wallet/card/:domain/get-physical/name', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/name`, + getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/name` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE: { route: '/settings/wallet/card/:domain/get-physical/phone', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/phone`, + getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/phone` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS: { route: '/settings/wallet/card/:domain/get-physical/address', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/address`, + getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/address` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_CONFIRM: { route: '/settings/wallet/card/:domain/get-physical/confirm', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/confirm`, + getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/confirm` as const, }, SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', SETTINGS_ENABLE_PAYMENTS: 'settings/wallet/enable-payments', SETTINGS_WALLET_CARD_DIGITAL_DETAILS_UPDATE_ADDRESS: { route: 'settings/wallet/card/:domain/digital-details/update-address', - getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address`, + getRoute: (domain: string) => `settings/wallet/card/${domain}/digital-details/update-address` as const, }, SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: { route: '/settings/wallet/card/:domain/report-card-lost-or-damaged', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-card-lost-or-damaged`, + getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-card-lost-or-damaged` as const, }, SETTINGS_WALLET_CARD_ACTIVATE: { route: 'settings/wallet/card/:domain/activate', - getRoute: (domain: string) => `settings/wallet/card/${domain}/activate`, + getRoute: (domain: string) => `settings/wallet/card/${domain}/activate` as const, }, SETTINGS_PERSONAL_DETAILS: 'settings/profile/personal-details', SETTINGS_PERSONAL_DETAILS_LEGAL_NAME: 'settings/profile/personal-details/legal-name', @@ -130,7 +128,7 @@ export default { }, SETTINGS_CONTACT_METHOD_DETAILS: { route: 'settings/profile/contact-methods/:contactMethod/details', - getRoute: (contactMethod: string) => `settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details`, + getRoute: (contactMethod: string) => `settings/profile/contact-methods/${encodeURIComponent(contactMethod)}/details` as const, }, SETTINGS_NEW_CONTACT_METHOD: 'settings/profile/contact-methods/new', SETTINGS_2FA: 'settings/security/two-factor-auth', @@ -146,157 +144,158 @@ export default { REPORT: 'r', REPORT_WITH_ID: { route: 'r/:reportID?/:reportActionID?', - getRoute: (reportID: string) => `r/${reportID}`, + getRoute: (reportID: string) => `r/${reportID}` as const, }, EDIT_REQUEST: { route: 'r/:threadReportID/edit/:field', - getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}`, + getRoute: (threadReportID: string, field: ValueOf) => `r/${threadReportID}/edit/${field}` as const, }, EDIT_CURRENCY_REQUEST: { route: 'r/:threadReportID/edit/currency', - getRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}`, + getRoute: (threadReportID: string, currency: string, backTo: string) => `r/${threadReportID}/edit/currency?currency=${currency}&backTo=${backTo}` as const, }, REPORT_WITH_ID_DETAILS_SHARE_CODE: { route: 'r/:reportID/details/shareCode', - getRoute: (reportID: string) => `r/${reportID}/details/shareCode`, + getRoute: (reportID: string) => `r/${reportID}/details/shareCode` as const, }, REPORT_ATTACHMENTS: { route: 'r/:reportID/attachment', - getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}`, + getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}` as const, }, REPORT_PARTICIPANTS: { route: 'r/:reportID/participants', - getRoute: (reportID: string) => `r/${reportID}/participants`, + getRoute: (reportID: string) => `r/${reportID}/participants` as const, }, REPORT_WITH_ID_DETAILS: { route: 'r/:reportID/details', - getRoute: (reportID: string) => `r/${reportID}/details`, + getRoute: (reportID: string) => `r/${reportID}/details` as const, }, REPORT_SETTINGS: { route: 'r/:reportID/settings', - getRoute: (reportID: string) => `r/${reportID}/settings`, + getRoute: (reportID: string) => `r/${reportID}/settings` as const, }, REPORT_SETTINGS_ROOM_NAME: { route: 'r/:reportID/settings/room-name', - getRoute: (reportID: string) => `r/${reportID}/settings/room-name`, + getRoute: (reportID: string) => `r/${reportID}/settings/room-name` as const, }, REPORT_SETTINGS_NOTIFICATION_PREFERENCES: { route: 'r/:reportID/settings/notification-preferences', - getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences`, + getRoute: (reportID: string) => `r/${reportID}/settings/notification-preferences` as const, }, REPORT_SETTINGS_WRITE_CAPABILITY: { route: 'r/:reportID/settings/who-can-post', - getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post`, + getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post` as const, }, REPORT_WELCOME_MESSAGE: { route: 'r/:reportID/welcomeMessage', - getRoute: (reportID: string) => `r/${reportID}/welcomeMessage`, + getRoute: (reportID: string) => `r/${reportID}/welcomeMessage` as const, }, SPLIT_BILL_DETAILS: { route: 'r/:reportID/split/:reportActionID', - getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}`, + getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` as const, }, EDIT_SPLIT_BILL: { route: `r/:reportID/split/:reportActionID/edit/:field`, - getRoute: (reportID: string, reportActionID: string, field: ValueOf) => `r/${reportID}/split/${reportActionID}/edit/${field}`, + getRoute: (reportID: string, reportActionID: string, field: ValueOf) => `r/${reportID}/split/${reportActionID}/edit/${field}` as const, }, EDIT_SPLIT_BILL_CURRENCY: { route: 'r/:reportID/split/:reportActionID/edit/currency', - getRoute: (reportID: string, reportActionID: string, currency: string, backTo: string) => `r/${reportID}/split/${reportActionID}/edit/currency?currency=${currency}&backTo=${backTo}`, + getRoute: (reportID: string, reportActionID: string, currency: string, backTo: string) => + `r/${reportID}/split/${reportActionID}/edit/currency?currency=${currency}&backTo=${backTo}` as const, }, TASK_TITLE: { route: 'r/:reportID/title', - getRoute: (reportID: string) => `r/${reportID}/title`, + getRoute: (reportID: string) => `r/${reportID}/title` as const, }, TASK_DESCRIPTION: { route: 'r/:reportID/description', - getRoute: (reportID: string) => `r/${reportID}/description`, + getRoute: (reportID: string) => `r/${reportID}/description` as const, }, TASK_ASSIGNEE: { route: 'r/:reportID/assignee', - getRoute: (reportID: string) => `r/${reportID}/assignee`, + getRoute: (reportID: string) => `r/${reportID}/assignee` as const, }, PRIVATE_NOTES_VIEW: { route: 'r/:reportID/notes/:accountID', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}`, + getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}` as const, }, PRIVATE_NOTES_LIST: { route: 'r/:reportID/notes', - getRoute: (reportID: string) => `r/${reportID}/notes`, + getRoute: (reportID: string) => `r/${reportID}/notes` as const, }, PRIVATE_NOTES_EDIT: { route: 'r/:reportID/notes/:accountID/edit', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit`, + getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}/edit` as const, }, ROOM_MEMBERS: { route: 'r/:reportID/members', - getRoute: (reportID: string) => `r/${reportID}/members`, + getRoute: (reportID: string) => `r/${reportID}/members` as const, }, ROOM_INVITE: { route: 'r/:reportID/invite', - getRoute: (reportID: string) => `r/${reportID}/invite`, + getRoute: (reportID: string) => `r/${reportID}/invite` as const, }, // To see the available iouType, please refer to CONST.IOU.TYPE MONEY_REQUEST: { route: ':iouType/new/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}` as const, }, MONEY_REQUEST_AMOUNT: { route: ':iouType/new/amount/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/amount/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/amount/${reportID}` as const, }, MONEY_REQUEST_PARTICIPANTS: { route: ':iouType/new/participants/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/participants/${reportID}` as const, }, MONEY_REQUEST_CONFIRMATION: { route: ':iouType/new/confirmation/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/confirmation/${reportID}` as const, }, MONEY_REQUEST_DATE: { route: ':iouType/new/date/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/date/${reportID}` as const, }, MONEY_REQUEST_CURRENCY: { route: ':iouType/new/currency/:reportID?', - getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}`, + getRoute: (iouType: string, reportID: string, currency: string, backTo: string) => `${iouType}/new/currency/${reportID}?currency=${currency}&backTo=${backTo}` as const, }, MONEY_REQUEST_DESCRIPTION: { route: ':iouType/new/description/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/description/${reportID}` as const, }, MONEY_REQUEST_CATEGORY: { route: ':iouType/new/category/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/category/${reportID}` as const, }, MONEY_REQUEST_TAG: { route: ':iouType/new/tag/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/tag/${reportID}` as const, }, MONEY_REQUEST_MERCHANT: { route: ':iouType/new/merchant/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/merchant/${reportID}` as const, }, MONEY_REQUEST_WAYPOINT: { route: ':iouType/new/waypoint/:waypointIndex', - getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}`, + getRoute: (iouType: string, waypointIndex: number) => `${iouType}/new/waypoint/${waypointIndex}` as const, }, MONEY_REQUEST_RECEIPT: { route: ':iouType/new/receipt/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const, }, MONEY_REQUEST_DISTANCE: { route: ':iouType/new/address/:reportID?', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const, }, MONEY_REQUEST_EDIT_WAYPOINT: { route: 'r/:threadReportID/edit/distance/:transactionID/waypoint/:waypointIndex', - getRoute: (threadReportID: number, transactionID: string, waypointIndex: number) => `r/${threadReportID}/edit/distance/${transactionID}/waypoint/${waypointIndex}`, + getRoute: (threadReportID: number, transactionID: string, waypointIndex: number) => `r/${threadReportID}/edit/distance/${transactionID}/waypoint/${waypointIndex}` as const, }, MONEY_REQUEST_DISTANCE_TAB: { route: ':iouType/new/:reportID?/distance', - getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance`, + getRoute: (iouType: string, reportID = '') => `${iouType}/new/${reportID}/distance` as const, }, MONEY_REQUEST_MANUAL_TAB: ':iouType/new/:reportID?/manual', MONEY_REQUEST_SCAN_TAB: ':iouType/new/:reportID?/scan', @@ -321,63 +320,63 @@ export default { ERECEIPT: { route: 'eReceipt/:transactionID', - getRoute: (transactionID: string) => `eReceipt/${transactionID}`, + getRoute: (transactionID: string) => `eReceipt/${transactionID}` as const, }, WORKSPACE_NEW: 'workspace/new', WORKSPACE_NEW_ROOM: 'workspace/new-room', WORKSPACE_INITIAL: { route: 'workspace/:policyID', - getRoute: (policyID: string) => `workspace/${policyID}`, + getRoute: (policyID: string) => `workspace/${policyID}` as const, }, WORKSPACE_INVITE: { route: 'workspace/:policyID/invite', - getRoute: (policyID: string) => `workspace/${policyID}/invite`, + getRoute: (policyID: string) => `workspace/${policyID}/invite` as const, }, WORKSPACE_INVITE_MESSAGE: { route: 'workspace/:policyID/invite-message', - getRoute: (policyID: string) => `workspace/${policyID}/invite-message`, + getRoute: (policyID: string) => `workspace/${policyID}/invite-message` as const, }, WORKSPACE_SETTINGS: { route: 'workspace/:policyID/settings', - getRoute: (policyID: string) => `workspace/${policyID}/settings`, + getRoute: (policyID: string) => `workspace/${policyID}/settings` as const, }, WORKSPACE_SETTINGS_CURRENCY: { route: 'workspace/:policyID/settings/currency', - getRoute: (policyID: string) => `workspace/${policyID}/settings/currency`, + getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', - getRoute: (policyID: string) => `workspace/${policyID}/card`, + getRoute: (policyID: string) => `workspace/${policyID}/card` as const, }, WORKSPACE_REIMBURSE: { route: 'workspace/:policyID/reimburse', - getRoute: (policyID: string) => `workspace/${policyID}/reimburse`, + getRoute: (policyID: string) => `workspace/${policyID}/reimburse` as const, }, WORKSPACE_RATE_AND_UNIT: { route: 'workspace/:policyID/rateandunit', - getRoute: (policyID: string) => `workspace/${policyID}/rateandunit`, + getRoute: (policyID: string) => `workspace/${policyID}/rateandunit` as const, }, WORKSPACE_BILLS: { route: 'workspace/:policyID/bills', - getRoute: (policyID: string) => `workspace/${policyID}/bills`, + getRoute: (policyID: string) => `workspace/${policyID}/bills` as const, }, WORKSPACE_INVOICES: { route: 'workspace/:policyID/invoices', - getRoute: (policyID: string) => `workspace/${policyID}/invoices`, + getRoute: (policyID: string) => `workspace/${policyID}/invoices` as const, }, WORKSPACE_TRAVEL: { route: 'workspace/:policyID/travel', - getRoute: (policyID: string) => `workspace/${policyID}/travel`, + getRoute: (policyID: string) => `workspace/${policyID}/travel` as const, }, WORKSPACE_MEMBERS: { route: 'workspace/:policyID/members', - getRoute: (policyID: string) => `workspace/${policyID}/members`, + getRoute: (policyID: string) => `workspace/${policyID}/members` as const, }, // Referral program promotion REFERRAL_DETAILS_MODAL: { route: 'referral/:contentType', - getRoute: (contentType: string) => `referral/${contentType}`, + getRoute: (contentType: string) => `referral/${contentType}` as const, }, // These are some one-off routes that will be removed once they're no longer needed (see GH issues for details) @@ -385,3 +384,24 @@ export default { SBE: 'sbe', MONEY2020: 'money2020', } as const; + +export default ROUTES; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type ExtractRouteName = TRoute extends {getRoute: (...args: any[]) => infer TRouteName} ? TRouteName : TRoute; + +type AllRoutes = { + [K in keyof typeof ROUTES]: ExtractRouteName<(typeof ROUTES)[K]>; +}[keyof typeof ROUTES]; + +type RouteIsPlainString = IsEqual; + +/** + * Represents all routes in the app as a union of literal strings. + * + * If this type resolves to `never`, it implies that one or more routes defined within `ROUTES` have not correctly used + * `as const` in their `getRoute` function return value. + */ +type Route = RouteIsPlainString extends true ? never : AllRoutes; + +export type {Route}; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f957a1dbb25e..f4cbcf4f2564 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -9,13 +9,13 @@ const PROTECTED_SCREENS = { REPORT_ATTACHMENTS: 'ReportAttachments', } as const; -export default { +const SCREENS = { ...PROTECTED_SCREENS, - LOADING: 'Loading', REPORT: 'Report', NOT_FOUND: 'not-found', TRANSITION_BETWEEN_APPS: 'TransitionBetweenApps', VALIDATE_LOGIN: 'ValidateLogin', + UNLINK_LOGIN: 'UnlinkLogin', SETTINGS: { ROOT: 'Settings_Root', PREFERENCES: 'Settings_Preferences', @@ -40,4 +40,5 @@ export default { SAML_SIGN_IN: 'SAMLSignIn', } as const; +export default SCREENS; export {PROTECTED_SCREENS}; diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index 22da02159073..3ce9eeae37b5 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -8,6 +8,7 @@ type CurrentReportIDContextValue = { updateCurrentReportID: (state: NavigationState) => void; currentReportID: string; }; + type CurrentReportIDContextProviderProps = { /** Actual content wrapped by this component */ children: React.ReactNode; diff --git a/src/components/withNavigation.tsx b/src/components/withNavigation.tsx index 0834eabc2adb..88788edafb79 100644 --- a/src/components/withNavigation.tsx +++ b/src/components/withNavigation.tsx @@ -1,9 +1,10 @@ import {NavigationProp, useNavigation} from '@react-navigation/native'; import React, {ComponentType, ForwardedRef, RefAttributes} from 'react'; import getComponentDisplayName from '@libs/getComponentDisplayName'; +import {RootStackParamList} from '@libs/Navigation/types'; type WithNavigationProps = { - navigation: NavigationProp; + navigation: NavigationProp; }; export default function withNavigation( diff --git a/src/hooks/useFlipper/index.js b/src/hooks/useFlipper/index.js deleted file mode 100644 index 2d1ec238274a..000000000000 --- a/src/hooks/useFlipper/index.js +++ /dev/null @@ -1 +0,0 @@ -export default () => {}; diff --git a/src/hooks/useFlipper/index.native.js b/src/hooks/useFlipper/index.native.js deleted file mode 100644 index 90779d5b8a14..000000000000 --- a/src/hooks/useFlipper/index.native.js +++ /dev/null @@ -1,3 +0,0 @@ -import {useFlipper} from '@react-navigation/devtools'; - -export default useFlipper; diff --git a/src/hooks/useFlipper/index.native.ts b/src/hooks/useFlipper/index.native.ts new file mode 100644 index 000000000000..df1aa3bf513b --- /dev/null +++ b/src/hooks/useFlipper/index.native.ts @@ -0,0 +1,6 @@ +import {useFlipper as useFlipperRN} from '@react-navigation/devtools'; +import UseFlipper from './types'; + +const useFlipper: UseFlipper = useFlipperRN; + +export default useFlipper; diff --git a/src/hooks/useFlipper/index.ts b/src/hooks/useFlipper/index.ts new file mode 100644 index 000000000000..26d4c9659ad8 --- /dev/null +++ b/src/hooks/useFlipper/index.ts @@ -0,0 +1,5 @@ +import UseFlipper from './types'; + +const useFlipper: UseFlipper = () => {}; + +export default useFlipper; diff --git a/src/hooks/useFlipper/types.ts b/src/hooks/useFlipper/types.ts new file mode 100644 index 000000000000..a995414e5dd1 --- /dev/null +++ b/src/hooks/useFlipper/types.ts @@ -0,0 +1,6 @@ +import {NavigationContainerRefWithCurrent} from '@react-navigation/core'; +import {RootStackParamList} from '@libs/Navigation/types'; + +type UseFlipper = (ref: NavigationContainerRefWithCurrent) => void; + +export default UseFlipper; diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.js b/src/libs/Navigation/AppNavigator/PublicScreens.js index b50769c7caed..09b5c0fd7734 100644 --- a/src/libs/Navigation/AppNavigator/PublicScreens.js +++ b/src/libs/Navigation/AppNavigator/PublicScreens.js @@ -26,27 +26,27 @@ function PublicScreens() { component={LogInWithShortLivedAuthTokenPage} /> diff --git a/src/libs/Navigation/FreezeWrapper.js b/src/libs/Navigation/FreezeWrapper.tsx similarity index 65% rename from src/libs/Navigation/FreezeWrapper.js rename to src/libs/Navigation/FreezeWrapper.tsx index 16a353ebddea..df3f117c9a2e 100644 --- a/src/libs/Navigation/FreezeWrapper.js +++ b/src/libs/Navigation/FreezeWrapper.tsx @@ -1,31 +1,24 @@ import {useIsFocused, useNavigation, useRoute} from '@react-navigation/native'; -import lodashFindIndex from 'lodash/findIndex'; -import PropTypes from 'prop-types'; import React, {useEffect, useRef, useState} from 'react'; import {Freeze} from 'react-freeze'; import {InteractionManager} from 'react-native'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; -const propTypes = { +type FreezeWrapperProps = ChildrenProps & { /** Prop to disable freeze */ - keepVisible: PropTypes.bool, - /** Children to wrap in FreezeWrapper. */ - children: PropTypes.node.isRequired, + keepVisible?: boolean; }; -const defaultProps = { - keepVisible: false, -}; - -function FreezeWrapper(props) { +function FreezeWrapper({keepVisible = false, children}: FreezeWrapperProps) { const [isScreenBlurred, setIsScreenBlurred] = useState(false); // we need to know the screen index to determine if the screen can be frozen - const screenIndexRef = useRef(null); + const screenIndexRef = useRef(null); const isFocused = useIsFocused(); const navigation = useNavigation(); const currentRoute = useRoute(); useEffect(() => { - const index = lodashFindIndex(navigation.getState().routes, (route) => route.key === currentRoute.key); + const index = navigation.getState().routes.findIndex((route) => route.key === currentRoute.key); screenIndexRef.current = index; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -35,7 +28,7 @@ function FreezeWrapper(props) { // if the screen is more than 1 screen away from the current screen, freeze it, // we don't want to freeze the screen if it's the previous screen because the freeze placeholder // would be visible at the beginning of the back animation then - if (navigation.getState().index - screenIndexRef.current > 1) { + if (navigation.getState().index - (screenIndexRef.current ?? 0) > 1) { InteractionManager.runAfterInteractions(() => setIsScreenBlurred(true)); } else { setIsScreenBlurred(false); @@ -44,11 +37,9 @@ function FreezeWrapper(props) { return () => unsubscribe(); }, [isFocused, isScreenBlurred, navigation]); - return {props.children}; + return {children}; } -FreezeWrapper.propTypes = propTypes; -FreezeWrapper.defaultProps = defaultProps; FreezeWrapper.displayName = 'FreezeWrapper'; export default FreezeWrapper; diff --git a/src/libs/Navigation/Navigation.js b/src/libs/Navigation/Navigation.ts similarity index 61% rename from src/libs/Navigation/Navigation.js rename to src/libs/Navigation/Navigation.ts index bfc0f509373e..c2dd3e76e7ad 100644 --- a/src/libs/Navigation/Navigation.js +++ b/src/libs/Navigation/Navigation.ts @@ -1,11 +1,10 @@ import {findFocusedRoute, getActionFromState} from '@react-navigation/core'; -import {CommonActions, getPathFromState, StackActions} from '@react-navigation/native'; -import _ from 'lodash'; -import lodashGet from 'lodash/get'; +import {CommonActions, EventMapCore, getPathFromState, NavigationState, PartialState, StackActions} from '@react-navigation/native'; +import findLastIndex from 'lodash/findLastIndex'; import Log from '@libs/Log'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; -import ROUTES from '@src/ROUTES'; +import ROUTES, {Route} from '@src/ROUTES'; import SCREENS, {PROTECTED_SCREENS} from '@src/SCREENS'; import getStateFromPath from './getStateFromPath'; import originalGetTopmostReportActionId from './getTopmostReportActionID'; @@ -13,13 +12,14 @@ import originalGetTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; import linkTo from './linkTo'; import navigationRef from './navigationRef'; +import {StackNavigationAction, StateOrRoute} from './types'; -let resolveNavigationIsReadyPromise; -const navigationIsReadyPromise = new Promise((resolve) => { +let resolveNavigationIsReadyPromise: () => void; +const navigationIsReadyPromise = new Promise((resolve) => { resolveNavigationIsReadyPromise = resolve; }); -let pendingRoute = null; +let pendingRoute: Route | null = null; let shouldPopAllStateOnUP = false; @@ -30,12 +30,7 @@ function setShouldPopAllStateOnUP() { shouldPopAllStateOnUP = true; } -/** - * @param {String} methodName - * @param {Object} params - * @returns {Boolean} - */ -function canNavigate(methodName, params = {}) { +function canNavigate(methodName: string, params: Record = {}): boolean { if (navigationRef.isReady()) { return true; } @@ -49,37 +44,32 @@ const getTopmostReportId = (state = navigationRef.getState()) => originalGetTopm // Re-exporting the getTopmostReportActionID here to fill in default value for state. The getTopmostReportActionID isn't defined in this file to avoid cyclic dependencies. const getTopmostReportActionId = (state = navigationRef.getState()) => originalGetTopmostReportActionId(state); -/** - * Method for finding on which index in stack we are. - * @param {Object} route - * @param {Number} index - * @returns {Number} - */ -const getActiveRouteIndex = function (route, index) { - if (route.routes) { - const childActiveRoute = route.routes[route.index || 0]; - return getActiveRouteIndex(childActiveRoute, route.index || 0); +/** Method for finding on which index in stack we are. */ +function getActiveRouteIndex(stateOrRoute: StateOrRoute, index?: number): number | undefined { + if ('routes' in stateOrRoute && stateOrRoute.routes) { + const childActiveRoute = stateOrRoute.routes[stateOrRoute.index ?? 0]; + return getActiveRouteIndex(childActiveRoute, stateOrRoute.index ?? 0); } - if (route.state && route.state.routes) { - const childActiveRoute = route.state.routes[route.state.index || 0]; - return getActiveRouteIndex(childActiveRoute, route.state.index || 0); + if ('state' in stateOrRoute && stateOrRoute.state?.routes) { + const childActiveRoute = stateOrRoute.state.routes[stateOrRoute.state.index ?? 0]; + return getActiveRouteIndex(childActiveRoute, stateOrRoute.state.index ?? 0); } - if (route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + if ('name' in stateOrRoute && stateOrRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { return 0; } return index; -}; +} /** * Gets distance from the path in root navigator. In other words how much screen you have to pop to get to the route with this path. * The search is limited to 5 screens from the top for performance reasons. - * @param {String} path - Path that you are looking for. - * @return {Number} - Returns distance to path or -1 if the path is not found in root navigator. + * @param path - Path that you are looking for. + * @return - Returns distance to path or -1 if the path is not found in root navigator. */ -function getDistanceFromPathInRootNavigator(path) { +function getDistanceFromPathInRootNavigator(path: string): number { let currentState = navigationRef.getRootState(); for (let index = 0; index < 5; index++) { @@ -98,14 +88,10 @@ function getDistanceFromPathInRootNavigator(path) { return -1; } -/** - * Returns the current active route - * @returns {String} - */ -function getActiveRoute() { +/** Returns the current active route */ +function getActiveRoute(): string { const currentRoute = navigationRef.current && navigationRef.current.getCurrentRoute(); - const currentRouteHasName = lodashGet(currentRoute, 'name', false); - if (!currentRouteHasName) { + if (!currentRoute?.name) { return ''; } @@ -124,20 +110,19 @@ function getActiveRoute() { * Building path with getPathFromState since navigationRef.current.getCurrentRoute().path * is undefined in the first navigation. * - * @param {String} routePath Path to check - * @return {Boolean} is active + * @param routePath Path to check + * @return is active */ -function isActiveRoute(routePath) { +function isActiveRoute(routePath: Route): boolean { // We remove First forward slash from the URL before matching return getActiveRoute().substring(1) === routePath; } /** * Main navigation method for redirecting to a route. - * @param {String} route - * @param {String} [type] - Type of action to perform. Currently UP is supported. + * @param [type] - Type of action to perform. Currently UP is supported. */ -function navigate(route = ROUTES.HOME, type) { +function navigate(route: Route = ROUTES.HOME, type?: string) { if (!canNavigate('navigate', {route})) { // Store intended route if the navigator is not yet available, // we will try again after the NavigationContainer is ready @@ -149,11 +134,11 @@ function navigate(route = ROUTES.HOME, type) { } /** - * @param {String} fallbackRoute - Fallback route if pop/goBack action should, but is not possible within RHP - * @param {Boolean} shouldEnforceFallback - Enforces navigation to fallback route - * @param {Boolean} shouldPopToTop - Should we navigate to LHN on back press + * @param fallbackRoute - Fallback route if pop/goBack action should, but is not possible within RHP + * @param shouldEnforceFallback - Enforces navigation to fallback route + * @param shouldPopToTop - Should we navigate to LHN on back press */ -function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = false) { +function goBack(fallbackRoute: Route, shouldEnforceFallback = false, shouldPopToTop = false) { if (!canNavigate('goBack')) { return; } @@ -161,12 +146,12 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f if (shouldPopToTop) { if (shouldPopAllStateOnUP) { shouldPopAllStateOnUP = false; - navigationRef.current.dispatch(StackActions.popToTop()); + navigationRef.current?.dispatch(StackActions.popToTop()); return; } } - if (!navigationRef.current.canGoBack()) { + if (!navigationRef.current?.canGoBack()) { Log.hmmm('[Navigation] Unable to go back'); return; } @@ -174,9 +159,9 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f const isFirstRouteInNavigator = !getActiveRouteIndex(navigationRef.current.getState()); if (isFirstRouteInNavigator) { const rootState = navigationRef.getRootState(); - const lastRoute = _.last(rootState.routes); + const lastRoute = rootState.routes.at(-1); // If the user comes from a different flow (there is more than one route in RHP) we should go back to the previous flow on UP button press instead of using the fallbackRoute. - if (lastRoute.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && lastRoute.state.index > 0) { + if (lastRoute?.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && (lastRoute.state?.index ?? 0) > 0) { navigationRef.current.goBack(); return; } @@ -187,7 +172,7 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f return; } - const isCentralPaneFocused = findFocusedRoute(navigationRef.current.getState()).name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR; + const isCentralPaneFocused = findFocusedRoute(navigationRef.current.getState())?.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR; const distanceFromPathInRootNavigator = getDistanceFromPathInRootNavigator(fallbackRoute); // Allow CentralPane to use UP with fallback route if the path is not found in root navigator. @@ -196,7 +181,7 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f return; } - // Add posibility to go back more than one screen in root navigator if that screen is on the stack. + // Add possibility to go back more than one screen in root navigator if that screen is on the stack. if (isCentralPaneFocused && fallbackRoute && distanceFromPathInRootNavigator > 0) { navigationRef.current.dispatch(StackActions.pop(distanceFromPathInRootNavigator)); return; @@ -207,12 +192,9 @@ function goBack(fallbackRoute, shouldEnforceFallback = false, shouldPopToTop = f /** * Update route params for the specified route. - * - * @param {Object} params - * @param {String} routeKey */ -function setParams(params, routeKey) { - navigationRef.current.dispatch({ +function setParams(params: Record, routeKey: string) { + navigationRef.current?.dispatch({ ...CommonActions.setParams(params), source: routeKey, }); @@ -221,15 +203,15 @@ function setParams(params, routeKey) { /** * Dismisses the last modal stack if there is any * - * @param {String | undefined} targetReportID - The reportID to navigate to after dismissing the modal + * @param targetReportID - The reportID to navigate to after dismissing the modal */ -function dismissModal(targetReportID) { +function dismissModal(targetReportID?: string) { if (!canNavigate('dismissModal')) { return; } const rootState = navigationRef.getRootState(); - const lastRoute = _.last(rootState.routes); - switch (lastRoute.name) { + const lastRoute = rootState.routes.at(-1); + switch (lastRoute?.name) { case NAVIGATORS.RIGHT_MODAL_NAVIGATOR: case SCREENS.NOT_FOUND: case SCREENS.REPORT_ATTACHMENTS: @@ -237,16 +219,18 @@ function dismissModal(targetReportID) { if (targetReportID && targetReportID !== getTopmostReportId(rootState)) { const state = getStateFromPath(ROUTES.REPORT_WITH_ID.getRoute(targetReportID)); - const action = getActionFromState(state, linkingConfig.config); - action.type = 'REPLACE'; - navigationRef.current.dispatch(action); + const action: StackNavigationAction = getActionFromState(state, linkingConfig.config); + if (action) { + action.type = 'REPLACE'; + navigationRef.current?.dispatch(action); + } // If not-found page is in the route stack, we need to close it - } else if (targetReportID && _.some(rootState.routes, (route) => route.name === SCREENS.NOT_FOUND)) { + } else if (targetReportID && rootState.routes.some((route) => route.name === SCREENS.NOT_FOUND)) { const lastRouteIndex = rootState.routes.length - 1; - const centralRouteIndex = _.findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR); - navigationRef.current.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key}); + const centralRouteIndex = findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR); + navigationRef.current?.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key}); } else { - navigationRef.current.dispatch({...StackActions.pop(), target: rootState.key}); + navigationRef.current?.dispatch({...StackActions.pop(), target: rootState.key}); } break; default: { @@ -257,21 +241,17 @@ function dismissModal(targetReportID) { /** * Returns the current active route without the URL params - * @returns {String} */ -function getActiveRouteWithoutParams() { +function getActiveRouteWithoutParams(): string { return getActiveRoute().replace(/\?.*/, ''); } -/** Returns the active route name from a state event from the navigationRef - * @param {Object} event - * @returns {String | undefined} - * */ -function getRouteNameFromStateEvent(event) { +/** Returns the active route name from a state event from the navigationRef */ +function getRouteNameFromStateEvent(event: EventMapCore['state']): string | undefined { if (!event.data.state) { return; } - const currentRouteName = event.data.state.routes.slice(-1).name; + const currentRouteName = event.data.state.routes.at(-1)?.name; // Check to make sure we have a route name if (currentRouteName) { @@ -292,10 +272,7 @@ function goToPendingRoute() { pendingRoute = null; } -/** - * @returns {Promise} - */ -function isNavigationReady() { +function isNavigationReady(): Promise { return navigationIsReadyPromise; } @@ -307,57 +284,50 @@ function setIsNavigationReady() { /** * Checks if the navigation state contains routes that are protected (over the auth wall). * - * @function - * @param {Object} state - react-navigation state object - * - * @returns {Boolean} + * @param state - react-navigation state object */ -function navContainsProtectedRoutes(state) { - if (!state || !state.routeNames || !_.isArray(state.routeNames)) { +function navContainsProtectedRoutes(state: NavigationState | PartialState | undefined): boolean { + if (!state?.routeNames || !Array.isArray(state.routeNames)) { return false; } - const protectedScreensName = _.values(PROTECTED_SCREENS); - const difference = _.difference(protectedScreensName, state.routeNames); - - return !difference.length; + const protectedScreensName = Object.values(PROTECTED_SCREENS); + return !protectedScreensName.some((screen) => !state.routeNames?.includes(screen)); } /** - * Waits for the navitgation state to contain protected routes specified in PROTECTED_SCREENS constant. - * If the navigation is in a state, where protected routes are avilable, the promise resolve immediately. + * Waits for the navigation state to contain protected routes specified in PROTECTED_SCREENS constant. + * If the navigation is in a state, where protected routes are available, the promise resolve immediately. * * @function - * @returns {Promise} A promise that resolves when the one of the PROTECTED_SCREENS screen is available in the nav tree. + * @returns A promise that resolves when the one of the PROTECTED_SCREENS screen is available in the nav tree. * * @example * waitForProtectedRoutes() * .then(()=> console.log('Protected routes are present!')) */ function waitForProtectedRoutes() { - return new Promise((resolve) => { + return new Promise((resolve) => { isNavigationReady().then(() => { - const currentState = navigationRef.current.getState(); + const currentState = navigationRef.current?.getState(); if (navContainsProtectedRoutes(currentState)) { resolve(); return; } - let unsubscribe; - const handleStateChange = ({data}) => { - const state = lodashGet(data, 'state'); + + const unsubscribe = navigationRef.current?.addListener('state', ({data}) => { + const state = data?.state; if (navContainsProtectedRoutes(state)) { - unsubscribe(); + unsubscribe?.(); resolve(); } - }; - unsubscribe = navigationRef.current.addListener('state', handleStateChange); + }); }); }); } export default { setShouldPopAllStateOnUP, - canNavigate, navigate, setParams, dismissModal, @@ -371,7 +341,6 @@ export default { getRouteNameFromStateEvent, getTopmostReportActionId, waitForProtectedRoutes, - navContainsProtectedRoutes, }; export {navigationRef}; diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.tsx similarity index 68% rename from src/libs/Navigation/NavigationRoot.js rename to src/libs/Navigation/NavigationRoot.tsx index 2373066cf4bd..b498bcdfdf4d 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.tsx @@ -1,7 +1,7 @@ -import {DefaultTheme, getPathFromState, NavigationContainer} from '@react-navigation/native'; -import PropTypes from 'prop-types'; +import {DefaultTheme, getPathFromState, NavigationContainer, NavigationState} from '@react-navigation/native'; import React, {useEffect, useRef} from 'react'; -import {Easing, interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated'; +import {ColorValue} from 'react-native'; +import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated'; import useCurrentReportID from '@hooks/useCurrentReportID'; import useFlipper from '@hooks/useFlipper'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -21,19 +21,18 @@ const navigationTheme = { }, }; -const propTypes = { +type NavigationRootProps = { /** Whether the current user is logged in with an authToken */ - authenticated: PropTypes.bool.isRequired, + authenticated: boolean; /** Fired when react-navigation is ready */ - onReady: PropTypes.func.isRequired, + onReady: () => void; }; /** * Intercept navigation state changes and log it - * @param {NavigationState} state */ -function parseAndLogRoute(state) { +function parseAndLogRoute(state: NavigationState) { if (!state) { return; } @@ -50,11 +49,11 @@ function parseAndLogRoute(state) { Navigation.setIsNavigationReady(); } -function NavigationRoot(props) { +function NavigationRoot({authenticated, onReady}: NavigationRootProps) { useFlipper(navigationRef); const firstRenderRef = useRef(true); - const {updateCurrentReportID} = useCurrentReportID(); + const currentReportIDValue = useCurrentReportID(); const {isSmallScreenWidth} = useWindowDimensions(); useEffect(() => { @@ -72,24 +71,24 @@ function NavigationRoot(props) { }, [isSmallScreenWidth]); useEffect(() => { - if (!navigationRef.isReady() || !props.authenticated) { + if (!navigationRef.isReady() || !authenticated) { return; } // We need to force state rehydration so the CustomRouter can add the CentralPaneNavigator route if necessary. navigationRef.resetRoot(navigationRef.getRootState()); - }, [isSmallScreenWidth, props.authenticated]); + }, [isSmallScreenWidth, authenticated]); const prevStatusBarBackgroundColor = useRef(themeColors.appBG); const statusBarBackgroundColor = useRef(themeColors.appBG); const statusBarAnimation = useSharedValue(0); - const updateStatusBarBackgroundColor = (color) => StatusBar.setBackgroundColor(color); + const updateStatusBarBackgroundColor = (color: ColorValue) => StatusBar.setBackgroundColor(color); useAnimatedReaction( () => statusBarAnimation.value, (current, previous) => { // Do not run if either of the animated value is null // or previous animated value is greater than or equal to the current one - if ([current, previous].includes(null) || current <= previous) { + if (previous === null || current === null || current <= previous) { return; } const color = interpolateColor(statusBarAnimation.value, [0, 1], [prevStatusBarBackgroundColor.current, statusBarBackgroundColor.current]); @@ -99,7 +98,14 @@ function NavigationRoot(props) { const animateStatusBarBackgroundColor = () => { const currentRoute = navigationRef.getCurrentRoute(); - const currentScreenBackgroundColor = (currentRoute.params && currentRoute.params.backgroundColor) || themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG; + + const backgroundColorFromRoute = + currentRoute?.params && 'backgroundColor' in currentRoute.params && typeof currentRoute.params.backgroundColor === 'string' && currentRoute.params.backgroundColor; + const backgroundColorFallback = themeColors.PAGE_BACKGROUND_COLORS[currentRoute?.name as keyof typeof themeColors.PAGE_BACKGROUND_COLORS] || themeColors.appBG; + + // It's possible for backgroundColorFromRoute to be empty string, so we must use "||" to fallback to backgroundColorFallback. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const currentScreenBackgroundColor = backgroundColorFromRoute || backgroundColorFallback; prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current; statusBarBackgroundColor.current = currentScreenBackgroundColor; @@ -109,22 +115,17 @@ function NavigationRoot(props) { } statusBarAnimation.value = 0; - statusBarAnimation.value = withDelay( - 300, - withTiming(1, { - duration: 300, - easing: Easing.in, - }), - ); + statusBarAnimation.value = withDelay(300, withTiming(1)); }; - const handleStateChange = (state) => { + const handleStateChange = (state: NavigationState | undefined) => { if (!state) { return; } + // Performance optimization to avoid context consumers to delay first render setTimeout(() => { - updateCurrentReportID(state); + currentReportIDValue?.updateCurrentReportID(state); }, 0); parseAndLogRoute(state); animateStatusBarBackgroundColor(); @@ -133,7 +134,7 @@ function NavigationRoot(props) { return ( - + ); } NavigationRoot.displayName = 'NavigationRoot'; -NavigationRoot.propTypes = propTypes; + export default NavigationRoot; diff --git a/src/libs/Navigation/OnyxTabNavigator.js b/src/libs/Navigation/OnyxTabNavigator.tsx similarity index 54% rename from src/libs/Navigation/OnyxTabNavigator.js rename to src/libs/Navigation/OnyxTabNavigator.tsx index eeed3e0cd270..1ea57e773323 100644 --- a/src/libs/Navigation/OnyxTabNavigator.js +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -1,31 +1,33 @@ -import {createMaterialTopTabNavigator} from '@react-navigation/material-top-tabs'; -import PropTypes from 'prop-types'; +import {createMaterialTopTabNavigator, MaterialTopTabNavigationEventMap} from '@react-navigation/material-top-tabs'; +import {EventMapCore, NavigationState, ScreenListeners} from '@react-navigation/native'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry} from 'react-native-onyx/lib/types'; import Tab from '@userActions/Tab'; import ONYXKEYS from '@src/ONYXKEYS'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; -const propTypes = { - /** ID of the tab component to be saved in onyx */ - id: PropTypes.string.isRequired, +type OnyxTabNavigatorOnyxProps = { + selectedTab: OnyxEntry; +}; - /** Name of the selected tab */ - selectedTab: PropTypes.string, +type OnyxTabNavigatorProps = OnyxTabNavigatorOnyxProps & + ChildrenProps & { + /** ID of the tab component to be saved in onyx */ + id: string; - /** Children nodes */ - children: PropTypes.node.isRequired, -}; + /** Name of the selected tab */ + selectedTab?: string; -const defaultProps = { - selectedTab: '', -}; + screenListeners?: ScreenListeners; + }; // eslint-disable-next-line rulesdir/no-inline-named-export export const TopTab = createMaterialTopTabNavigator(); // This takes all the same props as MaterialTopTabsNavigator: https://reactnavigation.org/docs/material-top-tab-navigator/#props, // except ID is now required, and it gets a `selectedTab` from Onyx -function OnyxTabNavigator({id, selectedTab, children, ...rest}) { +function OnyxTabNavigator({id, selectedTab = '', children, screenListeners, ...rest}: OnyxTabNavigatorProps) { return ( { + state: (e) => { + const event = e as unknown as EventMapCore['state']; const state = event.data.state; const index = state.index; const routeNames = state.routeNames; Tab.setSelectedTab(id, routeNames[index]); }, - ...(rest.screenListeners || {}), + ...(screenListeners ?? {}), }} > {children} @@ -49,11 +52,9 @@ function OnyxTabNavigator({id, selectedTab, children, ...rest}) { ); } -OnyxTabNavigator.defaultProps = defaultProps; -OnyxTabNavigator.propTypes = propTypes; OnyxTabNavigator.displayName = 'OnyxTabNavigator'; -export default withOnyx({ +export default withOnyx({ selectedTab: { key: ({id}) => `${ONYXKEYS.COLLECTION.SELECTED_TAB}${id}`, }, diff --git a/src/libs/Navigation/getStateFromPath.js b/src/libs/Navigation/getStateFromPath.ts similarity index 57% rename from src/libs/Navigation/getStateFromPath.js rename to src/libs/Navigation/getStateFromPath.ts index f2564c9d2512..3a53b02fc3c7 100644 --- a/src/libs/Navigation/getStateFromPath.js +++ b/src/libs/Navigation/getStateFromPath.ts @@ -1,11 +1,12 @@ -import {getStateFromPath as RNGetStateFromPath} from '@react-navigation/native'; +import {NavigationState, PartialState, getStateFromPath as RNGetStateFromPath} from '@react-navigation/native'; +import {Route} from '@src/ROUTES'; import linkingConfig from './linkingConfig'; /** - * @param {String} path - The path to parse - * @returns {Object | undefined} - It's possible that there is no navigation action for the given path + * @param path - The path to parse + * @returns - It's possible that there is no navigation action for the given path */ -function getStateFromPath(path) { +function getStateFromPath(path: Route): PartialState { const normalizedPath = !path.startsWith('/') ? `/${path}` : path; const state = linkingConfig.getStateFromPath ? linkingConfig.getStateFromPath(normalizedPath, linkingConfig.config) : RNGetStateFromPath(normalizedPath, linkingConfig.config); @@ -13,6 +14,7 @@ function getStateFromPath(path) { if (!state) { throw new Error('Failed to parse the path to a navigation state.'); } + return state; } diff --git a/src/libs/Navigation/getTopmostReportActionID.js b/src/libs/Navigation/getTopmostReportActionID.js deleted file mode 100644 index a4480931cda0..000000000000 --- a/src/libs/Navigation/getTopmostReportActionID.js +++ /dev/null @@ -1,42 +0,0 @@ -import lodashFindLast from 'lodash/findLast'; -import lodashGet from 'lodash/get'; - -// This function is in a separate file than Navigation.js to avoid cyclic dependency. - -/** - * Find the last visited report screen in the navigation state and get the linked reportActionID of it. - * - * @param {Object} state - The react-navigation state - * @returns {String | undefined} - It's possible that there is no report screen - */ -function getTopmostReportActionID(state) { - if (!state) { - return; - } - const topmostCentralPane = lodashFindLast(state.routes, (route) => route.name === 'CentralPaneNavigator'); - - if (!topmostCentralPane) { - return; - } - - const directReportActionIDParam = lodashGet(topmostCentralPane, 'params.params.reportActionID'); - - if (!topmostCentralPane.state && !directReportActionIDParam) { - return; - } - - if (directReportActionIDParam) { - return directReportActionIDParam; - } - - const topmostReport = lodashFindLast(topmostCentralPane.state.routes, (route) => route.name === 'Report'); - if (!topmostReport) { - return; - } - - const topmostReportActionID = lodashGet(topmostReport, 'params.reportActionID'); - - return topmostReportActionID; -} - -export default getTopmostReportActionID; diff --git a/src/libs/Navigation/getTopmostReportActionID.ts b/src/libs/Navigation/getTopmostReportActionID.ts new file mode 100644 index 000000000000..15ab1efef704 --- /dev/null +++ b/src/libs/Navigation/getTopmostReportActionID.ts @@ -0,0 +1,48 @@ +import {NavigationState, PartialState} from '@react-navigation/native'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; +import {RootStackParamList} from './types'; + +// This function is in a separate file than Navigation.js to avoid cyclic dependency. + +/** + * Find the last visited report screen in the navigation state and get the linked reportActionID of it. + * + * @param state - The react-navigation state + * @returns - It's possible that there is no report screen + */ +function getTopmostReportActionID(state: NavigationState | NavigationState | PartialState): string | undefined { + if (!state) { + return; + } + + const topmostCentralPane = state.routes.filter((route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR).at(-1); + if (!topmostCentralPane) { + return; + } + + const directReportParams = topmostCentralPane.params && 'params' in topmostCentralPane.params && topmostCentralPane?.params?.params; + const directReportActionIDParam = directReportParams && 'reportActionID' in directReportParams && directReportParams?.reportActionID; + + if (!topmostCentralPane.state && !directReportActionIDParam) { + return; + } + + if (directReportActionIDParam) { + return directReportActionIDParam; + } + + const topmostReport = topmostCentralPane.state?.routes.filter((route) => route.name === SCREENS.REPORT).at(-1); + if (!topmostReport) { + return; + } + + const topmostReportActionID = topmostReport.params && 'reportActionID' in topmostReport.params && topmostReport.params?.reportActionID; + if (typeof topmostReportActionID !== 'string') { + return; + } + + return topmostReportActionID; +} + +export default getTopmostReportActionID; diff --git a/src/libs/Navigation/getTopmostReportId.js b/src/libs/Navigation/getTopmostReportId.js deleted file mode 100644 index 8ca9c39baf6a..000000000000 --- a/src/libs/Navigation/getTopmostReportId.js +++ /dev/null @@ -1,42 +0,0 @@ -import lodashFindLast from 'lodash/findLast'; -import lodashGet from 'lodash/get'; - -// This function is in a separate file than Navigation.js to avoid cyclic dependency. - -/** - * Find the last visited report screen in the navigation state and get the id of it. - * - * @param {Object} state - The react-navigation state - * @returns {String | undefined} - It's possible that there is no report screen - */ -function getTopmostReportId(state) { - if (!state) { - return; - } - const topmostCentralPane = lodashFindLast(state.routes, (route) => route.name === 'CentralPaneNavigator'); - - if (!topmostCentralPane) { - return; - } - - const directReportIdParam = lodashGet(topmostCentralPane, 'params.params.reportID'); - - if (!topmostCentralPane.state && !directReportIdParam) { - return; - } - - if (directReportIdParam) { - return directReportIdParam; - } - - const topmostReport = lodashFindLast(topmostCentralPane.state.routes, (route) => route.name === 'Report'); - if (!topmostReport) { - return; - } - - const topmostReportId = lodashGet(topmostReport, 'params.reportID'); - - return topmostReportId; -} - -export default getTopmostReportId; diff --git a/src/libs/Navigation/getTopmostReportId.ts b/src/libs/Navigation/getTopmostReportId.ts new file mode 100644 index 000000000000..3342761e7ccf --- /dev/null +++ b/src/libs/Navigation/getTopmostReportId.ts @@ -0,0 +1,48 @@ +import {NavigationState, PartialState} from '@react-navigation/native'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; +import {RootStackParamList} from './types'; + +// This function is in a separate file than Navigation.js to avoid cyclic dependency. + +/** + * Find the last visited report screen in the navigation state and get the id of it. + * + * @param state - The react-navigation state + * @returns - It's possible that there is no report screen + */ +function getTopmostReportId(state: NavigationState | NavigationState | PartialState): string | undefined { + if (!state) { + return; + } + + const topmostCentralPane = state.routes.filter((route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR).at(-1); + if (!topmostCentralPane) { + return; + } + + const directReportParams = topmostCentralPane.params && 'params' in topmostCentralPane.params && topmostCentralPane?.params?.params; + const directReportIdParam = directReportParams && 'reportID' in directReportParams && directReportParams?.reportID; + + if (!topmostCentralPane.state && !directReportIdParam) { + return; + } + + if (directReportIdParam) { + return directReportIdParam; + } + + const topmostReport = topmostCentralPane.state?.routes.filter((route) => route.name === SCREENS.REPORT).at(-1); + if (!topmostReport) { + return; + } + + const topmostReportId = topmostReport.params && 'reportID' in topmostReport.params && topmostReport.params?.reportID; + if (typeof topmostReportId !== 'string') { + return; + } + + return topmostReportId; +} + +export default getTopmostReportId; diff --git a/src/libs/Navigation/linkTo.js b/src/libs/Navigation/linkTo.ts similarity index 60% rename from src/libs/Navigation/linkTo.js rename to src/libs/Navigation/linkTo.ts index ca87a0d7b79c..1a4aa2d0cfb7 100644 --- a/src/libs/Navigation/linkTo.js +++ b/src/libs/Navigation/linkTo.ts @@ -1,39 +1,53 @@ import {getActionFromState} from '@react-navigation/core'; -import _ from 'lodash'; +import {NavigationAction, NavigationContainerRef, NavigationState, PartialState} from '@react-navigation/native'; +import {Writable} from 'type-fest'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; +import {Route} from '@src/ROUTES'; import getStateFromPath from './getStateFromPath'; import getTopmostReportId from './getTopmostReportId'; import linkingConfig from './linkingConfig'; +import {NavigationRoot, RootStackParamList, StackNavigationAction} from './types'; + +type ActionPayloadParams = { + screen?: string; + params?: unknown; + path?: string; +}; + +type ActionPayload = { + params?: ActionPayloadParams; +}; /** * Motivation for this function is described in NAVIGATION.md * - * @param {Object} action action generated by getActionFromState - * @param {Object} state The root state - * @returns {Object} minimalAction minimal action is the action that we should dispatch + * @param action action generated by getActionFromState + * @param state The root state + * @returns minimalAction minimal action is the action that we should dispatch */ -function getMinimalAction(action, state) { - let currentAction = action; - let currentState = state; - let currentTargetKey = null; +function getMinimalAction(action: NavigationAction, state: NavigationState): Writable { + let currentAction: NavigationAction = action; + let currentState: NavigationState | PartialState | undefined = state; + let currentTargetKey: string | undefined; - while (currentState.routes[currentState.index].name === currentAction.payload.name) { - if (!currentState.routes[currentState.index].state) { + while (currentAction.payload && 'name' in currentAction.payload && currentState?.routes[currentState.index ?? -1].name === currentAction.payload.name) { + if (!currentState?.routes[currentState.index ?? -1].state) { break; } - currentState = currentState.routes[currentState.index].state; + currentState = currentState?.routes[currentState.index ?? -1].state; + currentTargetKey = currentState?.key; - currentTargetKey = currentState.key; + const payload = currentAction.payload as ActionPayload; // Creating new smaller action currentAction = { type: currentAction.type, payload: { - name: currentAction.payload.params.screen, - params: currentAction.payload.params.params, - path: currentAction.payload.params.path, + name: payload?.params?.screen, + params: payload?.params?.params, + path: payload?.params?.path, }, target: currentTargetKey, }; @@ -41,13 +55,13 @@ function getMinimalAction(action, state) { return currentAction; } -export default function linkTo(navigation, path, type, isActiveRoute) { - if (navigation === undefined) { +export default function linkTo(navigation: NavigationContainerRef | null, path: Route, type?: string, isActiveRoute?: boolean) { + if (!navigation) { throw new Error("Couldn't find a navigation object. Is your component inside a screen in a navigator?"); } - let root = navigation; - let current; + let root: NavigationRoot = navigation; + let current: NavigationRoot | undefined; // Traverse up to get the root navigation // eslint-disable-next-line no-cond-assign @@ -55,18 +69,18 @@ export default function linkTo(navigation, path, type, isActiveRoute) { root = current; } + const rootState = root.getState(); const state = getStateFromPath(path); - - const action = getActionFromState(state, linkingConfig.config); + const action: StackNavigationAction = getActionFromState(state, linkingConfig.config); // If action type is different than NAVIGATE we can't change it to the PUSH safely - if (action.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) { + if (action?.type === CONST.NAVIGATION.ACTION_TYPE.NAVIGATE) { // In case if type is 'FORCED_UP' we replace current screen with the provided. This means the current screen no longer exists in the stack if (type === CONST.NAVIGATION.TYPE.FORCED_UP) { action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; // If this action is navigating to the report screen and the top most navigator is different from the one we want to navigate - PUSH the new screen to the top of the stack - } else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(root.getState()) !== getTopmostReportId(state)) { + } else if (action.payload.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR && getTopmostReportId(rootState) !== getTopmostReportId(state)) { action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; // If the type is UP, we deeplinked into one of the RHP flows and we want to replace the current screen with the previous one in the flow @@ -75,12 +89,12 @@ export default function linkTo(navigation, path, type, isActiveRoute) { action.type = CONST.NAVIGATION.ACTION_TYPE.REPLACE; // If this action is navigating to the RightModalNavigator and the last route on the root navigator is not RightModalNavigator then push - } else if (action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && _.last(root.getState().routes).name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + } else if (action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR && rootState.routes.at(-1)?.name !== NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { action.type = CONST.NAVIGATION.ACTION_TYPE.PUSH; } } - if (action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + if (action && 'payload' in action && action.payload && 'name' in action.payload && action.payload.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { const minimalAction = getMinimalAction(action, navigation.getRootState()); if (minimalAction) { // There are situations where a route already exists on the current navigation stack diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.ts similarity index 97% rename from src/libs/Navigation/linkingConfig.js rename to src/libs/Navigation/linkingConfig.ts index e0ac35c957a3..ae48d8e49201 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.ts @@ -1,21 +1,24 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import {LinkingOptions} from '@react-navigation/native'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import {RootStackParamList} from './types'; -export default { +const linkingConfig: LinkingOptions = { prefixes: ['new-expensify://', 'https://www.expensify.cash', 'https://staging.expensify.cash', 'https://dev.new.expensify.com', CONST.NEW_EXPENSIFY_URL, CONST.STAGING_NEW_EXPENSIFY_URL], config: { initialRouteName: SCREENS.HOME, screens: { // Main Routes - ValidateLogin: ROUTES.VALIDATE_LOGIN, - UnlinkLogin: ROUTES.UNLINK_LOGIN, + [SCREENS.VALIDATE_LOGIN]: ROUTES.VALIDATE_LOGIN, + [SCREENS.UNLINK_LOGIN]: ROUTES.UNLINK_LOGIN, [SCREENS.TRANSITION_BETWEEN_APPS]: ROUTES.TRANSITION_BETWEEN_APPS, [SCREENS.CONCIERGE]: ROUTES.CONCIERGE, - AppleSignInDesktop: ROUTES.APPLE_SIGN_IN, - GoogleSignInDesktop: ROUTES.GOOGLE_SIGN_IN, - SAMLSignIn: ROUTES.SAML_SIGN_IN, + [SCREENS.SIGN_IN_WITH_APPLE_DESKTOP]: ROUTES.APPLE_SIGN_IN, + [SCREENS.SIGN_IN_WITH_GOOGLE_DESKTOP]: ROUTES.GOOGLE_SIGN_IN, + [SCREENS.SAML_SIGN_IN]: ROUTES.SAML_SIGN_IN, [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: ROUTES.DESKTOP_SIGN_IN_REDIRECT, [SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS.route, @@ -448,3 +451,5 @@ export default { }, }, }; + +export default linkingConfig; diff --git a/src/libs/Navigation/navigationRef.js b/src/libs/Navigation/navigationRef.js deleted file mode 100644 index 00c98d178f7e..000000000000 --- a/src/libs/Navigation/navigationRef.js +++ /dev/null @@ -1,4 +0,0 @@ -import {createNavigationContainerRef} from '@react-navigation/native'; - -const navigationRef = createNavigationContainerRef(); -export default navigationRef; diff --git a/src/libs/Navigation/navigationRef.ts b/src/libs/Navigation/navigationRef.ts new file mode 100644 index 000000000000..032d9f9f3d9a --- /dev/null +++ b/src/libs/Navigation/navigationRef.ts @@ -0,0 +1,6 @@ +import {createNavigationContainerRef} from '@react-navigation/native'; +import {NavigationRef} from './types'; + +const navigationRef: NavigationRef = createNavigationContainerRef(); + +export default navigationRef; diff --git a/src/libs/Navigation/shouldPreventDeeplinkPrompt/index.js b/src/libs/Navigation/shouldPreventDeeplinkPrompt.ts similarity index 57% rename from src/libs/Navigation/shouldPreventDeeplinkPrompt/index.js rename to src/libs/Navigation/shouldPreventDeeplinkPrompt.ts index 23f46cb9808f..2b19da1f5224 100644 --- a/src/libs/Navigation/shouldPreventDeeplinkPrompt/index.js +++ b/src/libs/Navigation/shouldPreventDeeplinkPrompt.ts @@ -2,12 +2,9 @@ import CONST from '@src/CONST'; /** * Determines if the deeplink prompt should be shown on the current screen - * @param {String} screenName - * @param {Boolean} isAuthenticated - * @returns {Boolean} */ -export default function shouldPreventDeeplinkPrompt(screenName) { +export default function shouldPreventDeeplinkPrompt(screenName: string): boolean { // We don't want to show the deeplink prompt on screens where a user is in the // authentication process, so we are blocking the prompt on the following screens (Denylist) - return CONST.DEEPLINK_PROMPT_DENYLIST.includes(screenName); + return CONST.DEEPLINK_PROMPT_DENYLIST.some((name) => name === screenName); } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts new file mode 100644 index 000000000000..41df21d8e237 --- /dev/null +++ b/src/libs/Navigation/types.ts @@ -0,0 +1,401 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import {CommonActions, NavigationContainerRefWithCurrent, NavigationHelpers, NavigationState, NavigatorScreenParams, PartialRoute, Route} from '@react-navigation/native'; +import {ValueOf} from 'type-fest'; +import CONST from '@src/CONST'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; + +type NavigationRef = NavigationContainerRefWithCurrent; + +type NavigationRoot = NavigationHelpers; + +type GoBackAction = Extract; +type ResetAction = Extract; +type SetParamsAction = Extract; + +type ActionNavigate = { + type: ValueOf; + payload: { + name?: string; + key?: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + params?: any; + path?: string; + merge?: boolean; + }; + source?: string; + target?: string; +}; + +type StackNavigationAction = GoBackAction | ResetAction | SetParamsAction | ActionNavigate | undefined; + +type NavigationStateRoute = NavigationState['routes'][number]; +type NavigationPartialRoute = PartialRoute>; +type StateOrRoute = NavigationState | NavigationStateRoute | NavigationPartialRoute; + +type CentralPaneNavigatorParamList = { + [SCREENS.REPORT]: { + reportActionID: string; + reportID: string; + }; +}; + +type SettingsNavigatorParamList = { + [SCREENS.SETTINGS.ROOT]: undefined; + Settings_Share_Code: undefined; + [SCREENS.SETTINGS.WORKSPACES]: undefined; + Settings_Profile: undefined; + Settings_Pronouns: undefined; + Settings_Display_Name: undefined; + Settings_Timezone: undefined; + Settings_Timezone_Select: undefined; + Settings_PersonalDetails_Initial: undefined; + Settings_PersonalDetails_LegalName: undefined; + Settings_PersonalDetails_DateOfBirth: undefined; + Settings_PersonalDetails_Address: undefined; + Settings_PersonalDetails_Address_Country: undefined; + Settings_ContactMethods: undefined; + Settings_ContactMethodDetails: undefined; + Settings_NewContactMethod: undefined; + [SCREENS.SETTINGS.PREFERENCES]: undefined; + Settings_Preferences_PriorityMode: undefined; + Settings_Preferences_Language: undefined; + Settings_Preferences_Theme: undefined; + Settings_Close: undefined; + [SCREENS.SETTINGS.SECURITY]: undefined; + Settings_About: undefined; + Settings_App_Download_Links: undefined; + Settings_Lounge_Access: undefined; + Settings_Wallet: undefined; + Settings_Wallet_Cards_Digital_Details_Update_Address: undefined; + Settings_Wallet_DomainCard: undefined; + Settings_Wallet_ReportVirtualCardFraud: undefined; + Settings_Wallet_Card_Activate: undefined; + [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.NAME]: undefined; + [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.PHONE]: undefined; + [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.ADDRESS]: undefined; + [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.CONFIRM]: undefined; + Settings_Wallet_Transfer_Balance: undefined; + Settings_Wallet_Choose_Transfer_Account: undefined; + Settings_Wallet_EnablePayments: undefined; + Settings_Add_Debit_Card: undefined; + Settings_Add_Bank_Account: undefined; + [SCREENS.SETTINGS.STATUS]: undefined; + Settings_Status_Set: undefined; + Workspace_Initial: undefined; + Workspace_Settings: undefined; + Workspace_Settings_Currency: undefined; + Workspace_Card: { + policyID: string; + }; + Workspace_Reimburse: { + policyID: string; + }; + Workspace_RateAndUnit: undefined; + Workspace_Bills: { + policyID: string; + }; + Workspace_Invoices: { + policyID: string; + }; + Workspace_Travel: { + policyID: string; + }; + Workspace_Members: { + policyID: string; + }; + Workspace_Invite: { + policyID: string; + }; + Workspace_Invite_Message: { + policyID: string; + }; + ReimbursementAccount: { + stepToOpen: string; + policyID: string; + }; + GetAssistance: { + taskID: string; + }; + Settings_TwoFactorAuth: undefined; + Settings_ReportCardLostOrDamaged: undefined; + KeyboardShortcuts: undefined; +}; + +type NewChatNavigatorParamList = { + NewChat_Root: undefined; +}; + +type SearchNavigatorParamList = { + Search_Root: undefined; +}; + +type DetailsNavigatorParamList = { + Details_Root: { + login: string; + reportID: string; + }; +}; + +type ProfileNavigatorParamList = { + Profile_Root: { + accountID: string; + reportID: string; + }; +}; + +type ReportDetailsNavigatorParamList = { + Report_Details_Root: undefined; + Report_Details_Share_Code: { + reportID: string; + }; +}; + +type ReportSettingsNavigatorParamList = { + Report_Settings_Root: undefined; + Report_Settings_Room_Name: undefined; + Report_Settings_Notification_Preferences: undefined; + Report_Settings_Write_Capability: undefined; +}; + +type ReportWelcomeMessageNavigatorParamList = { + Report_WelcomeMessage_Root: {reportID: string}; +}; + +type ParticipantsNavigatorParamList = { + ReportParticipants_Root: {reportID: string}; +}; + +type RoomMembersNavigatorParamList = { + RoomMembers_Root: undefined; +}; + +type RoomInviteNavigatorParamList = { + RoomInvite_Root: undefined; +}; + +type MoneyRequestNavigatorParamList = { + Money_Request: undefined; + Money_Request_Amount: undefined; + Money_Request_Participants: { + iouType: string; + reportID: string; + }; + Money_Request_Confirmation: { + iouType: string; + reportID: string; + }; + Money_Request_Currency: { + iouType: string; + reportID: string; + currency: string; + backTo: string; + }; + Money_Request_Date: { + iouType: string; + reportID: string; + field: string; + threadReportID: string; + }; + Money_Request_Description: { + iouType: string; + reportID: string; + field: string; + threadReportID: string; + }; + Money_Request_Category: { + iouType: string; + reportID: string; + }; + Money_Request_Tag: { + iouType: string; + reportID: string; + }; + Money_Request_Merchant: { + iouType: string; + reportID: string; + field: string; + threadReportID: string; + }; + IOU_Send_Enable_Payments: undefined; + IOU_Send_Add_Bank_Account: undefined; + IOU_Send_Add_Debit_Card: undefined; + Money_Request_Waypoint: { + iouType: string; + transactionID: string; + waypointIndex: string; + threadReportID: number; + }; + Money_Request_Edit_Waypoint: { + iouType: string; + transactionID: string; + waypointIndex: string; + threadReportID: number; + }; + Money_Request_Distance: { + iouType: ValueOf; + reportID: string; + }; + Money_Request_Receipt: { + iouType: string; + reportID: string; + }; +}; + +type NewTaskNavigatorParamList = { + NewTask_Root: undefined; + NewTask_TaskAssigneeSelector: undefined; + NewTask_TaskShareDestinationSelector: undefined; + NewTask_Details: undefined; + NewTask_Title: undefined; + NewTask_Description: undefined; +}; + +type TeachersUniteNavigatorParamList = { + [SCREENS.SAVE_THE_WORLD.ROOT]: undefined; + I_Know_A_Teacher: undefined; + Intro_School_Principal: undefined; + I_Am_A_Teacher: undefined; +}; + +type TaskDetailsNavigatorParamList = { + Task_Title: undefined; + Task_Description: undefined; + Task_Assignee: { + reportID: string; + }; +}; + +type EnablePaymentsNavigatorParamList = { + EnablePayments_Root: undefined; +}; + +type SplitDetailsNavigatorParamList = { + SplitDetails_Root: { + reportActionID: string; + }; + SplitDetails_Edit_Request: undefined; + SplitDetails_Edit_Currency: undefined; +}; + +type AddPersonalBankAccountNavigatorParamList = { + AddPersonalBankAccount_Root: undefined; +}; + +type WalletStatementNavigatorParamList = { + WalletStatement_Root: undefined; +}; + +type FlagCommentNavigatorParamList = { + FlagComment_Root: { + reportID: string; + reportActionID: string; + }; +}; + +type EditRequestNavigatorParamList = { + EditRequest_Root: { + field: string; + threadReportID: string; + }; + EditRequest_Currency: undefined; +}; + +type SignInNavigatorParamList = { + SignIn_Root: undefined; +}; + +type ReferralDetailsNavigatorParamList = { + Referral_Details: undefined; +}; + +type PrivateNotesNavigatorParamList = { + PrivateNotes_View: { + reportID: string; + accountID: string; + }; + PrivateNotes_List: { + reportID: string; + accountID: string; + }; + PrivateNotes_Edit: { + reportID: string; + accountID: string; + }; +}; + +type RightModalNavigatorParamList = { + Settings: NavigatorScreenParams; + NewChat: NavigatorScreenParams; + Search: NavigatorScreenParams; + Details: NavigatorScreenParams; + Profile: NavigatorScreenParams; + Report_Details: NavigatorScreenParams; + Report_Settings: NavigatorScreenParams; + Report_WelcomeMessage: NavigatorScreenParams; + Participants: NavigatorScreenParams; + RoomMembers: NavigatorScreenParams; + RoomInvite: NavigatorScreenParams; + MoneyRequest: NavigatorScreenParams; + NewTask: NavigatorScreenParams; + TeachersUnite: NavigatorScreenParams; + Task_Details: NavigatorScreenParams; + EnablePayments: NavigatorScreenParams; + SplitDetails: NavigatorScreenParams; + AddPersonalBankAccount: NavigatorScreenParams; + Wallet_Statement: NavigatorScreenParams; + Flag_Comment: NavigatorScreenParams; + EditRequest: NavigatorScreenParams; + SignIn: NavigatorScreenParams; + Referral: NavigatorScreenParams; + Private_Notes: NavigatorScreenParams; +}; + +type PublicScreensParamList = { + [SCREENS.HOME]: undefined; + [SCREENS.TRANSITION_BETWEEN_APPS]: { + shouldForceLogin: string; + email: string; + shortLivedAuthToken: string; + exitTo: string; + }; + [SCREENS.VALIDATE_LOGIN]: { + accountID: string; + validateCode: string; + }; + [SCREENS.UNLINK_LOGIN]: { + accountID: string; + validateCode: string; + }; + [SCREENS.SIGN_IN_WITH_APPLE_DESKTOP]: undefined; + [SCREENS.SIGN_IN_WITH_GOOGLE_DESKTOP]: undefined; + [SCREENS.SAML_SIGN_IN]: undefined; +}; + +type AuthScreensParamList = { + [SCREENS.HOME]: undefined; + [NAVIGATORS.CENTRAL_PANE_NAVIGATOR]: NavigatorScreenParams; + [SCREENS.VALIDATE_LOGIN]: { + accountID: string; + validateCode: string; + }; + [SCREENS.TRANSITION_BETWEEN_APPS]: { + shouldForceLogin: string; + email: string; + shortLivedAuthToken: string; + exitTo: string; + }; + [SCREENS.CONCIERGE]: undefined; + [SCREENS.REPORT_ATTACHMENTS]: { + reportID: string; + source: string; + }; + [SCREENS.NOT_FOUND]: undefined; + [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams; + [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined; + [CONST.DEMO_PAGES.MONEY2020]: undefined; +}; + +type RootStackParamList = PublicScreensParamList & AuthScreensParamList; + +export type {NavigationRef, StackNavigationAction, CentralPaneNavigatorParamList, RootStackParamList, StateOrRoute, NavigationStateRoute, NavigationRoot}; diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index accf84002bd3..4de8f1c1f171 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -13,7 +13,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as SessionUtils from '@libs/SessionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; +import ROUTES, {Route} from '@src/ROUTES'; import * as OnyxTypes from '@src/types/onyx'; import {SelectedTimezone} from '@src/types/onyx/PersonalDetails'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -392,7 +392,7 @@ function setUpPoliciesAndNavigate(session: OnyxTypes.Session) { const isLoggingInAsNewUser = !!session.email && SessionUtils.isLoggingInAsNewUser(currentUrl, session.email); const url = new URL(currentUrl); - const exitTo = url.searchParams.get('exitTo'); + const exitTo = url.searchParams.get('exitTo') as Route | null; // Approved Accountants and Guides can enter a flow where they make a workspace for other users, // and those are passed as a search parameter when using transition links diff --git a/src/types/modules/react-navigation.d.ts b/src/types/modules/react-navigation.d.ts new file mode 100644 index 000000000000..1ac35c937116 --- /dev/null +++ b/src/types/modules/react-navigation.d.ts @@ -0,0 +1,8 @@ +import {RootStackParamList} from '@libs/Navigation/types'; + +declare global { + namespace ReactNavigation { + // eslint-disable-next-line + interface RootParamList extends RootStackParamList {} + } +}