diff --git a/__mocks__/@ua/react-native-airship.js b/__mocks__/@ua/react-native-airship.js index 1672c064f9be..29be662e96a1 100644 --- a/__mocks__/@ua/react-native-airship.js +++ b/__mocks__/@ua/react-native-airship.js @@ -31,7 +31,7 @@ const Airship = { }, contact: { identify: jest.fn(), - getNamedUserId: jest.fn(), + getNamedUserId: () => Promise.resolve(undefined), reset: jest.fn(), }, }; diff --git a/contributingGuides/NAVIGATION.md b/contributingGuides/NAVIGATION.md index 32d3919efbe4..8467b97c29fb 100644 --- a/contributingGuides/NAVIGATION.md +++ b/contributingGuides/NAVIGATION.md @@ -1,6 +1,6 @@ # Overview -The navigation in the App consists of a top-level Stack Navigator (called `RootStack`) with each of its `Screen` components handling different high-level flow. All those flows can be seen in `AuthScreens.js` file. +The navigation in the App consists of a top-level Stack Navigator (called `RootStack`) with each of its `Screen` components handling different high-level flow. All those flows can be seen in `AuthScreens.tsx` file. ## Terminology @@ -20,11 +20,11 @@ Navigation Actions - User actions correspond to resulting navigation actions tha ## Adding RHP flows -Most of the time, if you want to add some of the flows concerning one of your reports, e.g. `Money Request` from a user, you will most probably use `RightModalNavigator.js` and `ModalStackNavigators.js` file: +Most of the time, if you want to add some of the flows concerning one of your reports, e.g. `Money Request` from a user, you will most probably use `RightModalNavigator.tsx` and `ModalStackNavigators.tsx` file: -- Since each of those flows is kind of a modal stack, if you want to add a page to the existing flow, you should just add a page to the correct stack in `ModalStackNavigators.js`. +- Since each of those flows is kind of a modal stack, if you want to add a page to the existing flow, you should just add a page to the correct stack in `ModalStackNavigators.tsx`. -- If you want to create new flow, add a `Screen` in `RightModalNavigator.js` and make new modal in `ModalStackNavigators.js` with chosen pages. +- If you want to create new flow, add a `Screen` in `RightModalNavigator.tsx` and make new modal in `ModalStackNavigators.tsx` with chosen pages. When creating RHP flows, you have to remember a couple things: @@ -196,4 +196,4 @@ The action for the first step created with `getMinimalAction` looks like this: ``` ### Deeplinking -There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones. \ No newline at end of file +There is no minimal action for deeplinking directly to the `Profile` screen. But because the `Settings_root` is not on the stack, pressing UP will reset the params for navigators to the correct ones. diff --git a/patches/react-native-web+0.19.9+005+image-header-support.patch b/patches/react-native-web+0.19.9+005+image-header-support.patch new file mode 100644 index 000000000000..4652e22662f0 --- /dev/null +++ b/patches/react-native-web+0.19.9+005+image-header-support.patch @@ -0,0 +1,200 @@ +diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js +index 95355d5..19109fc 100644 +--- a/node_modules/react-native-web/dist/exports/Image/index.js ++++ b/node_modules/react-native-web/dist/exports/Image/index.js +@@ -135,7 +135,22 @@ function resolveAssetUri(source) { + } + return uri; + } +-var Image = /*#__PURE__*/React.forwardRef((props, ref) => { ++function raiseOnErrorEvent(uri, _ref) { ++ var onError = _ref.onError, ++ onLoadEnd = _ref.onLoadEnd; ++ if (onError) { ++ onError({ ++ nativeEvent: { ++ error: "Failed to load resource " + uri + " (404)" ++ } ++ }); ++ } ++ if (onLoadEnd) onLoadEnd(); ++} ++function hasSourceDiff(a, b) { ++ return a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers); ++} ++var BaseImage = /*#__PURE__*/React.forwardRef((props, ref) => { + var ariaLabel = props['aria-label'], + blurRadius = props.blurRadius, + defaultSource = props.defaultSource, +@@ -236,16 +251,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { + } + }, function error() { + updateState(ERRORED); +- if (onError) { +- onError({ +- nativeEvent: { +- error: "Failed to load resource " + uri + " (404)" +- } +- }); +- } +- if (onLoadEnd) { +- onLoadEnd(); +- } ++ raiseOnErrorEvent(uri, { ++ onError, ++ onLoadEnd ++ }); + }); + } + function abortPendingRequest() { +@@ -277,10 +286,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { + suppressHydrationWarning: true + }), hiddenImage, createTintColorSVG(tintColor, filterRef.current)); + }); +-Image.displayName = 'Image'; ++BaseImage.displayName = 'Image'; ++ ++/** ++ * This component handles specifically loading an image source with headers ++ * default source is never loaded using headers ++ */ ++var ImageWithHeaders = /*#__PURE__*/React.forwardRef((props, ref) => { ++ // $FlowIgnore: This component would only be rendered when `source` matches `ImageSource` ++ var nextSource = props.source; ++ var _React$useState3 = React.useState(''), ++ blobUri = _React$useState3[0], ++ setBlobUri = _React$useState3[1]; ++ var request = React.useRef({ ++ cancel: () => {}, ++ source: { ++ uri: '', ++ headers: {} ++ }, ++ promise: Promise.resolve('') ++ }); ++ var onError = props.onError, ++ onLoadStart = props.onLoadStart, ++ onLoadEnd = props.onLoadEnd; ++ React.useEffect(() => { ++ if (!hasSourceDiff(nextSource, request.current.source)) { ++ return; ++ } ++ ++ // When source changes we want to clean up any old/running requests ++ request.current.cancel(); ++ if (onLoadStart) { ++ onLoadStart(); ++ } ++ ++ // Store a ref for the current load request so we know what's the last loaded source, ++ // and so we can cancel it if a different source is passed through props ++ request.current = ImageLoader.loadWithHeaders(nextSource); ++ request.current.promise.then(uri => setBlobUri(uri)).catch(() => raiseOnErrorEvent(request.current.source.uri, { ++ onError, ++ onLoadEnd ++ })); ++ }, [nextSource, onLoadStart, onError, onLoadEnd]); ++ ++ // Cancel any request on unmount ++ React.useEffect(() => request.current.cancel, []); ++ var propsToPass = _objectSpread(_objectSpread({}, props), {}, { ++ // `onLoadStart` is called from the current component ++ // We skip passing it down to prevent BaseImage raising it a 2nd time ++ onLoadStart: undefined, ++ // Until the current component resolves the request (using headers) ++ // we skip forwarding the source so the base component doesn't attempt ++ // to load the original source ++ source: blobUri ? _objectSpread(_objectSpread({}, nextSource), {}, { ++ uri: blobUri ++ }) : undefined ++ }); ++ return /*#__PURE__*/React.createElement(BaseImage, _extends({ ++ ref: ref ++ }, propsToPass)); ++}); + + // $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet +-var ImageWithStatics = Image; ++var ImageWithStatics = /*#__PURE__*/React.forwardRef((props, ref) => { ++ if (props.source && props.source.headers) { ++ return /*#__PURE__*/React.createElement(ImageWithHeaders, _extends({ ++ ref: ref ++ }, props)); ++ } ++ return /*#__PURE__*/React.createElement(BaseImage, _extends({ ++ ref: ref ++ }, props)); ++}); + ImageWithStatics.getSize = function (uri, success, failure) { + ImageLoader.getSize(uri, success, failure); + }; +diff --git a/node_modules/react-native-web/dist/modules/ImageLoader/index.js b/node_modules/react-native-web/dist/modules/ImageLoader/index.js +index bc06a87..e309394 100644 +--- a/node_modules/react-native-web/dist/modules/ImageLoader/index.js ++++ b/node_modules/react-native-web/dist/modules/ImageLoader/index.js +@@ -76,7 +76,7 @@ var ImageLoader = { + var image = requests["" + requestId]; + if (image) { + var naturalHeight = image.naturalHeight, +- naturalWidth = image.naturalWidth; ++ naturalWidth = image.naturalWidth; + if (naturalHeight && naturalWidth) { + success(naturalWidth, naturalHeight); + complete = true; +@@ -102,11 +102,19 @@ var ImageLoader = { + id += 1; + var image = new window.Image(); + image.onerror = onError; +- image.onload = e => { ++ image.onload = nativeEvent => { + // avoid blocking the main thread +- var onDecode = () => onLoad({ +- nativeEvent: e +- }); ++ var onDecode = () => { ++ // Append `source` to match RN's ImageLoadEvent interface ++ nativeEvent.source = { ++ uri: image.src, ++ width: image.naturalWidth, ++ height: image.naturalHeight ++ }; ++ onLoad({ ++ nativeEvent ++ }); ++ }; + if (typeof image.decode === 'function') { + // Safari currently throws exceptions when decoding svgs. + // We want to catch that error and allow the load handler +@@ -120,6 +128,32 @@ var ImageLoader = { + requests["" + id] = image; + return id; + }, ++ loadWithHeaders(source) { ++ var uri; ++ var abortController = new AbortController(); ++ var request = new Request(source.uri, { ++ headers: source.headers, ++ signal: abortController.signal ++ }); ++ request.headers.append('accept', 'image/*'); ++ var promise = fetch(request).then(response => response.blob()).then(blob => { ++ uri = URL.createObjectURL(blob); ++ return uri; ++ }).catch(error => { ++ if (error.name === 'AbortError') { ++ return ''; ++ } ++ throw error; ++ }); ++ return { ++ promise, ++ source, ++ cancel: () => { ++ abortController.abort(); ++ URL.revokeObjectURL(uri); ++ } ++ }; ++ }, + prefetch(uri) { + return new Promise((resolve, reject) => { + ImageLoader.load(uri, () => { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 1bc06e231448..9cd55b41455b 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -416,6 +416,7 @@ type OnyxValues = { [ONYXKEYS.IS_LOADING_PAYMENT_METHODS]: boolean; [ONYXKEYS.IS_LOADING_REPORT_DATA]: boolean; [ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN]: boolean; + [ONYXKEYS.IS_LOADING_APP]: boolean; [ONYXKEYS.WALLET_TRANSFER]: OnyxTypes.WalletTransfer; [ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID]: string; [ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT]: boolean; @@ -428,6 +429,7 @@ type OnyxValues = { [ONYXKEYS.MAPBOX_ACCESS_TOKEN]: OnyxTypes.MapboxAccessToken; [ONYXKEYS.ONYX_UPDATES_FROM_SERVER]: OnyxTypes.OnyxUpdatesFromServer; [ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT]: number; + [ONYXKEYS.DEMO_INFO]: OnyxTypes.DemoInfo; [ONYXKEYS.MAX_CANVAS_AREA]: number; [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; diff --git a/src/SCREENS.ts b/src/SCREENS.ts index f4cbcf4f2564..c0d3df82e228 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -2,6 +2,7 @@ * This is a file containing constants for all of the screen names. In most cases, we should use the routes for * navigation. But there are situations where we may need to access screen names directly. */ +import DeepValueOf from './types/utils/DeepValueOf'; const PROTECTED_SCREENS = { HOME: 'Home', @@ -22,6 +23,25 @@ const SCREENS = { WORKSPACES: 'Settings_Workspaces', SECURITY: 'Settings_Security', STATUS: 'Settings_Status', + PROFILE: 'Settings_Profile', + PRONOUNS: 'Settings_Pronouns', + DISPLAY_NAME: 'Settings_Display_Name', + TIMEZONE: 'Settings_Timezone', + TIMEZONE_SELECT: 'Settings_Timezone_Select', + CONTACT_METHODS: 'Settings_ContactMethods', + CONTACT_METHOD_DETAILS: 'Settings_ContactMethodDetails', + NEW_CONTACT_METHOD: 'Settings_NewContactMethod', + SHARE_CODE: 'Settings_Share_Code', + ABOUT: 'Settings_About', + APP_DOWNLOAD_LINKS: 'Settings_App_Download_Links', + LOUNGE_ACCESS: 'Settings_Lounge_Access', + + PERSONAL_DETAILS_INITIAL: 'Settings_PersonalDetails_Initial', + PERSONAL_DETAILS_LEGAL_NAME: 'Settings_PersonalDetails_LegalName', + PERSONAL_DETAILS_DATE_OF_BIRTH: 'Settings_PersonalDetails_DateOfBirth', + PERSONAL_DETAILS_ADDRESS: 'Settings_PersonalDetails_Address', + PERSONAL_DETAILS_ADDRESS_COUNTRY: 'Settings_PersonalDetails_Address_Country', + WALLET: 'Settings_Wallet', WALLET_DOMAIN_CARD: 'Settings_Wallet_DomainCard', WALLET_CARD_GET_PHYSICAL: { @@ -30,15 +50,166 @@ const SCREENS = { ADDRESS: 'Settings_Card_Get_Physical_Address', CONFIRM: 'Settings_Card_Get_Physical_Confirm', }, + WALLET_TRANSFER_BALANCE: 'Settings_Wallet_Transfer_Balance', + WALLET_CHOOSE_TRANSFER_ACCOUNT: 'Settings_Wallet_Choose_Transfer_Account', + WALLET_ENABLE_PAYMENTS: 'Settings_Wallet_EnablePayments', + WALLET_CARD_ACTIVATE: 'Settings_Wallet_Card_Activate', + WALLET_REPORT_VIRTUAL_CARD_FRAUD: 'Settings_Wallet_ReportVirtualCardFraud', + WALLET_CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS: 'Settings_Wallet_Cards_Digital_Details_Update_Address', + + ADD_DEBIT_CARD: 'Settings_Add_Debit_Card', + ADD_BANK_ACCOUNT: 'Settings_Add_Bank_Account', + PREFERENCES_PRIORITY_MODE: 'Settings_Preferences_PriorityMode', + PREFERENCES_LANGUAGE: 'Settings_Preferences_Language', + PREFERENCES_THEME: 'Settings_Preferences_Theme', + CLOSE: 'Settings_Close', + STATUS_SET: 'Settings_Status_Set', + TWO_FACTOR_AUTH: 'Settings_TwoFactorAuth', + REPORT_CARD_LOST_OR_DAMAGED: 'Settings_ReportCardLostOrDamaged', }, SAVE_THE_WORLD: { ROOT: 'SaveTheWorld_Root', }, + RIGHT_MODAL: { + SETTINGS: 'Settings', + NEW_CHAT: 'NewChat', + SEARCH: 'Search', + DETAILS: 'Details', + PROFILE: 'Profile', + REPORT_DETAILS: 'Report_Details', + REPORT_SETTINGS: 'Report_Settings', + REPORT_WELCOME_MESSAGE: 'Report_WelcomeMessage', + PARTICIPANTS: 'Participants', + MONEY_REQUEST: 'MoneyRequest', + NEW_TASK: 'NewTask', + TEACHERS_UNITE: 'TeachersUnite', + TASK_DETAILS: 'Task_Details', + ENABLE_PAYMENTS: 'EnablePayments', + SPLIT_DETAILS: 'SplitDetails', + ADD_PERSONAL_BANK_ACCOUNT: 'AddPersonalBankAccount', + WALLET_STATEMENT: 'Wallet_Statement', + FLAG_COMMENT: 'Flag_Comment', + EDIT_REQUEST: 'EditRequest', + SIGN_IN: 'SignIn', + PRIVATE_NOTES: 'Private_Notes', + ROOM_MEMBERS: 'RoomMembers', + ROOM_INVITE: 'RoomInvite', + REFERRAL: 'Referral', + }, SIGN_IN_WITH_APPLE_DESKTOP: 'AppleSignInDesktop', SIGN_IN_WITH_GOOGLE_DESKTOP: 'GoogleSignInDesktop', DESKTOP_SIGN_IN_REDIRECT: 'DesktopSignInRedirect', SAML_SIGN_IN: 'SAMLSignIn', + + MONEY_REQUEST: { + ROOT: 'Money_Request', + AMOUNT: 'Money_Request_Amount', + PARTICIPANTS: 'Money_Request_Participants', + CONFIRMATION: 'Money_Request_Confirmation', + CURRENCY: 'Money_Request_Currency', + DATE: 'Money_Request_Date', + DESCRIPTION: 'Money_Request_Description', + CATEGORY: 'Money_Request_Category', + TAG: 'Money_Request_Tag', + MERCHANT: 'Money_Request_Merchant', + WAYPOINT: 'Money_Request_Waypoint', + EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint', + DISTANCE: 'Money_Request_Distance', + RECEIPT: 'Money_Request_Receipt', + }, + + IOU_SEND: { + ADD_BANK_ACCOUNT: 'IOU_Send_Add_Bank_Account', + ADD_DEBIT_CARD: 'IOU_Send_Add_Debit_Card', + ENABLE_PAYMENTS: 'IOU_Send_Enable_Payments', + }, + + REPORT_SETTINGS: { + ROOT: 'Report_Settings_Root', + ROOM_NAME: 'Report_Settings_Room_Name', + NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences', + WRITE_CAPABILITY: 'Report_Settings_Write_Capability', + }, + + NEW_TASK: { + ROOT: 'NewTask_Root', + TASK_ASSIGNEE_SELECTOR: 'NewTask_TaskAssigneeSelector', + TASK_SHARE_DESTINATION_SELECTOR: 'NewTask_TaskShareDestinationSelector', + DETAILS: 'NewTask_Details', + TITLE: 'NewTask_Title', + DESCRIPTION: 'NewTask_Description', + }, + + TASK: { + TITLE: 'Task_Title', + DESCRIPTION: 'Task_Description', + ASSIGNEE: 'Task_Assignee', + }, + + PRIVATE_NOTES: { + VIEW: 'PrivateNotes_View', + LIST: 'PrivateNotes_List', + EDIT: 'PrivateNotes_Edit', + }, + + REPORT_DETAILS: { + ROOT: 'Report_Details_Root', + SHARE_CODE: 'Report_Details_Share_Code', + }, + + WORKSPACE: { + INITIAL: 'Workspace_Initial', + SETTINGS: 'Workspace_Settings', + CARD: 'Workspace_Card', + REIMBURSE: 'Workspace_Reimburse', + RATE_AND_UNIT: 'Workspace_RateAndUnit', + BILLS: 'Workspace_Bills', + INVOICES: 'Workspace_Invoices', + TRAVEL: 'Workspace_Travel', + MEMBERS: 'Workspace_Members', + INVITE: 'Workspace_Invite', + INVITE_MESSAGE: 'Workspace_Invite_Message', + CURRENCY: 'Workspace_Settings_Currency', + }, + + EDIT_REQUEST: { + ROOT: 'EditRequest_Root', + CURRENCY: 'EditRequest_Currency', + }, + + I_KNOW_A_TEACHER: 'I_Know_A_Teacher', + INTRO_SCHOOL_PRINCIPAL: 'Intro_School_Principal', + I_AM_A_TEACHER: 'I_Am_A_Teacher', + + ENABLE_PAYMENTS_ROOT: 'EnablePayments_Root', + ADD_PERSONAL_BANK_ACCOUNT_ROOT: 'AddPersonalBankAccount_Root', + REIMBURSEMENT_ACCOUNT_ROOT: 'Reimbursement_Account_Root', + WALLET_STATEMENT_ROOT: 'WalletStatement_Root', + SIGN_IN_ROOT: 'SignIn_Root', + DETAILS_ROOT: 'Details_Root', + PROFILE_ROOT: 'Profile_Root', + REPORT_WELCOME_MESSAGE_ROOT: 'Report_WelcomeMessage_Root', + REPORT_PARTICIPANTS_ROOT: 'ReportParticipants_Root', + ROOM_MEMBERS_ROOT: 'RoomMembers_Root', + ROOM_INVITE_ROOT: 'RoomInvite_Root', + SEARCH_ROOT: 'Search_Root', + NEW_CHAT_ROOT: 'NewChat_Root', + FLAG_COMMENT_ROOT: 'FlagComment_Root', + + SPLIT_DETAILS: { + ROOT: 'SplitDetails_Root', + EDIT_REQUEST: 'SplitDetails_Edit_Request', + EDIT_CURRENCY: 'SplitDetails_Edit_Currency', + }, + + REIMBURSEMENT_ACCOUNT: 'ReimbursementAccount', + GET_ASSISTANCE: 'GetAssistance', + REFERRAL_DETAILS: 'Referral_Details', + KEYBOARD_SHORTCUTS: 'KeyboardShortcuts', } as const; +type Screen = DeepValueOf; + export default SCREENS; export {PROTECTED_SCREENS}; +export type {Screen}; diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index c18b706e1acf..68d529c4a78d 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -168,7 +168,7 @@ function AddPlaidBankAccount({ value: account.plaidAccountID, label: `${account.addressName} ${account.mask}`, })); - const {icon, iconSize, iconStyles} = getBankIcon({themeStyles: styles}); + const {icon, iconSize, iconStyles} = getBankIcon({styles}); const plaidErrors = lodashGet(plaidData, 'errors'); const plaidDataErrorMessage = !_.isEmpty(plaidErrors) ? _.chain(plaidErrors).values().first().value() : ''; const bankName = lodashGet(plaidData, 'bankName'); diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index b6861eb820b1..9cbd19e03dc7 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -7,6 +7,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Text from '@components/Text'; import withNavigationFallback from '@components/withNavigationFallback'; +import useActiveElement from '@hooks/useActiveElement'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import HapticFeedback from '@libs/HapticFeedback'; import useTheme from '@styles/themes/useTheme'; @@ -158,6 +159,9 @@ function Button( const theme = useTheme(); const styles = useThemeStyles(); const isFocused = useIsFocused(); + const activeElement = useActiveElement(); + const accessibilityRoles: string[] = Object.values(CONST.ACCESSIBILITY_ROLE); + const shouldDisableEnterShortcut = accessibilityRoles.includes(activeElement?.role ?? '') && activeElement?.role !== CONST.ACCESSIBILITY_ROLE.TEXT; const keyboardShortcutCallback = useCallback( (event?: GestureResponderEvent | KeyboardEvent) => { @@ -170,7 +174,7 @@ function Button( ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, keyboardShortcutCallback, { - isActive: pressOnEnter, + isActive: pressOnEnter && !shouldDisableEnterShortcut, shouldBubble: allowBubble, priority: enterKeyEventListenerPriority, shouldPreventDefault: false, diff --git a/src/components/ColorSchemeWrapper/index.tsx b/src/components/ColorSchemeWrapper/index.tsx index 577ccf9f3794..2909f1ffbe9f 100644 --- a/src/components/ColorSchemeWrapper/index.tsx +++ b/src/components/ColorSchemeWrapper/index.tsx @@ -5,9 +5,9 @@ import useThemeStyles from '@styles/useThemeStyles'; function ColorSchemeWrapper({children}: React.PropsWithChildren): React.ReactElement { const theme = useTheme(); - const themeStyles = useThemeStyles(); + const styles = useThemeStyles(); - return {children}; + return {children}; } export default ColorSchemeWrapper; diff --git a/src/components/EmojiPicker/EmojiPickerMenu/index.js b/src/components/EmojiPicker/EmojiPickerMenu/index.js index 91716cd7500e..2c77b393c2b9 100755 --- a/src/components/EmojiPicker/EmojiPickerMenu/index.js +++ b/src/components/EmojiPicker/EmojiPickerMenu/index.js @@ -308,6 +308,10 @@ function EmojiPickerMenu(props) { } const emoji = lodashGet(item, ['types', preferredSkinTone], item.code); onEmojiSelected(emoji, item); + // On web, avoid this Enter default input action; otherwise, it will add a new line in the subsequently focused composer. + keyBoardEvent.preventDefault(); + // On mWeb, avoid propagating this Enter keystroke to Pressable child component; otherwise, it will trigger the onEmojiSelected callback again. + keyBoardEvent.stopPropagation(); return; } diff --git a/src/components/Icon/BankIcons.ts b/src/components/Icon/BankIcons.ts index 49ff4a64446b..5e4c0192ca86 100644 --- a/src/components/Icon/BankIcons.ts +++ b/src/components/Icon/BankIcons.ts @@ -7,7 +7,7 @@ import CONST from '@src/CONST'; import {BankIcon, BankName, BankNameKey} from '@src/types/onyx/Bank'; type BankIconParams = { - themeStyles: ThemeStyles; + styles: ThemeStyles; bankName?: BankName; isCard?: boolean; }; @@ -115,7 +115,8 @@ function getBankNameKey(bankName: string): BankNameKey { /** * Returns Bank Icon Object that matches to existing bank icons or default icons */ -export default function getBankIcon({themeStyles, bankName, isCard = false}: BankIconParams): BankIcon { + +export default function getBankIcon({styles, bankName, isCard = false}: BankIconParams): BankIcon { const bankIcon: BankIcon = { icon: isCard ? GenericBankCard : GenericBank, }; @@ -130,11 +131,11 @@ export default function getBankIcon({themeStyles, bankName, isCard = false}: Ban // For default Credit Card icon the icon size should not be set. if (!isCard) { bankIcon.iconSize = variables.iconSizeExtraLarge; - bankIcon.iconStyles = [themeStyles.bankIconContainer]; + bankIcon.iconStyles = [styles.bankIconContainer]; } else { bankIcon.iconHeight = variables.bankCardHeight; bankIcon.iconWidth = variables.bankCardWidth; - bankIcon.iconStyles = [themeStyles.assignedCardsIconContainer]; + bankIcon.iconStyles = [styles.assignedCardsIconContainer]; } return bankIcon; diff --git a/src/components/Image/BaseImage.js b/src/components/Image/BaseImage.js new file mode 100644 index 000000000000..cd2326900c6c --- /dev/null +++ b/src/components/Image/BaseImage.js @@ -0,0 +1,29 @@ +import React, {useCallback} from 'react'; +import {Image as RNImage} from 'react-native'; +import {defaultProps, imagePropTypes} from './imagePropTypes'; + +function BaseImage({onLoad, ...props}) { + const imageLoadedSuccessfully = useCallback( + ({nativeEvent}) => { + // We override `onLoad`, so both web and native have the same signature + const {width, height} = nativeEvent.source; + onLoad({nativeEvent: {width, height}}); + }, + [onLoad], + ); + + return ( + + ); +} + +BaseImage.propTypes = imagePropTypes; +BaseImage.defaultProps = defaultProps; +BaseImage.displayName = 'BaseImage'; + +export default BaseImage; diff --git a/src/components/Image/BaseImage.native.js b/src/components/Image/BaseImage.native.js new file mode 100644 index 000000000000..a621947267a1 --- /dev/null +++ b/src/components/Image/BaseImage.native.js @@ -0,0 +1,3 @@ +import RNFastImage from 'react-native-fast-image'; + +export default RNFastImage; diff --git a/src/components/Image/index.js b/src/components/Image/index.js index ef1a69e19c12..8cee1cf95e14 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -1,51 +1,35 @@ import lodashGet from 'lodash/get'; -import React, {useEffect, useMemo} from 'react'; -import {Image as RNImage} from 'react-native'; +import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import BaseImage from './BaseImage'; import {defaultProps, imagePropTypes} from './imagePropTypes'; import RESIZE_MODES from './resizeModes'; -function Image(props) { - const {source: propsSource, isAuthTokenRequired, onLoad, session} = props; - /** - * Check if the image source is a URL - if so the `encryptedAuthToken` is appended - * to the source. - */ +function Image({source: propsSource, isAuthTokenRequired, session, ...forwardedProps}) { + // Update the source to include the auth token if required const source = useMemo(() => { - if (isAuthTokenRequired) { - // There is currently a `react-native-web` bug preventing the authToken being passed - // in the headers of the image request so the authToken is added as a query param. - // On native the authToken IS passed in the image request headers - const authToken = lodashGet(session, 'encryptedAuthToken', null); - return {uri: `${propsSource.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`}; + if (typeof lodashGet(propsSource, 'uri') === 'number') { + return propsSource.uri; } + if (typeof propsSource !== 'number' && isAuthTokenRequired) { + const authToken = lodashGet(session, 'encryptedAuthToken'); + return { + ...propsSource, + headers: { + [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken, + }, + }; + } + return propsSource; // The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034. // eslint-disable-next-line react-hooks/exhaustive-deps }, [propsSource, isAuthTokenRequired]); - /** - * The natural image dimensions are retrieved using the updated source - * and as a result the `onLoad` event needs to be manually invoked to return these dimensions - */ - useEffect(() => { - // If an onLoad callback was specified then manually call it and pass - // the natural image dimensions to match the native API - if (onLoad == null) { - return; - } - RNImage.getSize(source.uri, (width, height) => { - onLoad({nativeEvent: {width, height}}); - }); - }, [onLoad, source]); - - // Omit the props which the underlying RNImage won't use - const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']); - return ( - { - const {width, height} = evt.nativeEvent; - dimensionsCache.set(source.uri, {width, height}); - if (props.onLoad) { - props.onLoad(evt); - } - }} - /> - ); -} - -Image.propTypes = imagePropTypes; -Image.defaultProps = defaultProps; -Image.displayName = 'Image'; -const ImageWithOnyx = withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, -})(Image); -ImageWithOnyx.resizeMode = RESIZE_MODES; -ImageWithOnyx.resolveDimensions = resolveDimensions; - -export default ImageWithOnyx; diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 3b2de574ba17..1765c85cdd48 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -201,7 +201,7 @@ function OptionRowLHN(props) { styles.alignItemsCenter, styles.justifyContentBetween, styles.sidebarLink, - styles.sidebarLinkInner, + styles.sidebarLinkInnerLHN, StyleUtils.getBackgroundColorStyle(theme.sidebar), props.isFocused ? styles.sidebarLinkActive : null, (hovered || isContextMenuActive) && !props.isFocused ? props.hoverStyle || styles.sidebarLinkHover : null, diff --git a/src/components/Pressable/GenericPressable/index.tsx b/src/components/Pressable/GenericPressable/index.tsx index d87b3a9810d1..51099733e04f 100644 --- a/src/components/Pressable/GenericPressable/index.tsx +++ b/src/components/Pressable/GenericPressable/index.tsx @@ -14,7 +14,7 @@ function WebGenericPressable({focusable = true, ...props}: PressableProps, ref: // change native accessibility props to web accessibility props focusable={focusable} tabIndex={props.tabIndex ?? (!accessible || !focusable) ? -1 : 0} - role={props.accessibilityRole as Role} + role={(props.accessibilityRole ?? props.role) as Role} id={props.nativeID} aria-label={props.accessibilityLabel} aria-labelledby={props.accessibilityLabelledBy} diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index bd7a24d15f1f..4a1d60a869ad 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -33,7 +33,6 @@ function RoomHeaderAvatars(props) { @@ -78,7 +77,6 @@ function RoomHeaderAvatars(props) { diff --git a/src/components/Tooltip/TooltipRenderedOnPageBody.js b/src/components/Tooltip/TooltipRenderedOnPageBody.js index c3898a4fe983..d92457238675 100644 --- a/src/components/Tooltip/TooltipRenderedOnPageBody.js +++ b/src/components/Tooltip/TooltipRenderedOnPageBody.js @@ -115,7 +115,7 @@ function TooltipRenderedOnPageBody({ tooltipContentWidth: contentMeasuredWidth, tooltipWrapperHeight: wrapperMeasuredHeight, theme, - themeStyles: styles, + styles, shiftHorizontal, shiftVertical, }), diff --git a/src/languages/en.ts b/src/languages/en.ts index 817f06f6b344..a276de4e0f7c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1547,6 +1547,12 @@ export default { invitePeople: 'Invite new members', genericFailureMessage: 'An error occurred inviting the user to the workspace, please try again.', pleaseEnterValidLogin: `Please ensure the email or phone number is valid (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, + user: 'user', + users: 'users', + invited: 'invited', + removed: 'removed', + to: 'to', + from: 'from', }, inviteMessage: { inviteMessageTitle: 'Add message', diff --git a/src/languages/es.ts b/src/languages/es.ts index b219021daa0f..290d80a6f65d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1569,6 +1569,12 @@ export default { invitePeople: 'Invitar nuevos miembros', genericFailureMessage: 'Se produjo un error al invitar al usuario al espacio de trabajo. Vuelva a intentarlo..', pleaseEnterValidLogin: `Asegúrese de que el correo electrónico o el número de teléfono sean válidos (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`, + user: 'usuario', + users: 'usuarios', + invited: 'invitó', + removed: 'eliminó', + to: 'a', + from: 'de', }, inviteMessage: { inviteMessageTitle: 'Añadir un mensaje', diff --git a/src/libs/Localize/index.ts b/src/libs/Localize/index.ts index 488ff0d9b98a..77c34ebdc576 100644 --- a/src/libs/Localize/index.ts +++ b/src/libs/Localize/index.ts @@ -1,6 +1,7 @@ import * as RNLocalize from 'react-native-localize'; import Onyx from 'react-native-onyx'; import Log from '@libs/Log'; +import {MessageElementBase, MessageTextElement} from '@libs/MessageElement'; import Config from '@src/CONFIG'; import CONST from '@src/CONST'; import translations from '@src/languages/translations'; @@ -121,15 +122,48 @@ function translateIfPhraseKey(message: MaybePhraseKey): string { } } +function getPreferredListFormat(): Intl.ListFormat { + if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) { + init(); + } + + return CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()]; +} + /** * Format an array into a string with comma and "and" ("a dog, a cat and a chicken") */ -function arrayToString(anArray: string[]) { - if (!CONJUNCTION_LIST_FORMATS_FOR_LOCALES) { - init(); +function formatList(components: string[]) { + const listFormat = getPreferredListFormat(); + return listFormat.format(components); +} + +function formatMessageElementList(elements: readonly E[]): ReadonlyArray { + const listFormat = getPreferredListFormat(); + const parts = listFormat.formatToParts(elements.map((e) => e.content)); + const resultElements: Array = []; + + let nextElementIndex = 0; + for (const part of parts) { + if (part.type === 'element') { + /** + * The standard guarantees that all input elements will be present in the constructed parts, each exactly + * once, and without any modifications: https://tc39.es/ecma402/#sec-createpartsfromlist + */ + const element = elements[nextElementIndex++]; + + resultElements.push(element); + } else { + const literalElement: MessageTextElement = { + kind: 'text', + content: part.value, + }; + + resultElements.push(literalElement); + } } - const listFormat = CONJUNCTION_LIST_FORMATS_FOR_LOCALES[BaseLocaleListener.getPreferredLocale()]; - return listFormat.format(anArray); + + return resultElements; } /** @@ -139,5 +173,5 @@ function getDevicePreferredLocale(): string { return RNLocalize.findBestAvailableLanguage([CONST.LOCALES.EN, CONST.LOCALES.ES])?.languageTag ?? CONST.LOCALES.DEFAULT; } -export {translate, translateLocal, translateIfPhraseKey, arrayToString, getDevicePreferredLocale}; +export {translate, translateLocal, translateIfPhraseKey, formatList, formatMessageElementList, getDevicePreferredLocale}; export type {PhraseParameters, Phrase, MaybePhraseKey}; diff --git a/src/libs/MessageElement.ts b/src/libs/MessageElement.ts new file mode 100644 index 000000000000..584d7e1e289a --- /dev/null +++ b/src/libs/MessageElement.ts @@ -0,0 +1,11 @@ +type MessageElementBase = { + readonly kind: string; + readonly content: string; +}; + +type MessageTextElement = { + readonly kind: 'text'; + readonly content: string; +} & MessageElementBase; + +export type {MessageElementBase, MessageTextElement}; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.tsx similarity index 83% rename from src/libs/Navigation/AppNavigator/AuthScreens.js rename to src/libs/Navigation/AppNavigator/AuthScreens.tsx index de334635da54..a78b38728136 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -1,9 +1,6 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {memo, useEffect, useRef} from 'react'; import {View} from 'react-native'; -import Onyx, {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import Onyx, {OnyxEntry, withOnyx} from 'react-native-onyx'; import useWindowDimensions from '@hooks/useWindowDimensions'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import getCurrentUrl from '@libs/Navigation/currentUrl'; @@ -12,6 +9,7 @@ import NetworkConnection from '@libs/NetworkConnection'; import * as Pusher from '@libs/Pusher/pusher'; import PusherConnectionManager from '@libs/PusherConnectionManager'; import * as SessionUtils from '@libs/SessionUtils'; +import type {AuthScreensParamList} from '@navigation/types'; import DemoSetupPage from '@pages/DemoSetupPage'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import DesktopSignInRedirectPage from '@pages/signin/DesktopSignInRedirectPage'; @@ -31,32 +29,52 @@ import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import * as OnyxTypes from '@src/types/onyx'; +import type {SelectedTimezone, Timezone} from '@src/types/onyx/PersonalDetails'; import createCustomStackNavigator from './createCustomStackNavigator'; import defaultScreenOptions from './defaultScreenOptions'; import getRootNavigatorScreenOptions from './getRootNavigatorScreenOptions'; import CentralPaneNavigator from './Navigators/CentralPaneNavigator'; import RightModalNavigator from './Navigators/RightModalNavigator'; -const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default; -const loadSidebarScreen = () => require('../../../pages/home/sidebar/SidebarScreen').default; -const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default; -const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default; -const loadConciergePage = () => require('../../../pages/ConciergePage').default; +type AuthScreensProps = { + /** Session of currently logged in user */ + session: OnyxEntry; + + /** The report ID of the last opened public room as anonymous user */ + lastOpenedPublicRoomID: OnyxEntry; + + /** Opt-in experimental mode that prevents certain Onyx keys from persisting to disk */ + isUsingMemoryOnlyKeys: OnyxEntry; + + /** The last Onyx update ID was applied to the client */ + lastUpdateIDAppliedToClient: OnyxEntry; -let timezone; -let currentAccountID; -let isLoadingApp; + /** Information about any currently running demos */ + demoInfo: OnyxEntry; +}; + +const loadReportAttachments = () => require('../../../pages/home/report/ReportAttachments').default as React.ComponentType; +const loadSidebarScreen = () => require('../../../pages/home/sidebar/SidebarScreen').default as React.ComponentType; +const loadValidateLoginPage = () => require('../../../pages/ValidateLoginPage').default as React.ComponentType; +const loadLogOutPreviousUserPage = () => require('../../../pages/LogOutPreviousUserPage').default as React.ComponentType; +const loadConciergePage = () => require('../../../pages/ConciergePage').default as React.ComponentType; + +let timezone: Timezone | null; +let currentAccountID = -1; +let isLoadingApp = false; Onyx.connect({ key: ONYXKEYS.SESSION, - callback: (val) => { + callback: (value) => { // When signed out, val hasn't accountID - if (!_.has(val, 'accountID')) { + if (!(value && 'accountID' in value)) { timezone = null; return; } - currentAccountID = val.accountID; + currentAccountID = value.accountID ?? -1; + if (Navigation.isActiveRoute(ROUTES.SIGN_IN_MODAL)) { // This means sign in in RHP was successful, so we can dismiss the modal and subscribe to user events Navigation.dismissModal(); @@ -67,17 +85,17 @@ Onyx.connect({ Onyx.connect({ key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - if (!val || timezone) { + callback: (value) => { + if (!value || timezone) { return; } - timezone = lodashGet(val, [currentAccountID, 'timezone'], {}); - const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + timezone = value?.[currentAccountID]?.timezone ?? {}; + const currentTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone as SelectedTimezone; // If the current timezone is different than the user's timezone, and their timezone is set to automatic // then update their timezone. - if (_.isObject(timezone) && timezone.automatic && timezone.selected !== currentTimezone) { + if (timezone?.automatic && timezone?.selected !== currentTimezone) { timezone.selected = currentTimezone; PersonalDetails.updateAutomaticTimezone({ automatic: true, @@ -89,12 +107,12 @@ Onyx.connect({ Onyx.connect({ key: ONYXKEYS.IS_LOADING_APP, - callback: (val) => { - isLoadingApp = val; + callback: (value) => { + isLoadingApp = !!value; }, }); -const RootStack = createCustomStackNavigator(); +const RootStack = createCustomStackNavigator(); // We want to delay the re-rendering for components(e.g. ReportActionCompose) // that depends on modal visibility until Modal is completely closed and its focused // When modal screen is focused, update modal visibility in Onyx @@ -109,40 +127,7 @@ const modalScreenListeners = { }, }; -const propTypes = { - /** Session of currently logged in user */ - session: PropTypes.shape({ - email: PropTypes.string.isRequired, - }), - - /** The report ID of the last opened public room as anonymous user */ - lastOpenedPublicRoomID: PropTypes.string, - - /** Opt-in experimental mode that prevents certain Onyx keys from persisting to disk */ - isUsingMemoryOnlyKeys: PropTypes.bool, - - /** The last Onyx update ID was applied to the client */ - lastUpdateIDAppliedToClient: PropTypes.number, - - /** Information about any currently running demos */ - demoInfo: PropTypes.shape({ - money2020: PropTypes.shape({ - isBeginningDemo: PropTypes.bool, - }), - }), -}; - -const defaultProps = { - isUsingMemoryOnlyKeys: false, - session: { - email: null, - }, - lastOpenedPublicRoomID: null, - lastUpdateIDAppliedToClient: null, - demoInfo: {}, -}; - -function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, session, lastOpenedPublicRoomID, demoInfo}) { +function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoomID, demoInfo, isUsingMemoryOnlyKeys = false}: AuthScreensProps) { const styles = useThemeStyles(); const {isSmallScreenWidth} = useWindowDimensions(); const screenOptions = getRootNavigatorScreenOptions(isSmallScreenWidth, styles); @@ -158,8 +143,8 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio const searchShortcutConfig = CONST.KEYBOARD_SHORTCUTS.SEARCH; const chatShortcutConfig = CONST.KEYBOARD_SHORTCUTS.NEW_CHAT; const currentUrl = getCurrentUrl(); - const isLoggingInAsNewUser = SessionUtils.isLoggingInAsNewUser(currentUrl, session.email); - const shouldGetAllData = isUsingMemoryOnlyKeys || SessionUtils.didUserLogInDuringSession() || isLoggingInAsNewUser; + const isLoggingInAsNewUser = !!session?.email && SessionUtils.isLoggingInAsNewUser(currentUrl, session.email); + const shouldGetAllData = !!isUsingMemoryOnlyKeys || SessionUtils.didUserLogInDuringSession() || isLoggingInAsNewUser; // Sign out the current user if we're transitioning with a different user const isTransitioning = currentUrl.includes(ROUTES.TRANSITION_BETWEEN_APPS); if (isLoggingInAsNewUser && isTransitioning) { @@ -202,7 +187,7 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio App.redirectThirdPartyDesktopSignIn(); // Check if we should be running any demos immediately after signing in. - if (lodashGet(demoInfo, 'money2020.isBeginningDemo', false)) { + if (demoInfo?.money2020?.isBeginningDemo) { Navigation.navigate(ROUTES.MONEY2020, CONST.NAVIGATION.TYPE.FORCED_UP); } if (lastOpenedPublicRoomID) { @@ -265,15 +250,7 @@ function AuthScreens({isUsingMemoryOnlyKeys, lastUpdateIDAppliedToClient, sessio return ( - + true); -export default withOnyx({ +export default withOnyx({ session: { key: ONYXKEYS.SESSION, }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js deleted file mode 100644 index be803e62a98b..000000000000 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ /dev/null @@ -1,264 +0,0 @@ -import {CardStyleInterpolators, createStackNavigator} from '@react-navigation/stack'; -import React, {useMemo} from 'react'; -import _ from 'underscore'; -import useThemeStyles from '@styles/useThemeStyles'; -import SCREENS from '@src/SCREENS'; - -/** - * Create a modal stack navigator with an array of sub-screens. - * - * @param {Object} screens key/value pairs where the key is the name of the screen and the value is a functon that returns the lazy-loaded component - * @returns {Function} - */ -function createModalStackNavigator(screens) { - const ModalStackNavigator = createStackNavigator(); - - function ModalStack() { - const styles = useThemeStyles(); - - const defaultSubRouteOptions = useMemo( - () => ({ - cardStyle: styles.navigationScreenCardStyle, - headerShown: false, - cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, - }), - [styles], - ); - - return ( - - {_.map(screens, (getComponent, name) => ( - - ))} - - ); - } - - ModalStack.displayName = 'ModalStack'; - - return ModalStack; -} - -const MoneyRequestModalStackNavigator = createModalStackNavigator({ - Money_Request: () => require('../../../pages/iou/MoneyRequestSelectorPage').default, - Money_Request_Amount: () => require('../../../pages/iou/steps/NewRequestAmountPage').default, - Money_Request_Participants: () => require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default, - Money_Request_Confirmation: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default, - Money_Request_Currency: () => require('../../../pages/iou/IOUCurrencySelection').default, - Money_Request_Date: () => require('../../../pages/iou/MoneyRequestDatePage').default, - Money_Request_Description: () => require('../../../pages/iou/MoneyRequestDescriptionPage').default, - Money_Request_Category: () => require('../../../pages/iou/MoneyRequestCategoryPage').default, - Money_Request_Tag: () => require('../../../pages/iou/MoneyRequestTagPage').default, - Money_Request_Merchant: () => require('../../../pages/iou/MoneyRequestMerchantPage').default, - IOU_Send_Add_Bank_Account: () => require('../../../pages/AddPersonalBankAccountPage').default, - IOU_Send_Add_Debit_Card: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default, - IOU_Send_Enable_Payments: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default, - Money_Request_Waypoint: () => require('../../../pages/iou/NewDistanceRequestWaypointEditorPage').default, - Money_Request_Edit_Waypoint: () => require('../../../pages/iou/MoneyRequestEditWaypointPage').default, - Money_Request_Distance: () => require('../../../pages/iou/NewDistanceRequestPage').default, - Money_Request_Receipt: () => require('../../../pages/EditRequestReceiptPage').default, -}); - -const SplitDetailsModalStackNavigator = createModalStackNavigator({ - SplitDetails_Root: () => require('../../../pages/iou/SplitBillDetailsPage').default, - SplitDetails_Edit_Request: () => require('../../../pages/EditSplitBillPage').default, - SplitDetails_Edit_Currency: () => require('../../../pages/iou/IOUCurrencySelection').default, -}); - -const DetailsModalStackNavigator = createModalStackNavigator({ - Details_Root: () => require('../../../pages/DetailsPage').default, -}); - -const ProfileModalStackNavigator = createModalStackNavigator({ - Profile_Root: () => require('../../../pages/ProfilePage').default, -}); - -const ReportDetailsModalStackNavigator = createModalStackNavigator({ - Report_Details_Root: () => require('../../../pages/ReportDetailsPage').default, - Report_Details_Share_Code: () => require('../../../pages/home/report/ReportDetailsShareCodePage').default, -}); - -const ReportSettingsModalStackNavigator = createModalStackNavigator({ - Report_Settings_Root: () => require('../../../pages/settings/Report/ReportSettingsPage').default, - Report_Settings_Room_Name: () => require('../../../pages/settings/Report/RoomNamePage').default, - Report_Settings_Notification_Preferences: () => require('../../../pages/settings/Report/NotificationPreferencePage').default, - Report_Settings_Write_Capability: () => require('../../../pages/settings/Report/WriteCapabilityPage').default, -}); - -const TaskModalStackNavigator = createModalStackNavigator({ - Task_Title: () => require('../../../pages/tasks/TaskTitlePage').default, - Task_Description: () => require('../../../pages/tasks/TaskDescriptionPage').default, - Task_Assignee: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default, -}); - -const ReportWelcomeMessageModalStackNavigator = createModalStackNavigator({ - Report_WelcomeMessage_Root: () => require('../../../pages/ReportWelcomeMessagePage').default, -}); - -const ReportParticipantsModalStackNavigator = createModalStackNavigator({ - ReportParticipants_Root: () => require('../../../pages/ReportParticipantsPage').default, -}); - -const RoomMembersModalStackNavigator = createModalStackNavigator({ - RoomMembers_Root: () => require('../../../pages/RoomMembersPage').default, -}); - -const RoomInviteModalStackNavigator = createModalStackNavigator({ - RoomInvite_Root: () => require('../../../pages/RoomInvitePage').default, -}); - -const SearchModalStackNavigator = createModalStackNavigator({ - Search_Root: () => require('../../../pages/SearchPage').default, -}); - -const NewChatModalStackNavigator = createModalStackNavigator({ - NewChat_Root: () => require('../../../pages/NewChatSelectorPage').default, -}); - -const NewTaskModalStackNavigator = createModalStackNavigator({ - NewTask_Root: () => require('../../../pages/tasks/NewTaskPage').default, - NewTask_TaskAssigneeSelector: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default, - NewTask_TaskShareDestinationSelector: () => require('../../../pages/tasks/TaskShareDestinationSelectorModal').default, - NewTask_Details: () => require('../../../pages/tasks/NewTaskDetailsPage').default, - NewTask_Title: () => require('../../../pages/tasks/NewTaskTitlePage').default, - NewTask_Description: () => require('../../../pages/tasks/NewTaskDescriptionPage').default, -}); - -const NewTeachersUniteNavigator = createModalStackNavigator({ - [SCREENS.SAVE_THE_WORLD.ROOT]: () => require('../../../pages/TeachersUnite/SaveTheWorldPage').default, - I_Know_A_Teacher: () => require('../../../pages/TeachersUnite/KnowATeacherPage').default, - Intro_School_Principal: () => require('../../../pages/TeachersUnite/ImTeacherPage').default, - I_Am_A_Teacher: () => require('../../../pages/TeachersUnite/ImTeacherPage').default, -}); - -const SettingsModalStackNavigator = createModalStackNavigator({ - [SCREENS.SETTINGS.ROOT]: () => require('../../../pages/settings/InitialSettingsPage').default, - Settings_Share_Code: () => require('../../../pages/ShareCodePage').default, - [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../pages/workspace/WorkspacesListPage').default, - Settings_Profile: () => require('../../../pages/settings/Profile/ProfilePage').default, - Settings_Pronouns: () => require('../../../pages/settings/Profile/PronounsPage').default, - Settings_Display_Name: () => require('../../../pages/settings/Profile/DisplayNamePage').default, - Settings_Timezone: () => require('../../../pages/settings/Profile/TimezoneInitialPage').default, - Settings_Timezone_Select: () => require('../../../pages/settings/Profile/TimezoneSelectPage').default, - Settings_PersonalDetails_Initial: () => require('../../../pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage').default, - Settings_PersonalDetails_LegalName: () => require('../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default, - Settings_PersonalDetails_DateOfBirth: () => require('../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default, - Settings_PersonalDetails_Address: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default, - Settings_PersonalDetails_Address_Country: () => require('../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default, - Settings_ContactMethods: () => require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default, - Settings_ContactMethodDetails: () => require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default, - Settings_NewContactMethod: () => require('../../../pages/settings/Profile/Contacts/NewContactMethodPage').default, - [SCREENS.SETTINGS.PREFERENCES]: () => require('../../../pages/settings/Preferences/PreferencesPage').default, - Settings_Preferences_PriorityMode: () => require('../../../pages/settings/Preferences/PriorityModePage').default, - Settings_Preferences_Language: () => require('../../../pages/settings/Preferences/LanguagePage').default, - // Will be uncommented as part of https://github.com/Expensify/App/issues/21670 - // Settings_Preferences_Theme: () => require('../../../pages/settings/Preferences/ThemePage').default, - Settings_Close: () => require('../../../pages/settings/Security/CloseAccountPage').default, - [SCREENS.SETTINGS.SECURITY]: () => require('../../../pages/settings/Security/SecuritySettingsPage').default, - Settings_About: () => require('../../../pages/settings/AboutPage/AboutPage').default, - Settings_App_Download_Links: () => require('../../../pages/settings/AppDownloadLinks').default, - Settings_Lounge_Access: () => require('../../../pages/settings/Profile/LoungeAccessPage').default, - Settings_Wallet: () => require('../../../pages/settings/Wallet/WalletPage').default, - Settings_Wallet_Cards_Digital_Details_Update_Address: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default, - Settings_Wallet_DomainCard: () => require('../../../pages/settings/Wallet/ExpensifyCardPage').default, - Settings_Wallet_ReportVirtualCardFraud: () => require('../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default, - Settings_Wallet_Card_Activate: () => require('../../../pages/settings/Wallet/ActivatePhysicalCardPage').default, - [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.NAME]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardName').default, - [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.PHONE]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardPhone').default, - [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.ADDRESS]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardAddress').default, - [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.CONFIRM]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardConfirm').default, - Settings_Wallet_Transfer_Balance: () => require('../../../pages/settings/Wallet/TransferBalancePage').default, - Settings_Wallet_Choose_Transfer_Account: () => require('../../../pages/settings/Wallet/ChooseTransferAccountPage').default, - Settings_Wallet_EnablePayments: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default, - Settings_Add_Debit_Card: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default, - Settings_Add_Bank_Account: () => require('../../../pages/AddPersonalBankAccountPage').default, - [SCREENS.SETTINGS.STATUS]: () => require('../../../pages/settings/Profile/CustomStatus/StatusPage').default, - Settings_Status_Set: () => require('../../../pages/settings/Profile/CustomStatus/StatusSetPage').default, - Workspace_Initial: () => require('../../../pages/workspace/WorkspaceInitialPage').default, - Workspace_Settings: () => require('../../../pages/workspace/WorkspaceSettingsPage').default, - Workspace_Settings_Currency: () => require('../../../pages/workspace/WorkspaceSettingsCurrencyPage').default, - Workspace_Card: () => require('../../../pages/workspace/card/WorkspaceCardPage').default, - Workspace_Reimburse: () => require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default, - Workspace_RateAndUnit: () => require('../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage').default, - Workspace_Bills: () => require('../../../pages/workspace/bills/WorkspaceBillsPage').default, - Workspace_Invoices: () => require('../../../pages/workspace/invoices/WorkspaceInvoicesPage').default, - Workspace_Travel: () => require('../../../pages/workspace/travel/WorkspaceTravelPage').default, - Workspace_Members: () => require('../../../pages/workspace/WorkspaceMembersPage').default, - Workspace_Invite: () => require('../../../pages/workspace/WorkspaceInvitePage').default, - Workspace_Invite_Message: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default, - ReimbursementAccount: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default, - GetAssistance: () => require('../../../pages/GetAssistancePage').default, - Settings_TwoFactorAuth: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default, - Settings_ReportCardLostOrDamaged: () => require('../../../pages/settings/Wallet/ReportCardLostPage').default, - KeyboardShortcuts: () => require('../../../pages/KeyboardShortcutsPage').default, -}); - -const EnablePaymentsStackNavigator = createModalStackNavigator({ - EnablePayments_Root: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default, -}); - -const AddPersonalBankAccountModalStackNavigator = createModalStackNavigator({ - AddPersonalBankAccount_Root: () => require('../../../pages/AddPersonalBankAccountPage').default, -}); - -const ReimbursementAccountModalStackNavigator = createModalStackNavigator({ - ReimbursementAccount_Root: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default, -}); - -const WalletStatementStackNavigator = createModalStackNavigator({ - WalletStatement_Root: () => require('../../../pages/wallet/WalletStatementPage').default, -}); - -const FlagCommentStackNavigator = createModalStackNavigator({ - FlagComment_Root: () => require('../../../pages/FlagCommentPage').default, -}); - -const EditRequestStackNavigator = createModalStackNavigator({ - EditRequest_Root: () => require('../../../pages/EditRequestPage').default, - EditRequest_Currency: () => require('../../../pages/iou/IOUCurrencySelection').default, -}); - -const PrivateNotesModalStackNavigator = createModalStackNavigator({ - PrivateNotes_View: () => require('../../../pages/PrivateNotes/PrivateNotesViewPage').default, - PrivateNotes_List: () => require('../../../pages/PrivateNotes/PrivateNotesListPage').default, - PrivateNotes_Edit: () => require('../../../pages/PrivateNotes/PrivateNotesEditPage').default, -}); - -const SignInModalStackNavigator = createModalStackNavigator({ - SignIn_Root: () => require('../../../pages/signin/SignInModal').default, -}); -const ReferralModalStackNavigator = createModalStackNavigator({ - Referral_Details: () => require('../../../pages/ReferralDetailsPage').default, -}); - -export { - MoneyRequestModalStackNavigator, - SplitDetailsModalStackNavigator, - DetailsModalStackNavigator, - ProfileModalStackNavigator, - ReportDetailsModalStackNavigator, - TaskModalStackNavigator, - ReportSettingsModalStackNavigator, - ReportWelcomeMessageModalStackNavigator, - ReportParticipantsModalStackNavigator, - SearchModalStackNavigator, - NewChatModalStackNavigator, - NewTaskModalStackNavigator, - SettingsModalStackNavigator, - EnablePaymentsStackNavigator, - AddPersonalBankAccountModalStackNavigator, - ReimbursementAccountModalStackNavigator, - WalletStatementStackNavigator, - FlagCommentStackNavigator, - EditRequestStackNavigator, - PrivateNotesModalStackNavigator, - NewTeachersUniteNavigator, - SignInModalStackNavigator, - RoomMembersModalStackNavigator, - RoomInviteModalStackNavigator, - ReferralModalStackNavigator, -}; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx new file mode 100644 index 000000000000..163423036362 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -0,0 +1,294 @@ +import {ParamListBase} from '@react-navigation/routers'; +import {CardStyleInterpolators, createStackNavigator, StackNavigationOptions} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; +import type { + AddPersonalBankAccountNavigatorParamList, + DetailsNavigatorParamList, + EditRequestNavigatorParamList, + EnablePaymentsNavigatorParamList, + FlagCommentNavigatorParamList, + MoneyRequestNavigatorParamList, + NewChatNavigatorParamList, + NewTaskNavigatorParamList, + ParticipantsNavigatorParamList, + PrivateNotesNavigatorParamList, + ProfileNavigatorParamList, + ReferralDetailsNavigatorParamList, + ReimbursementAccountNavigatorParamList, + ReportDetailsNavigatorParamList, + ReportSettingsNavigatorParamList, + ReportWelcomeMessageNavigatorParamList, + RoomInviteNavigatorParamList, + RoomMembersNavigatorParamList, + SearchNavigatorParamList, + SettingsNavigatorParamList, + SignInNavigatorParamList, + SplitDetailsNavigatorParamList, + TaskDetailsNavigatorParamList, + TeachersUniteNavigatorParamList, + WalletStatementNavigatorParamList, +} from '@navigation/types'; +import useThemeStyles from '@styles/useThemeStyles'; +import SCREENS from '@src/SCREENS'; +import type {Screen} from '@src/SCREENS'; + +type Screens = Partial React.ComponentType>>; + +/** + * Create a modal stack navigator with an array of sub-screens. + * + * @param screens key/value pairs where the key is the name of the screen and the value is a functon that returns the lazy-loaded component + */ +function createModalStackNavigator(screens: Screens): React.ComponentType { + const ModalStackNavigator = createStackNavigator(); + + function ModalStack() { + const styles = useThemeStyles(); + + const defaultSubRouteOptions = useMemo( + (): StackNavigationOptions => ({ + cardStyle: styles.navigationScreenCardStyle, + headerShown: false, + cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, + }), + [styles], + ); + + return ( + + {Object.keys(screens as Required).map((name) => ( + )[name as Screen]} + /> + ))} + + ); + } + + ModalStack.displayName = 'ModalStack'; + + return ModalStack; +} + +const MoneyRequestModalStackNavigator = createModalStackNavigator({ + [SCREENS.MONEY_REQUEST.ROOT]: () => require('../../../pages/iou/MoneyRequestSelectorPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.AMOUNT]: () => require('../../../pages/iou/steps/NewRequestAmountPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.PARTICIPANTS]: () => require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.CONFIRMATION]: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.DATE]: () => require('../../../pages/iou/MoneyRequestDatePage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.DESCRIPTION]: () => require('../../../pages/iou/MoneyRequestDescriptionPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.CATEGORY]: () => require('../../../pages/iou/MoneyRequestCategoryPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.TAG]: () => require('../../../pages/iou/MoneyRequestTagPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.MERCHANT]: () => require('../../../pages/iou/MoneyRequestMerchantPage').default as React.ComponentType, + [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: () => require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType, + [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType, + [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.WAYPOINT]: () => require('../../../pages/iou/NewDistanceRequestWaypointEditorPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.EDIT_WAYPOINT]: () => require('../../../pages/iou/MoneyRequestEditWaypointPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.DISTANCE]: () => require('../../../pages/iou/NewDistanceRequestPage').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.RECEIPT]: () => require('../../../pages/EditRequestReceiptPage').default as React.ComponentType, +}); + +const SplitDetailsModalStackNavigator = createModalStackNavigator({ + [SCREENS.SPLIT_DETAILS.ROOT]: () => require('../../../pages/iou/SplitBillDetailsPage').default as React.ComponentType, + [SCREENS.SPLIT_DETAILS.EDIT_REQUEST]: () => require('../../../pages/EditSplitBillPage').default as React.ComponentType, + [SCREENS.SPLIT_DETAILS.EDIT_CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType, +}); + +const DetailsModalStackNavigator = createModalStackNavigator({ + [SCREENS.DETAILS_ROOT]: () => require('../../../pages/DetailsPage').default as React.ComponentType, +}); + +const ProfileModalStackNavigator = createModalStackNavigator({ + [SCREENS.PROFILE_ROOT]: () => require('../../../pages/ProfilePage').default as React.ComponentType, +}); + +const ReportDetailsModalStackNavigator = createModalStackNavigator({ + [SCREENS.REPORT_DETAILS.ROOT]: () => require('../../../pages/ReportDetailsPage').default as React.ComponentType, + [SCREENS.REPORT_DETAILS.SHARE_CODE]: () => require('../../../pages/home/report/ReportDetailsShareCodePage').default as React.ComponentType, +}); + +const ReportSettingsModalStackNavigator = createModalStackNavigator({ + [SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../pages/settings/Report/ReportSettingsPage').default as React.ComponentType, + [SCREENS.REPORT_SETTINGS.ROOM_NAME]: () => require('../../../pages/settings/Report/RoomNamePage').default as React.ComponentType, + [SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: () => require('../../../pages/settings/Report/NotificationPreferencePage').default as React.ComponentType, + [SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: () => require('../../../pages/settings/Report/WriteCapabilityPage').default as React.ComponentType, +}); + +const TaskModalStackNavigator = createModalStackNavigator({ + [SCREENS.TASK.TITLE]: () => require('../../../pages/tasks/TaskTitlePage').default as React.ComponentType, + [SCREENS.TASK.DESCRIPTION]: () => require('../../../pages/tasks/TaskDescriptionPage').default as React.ComponentType, + [SCREENS.TASK.ASSIGNEE]: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default as React.ComponentType, +}); + +const ReportWelcomeMessageModalStackNavigator = createModalStackNavigator({ + [SCREENS.REPORT_WELCOME_MESSAGE_ROOT]: () => require('../../../pages/ReportWelcomeMessagePage').default as React.ComponentType, +}); + +const ReportParticipantsModalStackNavigator = createModalStackNavigator({ + [SCREENS.REPORT_PARTICIPANTS_ROOT]: () => require('../../../pages/ReportParticipantsPage').default as React.ComponentType, +}); + +const RoomMembersModalStackNavigator = createModalStackNavigator({ + [SCREENS.ROOM_MEMBERS_ROOT]: () => require('../../../pages/RoomMembersPage').default as React.ComponentType, +}); + +const RoomInviteModalStackNavigator = createModalStackNavigator({ + [SCREENS.ROOM_INVITE_ROOT]: () => require('../../../pages/RoomInvitePage').default as React.ComponentType, +}); + +const SearchModalStackNavigator = createModalStackNavigator({ + [SCREENS.SEARCH_ROOT]: () => require('../../../pages/SearchPage').default as React.ComponentType, +}); + +const NewChatModalStackNavigator = createModalStackNavigator({ + [SCREENS.NEW_CHAT_ROOT]: () => require('../../../pages/NewChatSelectorPage').default as React.ComponentType, +}); + +const NewTaskModalStackNavigator = createModalStackNavigator({ + [SCREENS.NEW_TASK.ROOT]: () => require('../../../pages/tasks/NewTaskPage').default as React.ComponentType, + [SCREENS.NEW_TASK.TASK_ASSIGNEE_SELECTOR]: () => require('../../../pages/tasks/TaskAssigneeSelectorModal').default as React.ComponentType, + [SCREENS.NEW_TASK.TASK_SHARE_DESTINATION_SELECTOR]: () => require('../../../pages/tasks/TaskShareDestinationSelectorModal').default as React.ComponentType, + [SCREENS.NEW_TASK.DETAILS]: () => require('../../../pages/tasks/NewTaskDetailsPage').default as React.ComponentType, + [SCREENS.NEW_TASK.TITLE]: () => require('../../../pages/tasks/NewTaskTitlePage').default as React.ComponentType, + [SCREENS.NEW_TASK.DESCRIPTION]: () => require('../../../pages/tasks/NewTaskDescriptionPage').default as React.ComponentType, +}); + +const NewTeachersUniteNavigator = createModalStackNavigator({ + [SCREENS.SAVE_THE_WORLD.ROOT]: () => require('../../../pages/TeachersUnite/SaveTheWorldPage').default as React.ComponentType, + [SCREENS.I_KNOW_A_TEACHER]: () => require('../../../pages/TeachersUnite/KnowATeacherPage').default as React.ComponentType, + [SCREENS.INTRO_SCHOOL_PRINCIPAL]: () => require('../../../pages/TeachersUnite/ImTeacherPage').default as React.ComponentType, + [SCREENS.I_AM_A_TEACHER]: () => require('../../../pages/TeachersUnite/ImTeacherPage').default as React.ComponentType, +}); + +const SettingsModalStackNavigator = createModalStackNavigator({ + [SCREENS.SETTINGS.ROOT]: () => require('../../../pages/settings/InitialSettingsPage').default as React.ComponentType, + [SCREENS.SETTINGS.SHARE_CODE]: () => require('../../../pages/ShareCodePage').default as React.ComponentType, + [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../pages/workspace/WorkspacesListPage').default as React.ComponentType, + [SCREENS.SETTINGS.PROFILE]: () => require('../../../pages/settings/Profile/ProfilePage').default as React.ComponentType, + [SCREENS.SETTINGS.PRONOUNS]: () => require('../../../pages/settings/Profile/PronounsPage').default as React.ComponentType, + [SCREENS.SETTINGS.DISPLAY_NAME]: () => require('../../../pages/settings/Profile/DisplayNamePage').default as React.ComponentType, + [SCREENS.SETTINGS.TIMEZONE]: () => require('../../../pages/settings/Profile/TimezoneInitialPage').default as React.ComponentType, + [SCREENS.SETTINGS.TIMEZONE_SELECT]: () => require('../../../pages/settings/Profile/TimezoneSelectPage').default as React.ComponentType, + [SCREENS.SETTINGS.PERSONAL_DETAILS_INITIAL]: () => require('../../../pages/settings/Profile/PersonalDetails/PersonalDetailsInitialPage').default as React.ComponentType, + [SCREENS.SETTINGS.PERSONAL_DETAILS_LEGAL_NAME]: () => require('../../../pages/settings/Profile/PersonalDetails/LegalNamePage').default as React.ComponentType, + [SCREENS.SETTINGS.PERSONAL_DETAILS_DATE_OF_BIRTH]: () => require('../../../pages/settings/Profile/PersonalDetails/DateOfBirthPage').default as React.ComponentType, + [SCREENS.SETTINGS.PERSONAL_DETAILS_ADDRESS]: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType, + [SCREENS.SETTINGS.PERSONAL_DETAILS_ADDRESS_COUNTRY]: () => require('../../../pages/settings/Profile/PersonalDetails/CountrySelectionPage').default as React.ComponentType, + [SCREENS.SETTINGS.CONTACT_METHODS]: () => require('../../../pages/settings/Profile/Contacts/ContactMethodsPage').default as React.ComponentType, + [SCREENS.SETTINGS.CONTACT_METHOD_DETAILS]: () => require('../../../pages/settings/Profile/Contacts/ContactMethodDetailsPage').default as React.ComponentType, + [SCREENS.SETTINGS.NEW_CONTACT_METHOD]: () => require('../../../pages/settings/Profile/Contacts/NewContactMethodPage').default as React.ComponentType, + [SCREENS.SETTINGS.PREFERENCES]: () => require('../../../pages/settings/Preferences/PreferencesPage').default as React.ComponentType, + [SCREENS.SETTINGS.PREFERENCES_PRIORITY_MODE]: () => require('../../../pages/settings/Preferences/PriorityModePage').default as React.ComponentType, + [SCREENS.SETTINGS.PREFERENCES_LANGUAGE]: () => require('../../../pages/settings/Preferences/LanguagePage').default as React.ComponentType, + // Will be uncommented as part of https://github.com/Expensify/App/issues/21670 + // [SCREENS.SETTINGS.PREFERENCES_THEME]: () => require('../../../pages/settings/Preferences/ThemePage').default as React.ComponentType, + [SCREENS.SETTINGS.CLOSE]: () => require('../../../pages/settings/Security/CloseAccountPage').default as React.ComponentType, + [SCREENS.SETTINGS.SECURITY]: () => require('../../../pages/settings/Security/SecuritySettingsPage').default as React.ComponentType, + [SCREENS.SETTINGS.ABOUT]: () => require('../../../pages/settings/AboutPage/AboutPage').default as React.ComponentType, + [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS]: () => require('../../../pages/settings/AppDownloadLinks').default as React.ComponentType, + [SCREENS.SETTINGS.LOUNGE_ACCESS]: () => require('../../../pages/settings/Profile/LoungeAccessPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET]: () => require('../../../pages/settings/Wallet/WalletPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: () => require('../../../pages/settings/Profile/PersonalDetails/AddressPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_DOMAIN_CARD]: () => require('../../../pages/settings/Wallet/ExpensifyCardPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_REPORT_VIRTUAL_CARD_FRAUD]: () => require('../../../pages/settings/Wallet/ReportVirtualCardFraudPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_CARD_ACTIVATE]: () => require('../../../pages/settings/Wallet/ActivatePhysicalCardPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.NAME]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardName').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.PHONE]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardPhone').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.ADDRESS]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardAddress').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_CARD_GET_PHYSICAL.CONFIRM]: () => require('../../../pages/settings/Wallet/Card/GetPhysicalCardConfirm').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_TRANSFER_BALANCE]: () => require('../../../pages/settings/Wallet/TransferBalancePage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_CHOOSE_TRANSFER_ACCOUNT]: () => require('../../../pages/settings/Wallet/ChooseTransferAccountPage').default as React.ComponentType, + [SCREENS.SETTINGS.WALLET_ENABLE_PAYMENTS]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType, + [SCREENS.SETTINGS.ADD_DEBIT_CARD]: () => require('../../../pages/settings/Wallet/AddDebitCardPage').default as React.ComponentType, + [SCREENS.SETTINGS.ADD_BANK_ACCOUNT]: () => require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType, + [SCREENS.SETTINGS.STATUS]: () => require('../../../pages/settings/Profile/CustomStatus/StatusPage').default as React.ComponentType, + [SCREENS.SETTINGS.STATUS_SET]: () => require('../../../pages/settings/Profile/CustomStatus/StatusSetPage').default as React.ComponentType, + [SCREENS.WORKSPACE.INITIAL]: () => require('../../../pages/workspace/WorkspaceInitialPage').default as React.ComponentType, + [SCREENS.WORKSPACE.SETTINGS]: () => require('../../../pages/workspace/WorkspaceSettingsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.CURRENCY]: () => require('../../../pages/workspace/WorkspaceSettingsCurrencyPage').default as React.ComponentType, + [SCREENS.WORKSPACE.CARD]: () => require('../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType, + [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType, + [SCREENS.WORKSPACE.RATE_AND_UNIT]: () => require('../../../pages/workspace/reimburse/WorkspaceRateAndUnitPage').default as React.ComponentType, + [SCREENS.WORKSPACE.BILLS]: () => require('../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType, + [SCREENS.WORKSPACE.INVOICES]: () => require('../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType, + [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType, + [SCREENS.WORKSPACE.INVITE]: () => require('../../../pages/workspace/WorkspaceInvitePage').default as React.ComponentType, + [SCREENS.WORKSPACE.INVITE_MESSAGE]: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default as React.ComponentType, + [SCREENS.REIMBURSEMENT_ACCOUNT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, + [SCREENS.GET_ASSISTANCE]: () => require('../../../pages/GetAssistancePage').default as React.ComponentType, + [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: () => require('../../../pages/settings/Security/TwoFactorAuth/TwoFactorAuthPage').default as React.ComponentType, + [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: () => require('../../../pages/settings/Wallet/ReportCardLostPage').default as React.ComponentType, + [SCREENS.KEYBOARD_SHORTCUTS]: () => require('../../../pages/KeyboardShortcutsPage').default as React.ComponentType, +}); + +const EnablePaymentsStackNavigator = createModalStackNavigator({ + [SCREENS.ENABLE_PAYMENTS_ROOT]: () => require('../../../pages/EnablePayments/EnablePaymentsPage').default as React.ComponentType, +}); + +const AddPersonalBankAccountModalStackNavigator = createModalStackNavigator({ + [SCREENS.ADD_PERSONAL_BANK_ACCOUNT_ROOT]: () => require('../../../pages/AddPersonalBankAccountPage').default as React.ComponentType, +}); + +const ReimbursementAccountModalStackNavigator = createModalStackNavigator({ + [SCREENS.REIMBURSEMENT_ACCOUNT_ROOT]: () => require('../../../pages/ReimbursementAccount/ReimbursementAccountPage').default as React.ComponentType, +}); + +const WalletStatementStackNavigator = createModalStackNavigator({ + [SCREENS.WALLET_STATEMENT_ROOT]: () => require('../../../pages/wallet/WalletStatementPage').default as React.ComponentType, +}); + +const FlagCommentStackNavigator = createModalStackNavigator({ + [SCREENS.FLAG_COMMENT_ROOT]: () => require('../../../pages/FlagCommentPage').default as React.ComponentType, +}); + +const EditRequestStackNavigator = createModalStackNavigator({ + [SCREENS.EDIT_REQUEST.ROOT]: () => require('../../../pages/EditRequestPage').default as React.ComponentType, + [SCREENS.EDIT_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType, +}); + +const PrivateNotesModalStackNavigator = createModalStackNavigator({ + [SCREENS.PRIVATE_NOTES.VIEW]: () => require('../../../pages/PrivateNotes/PrivateNotesViewPage').default as React.ComponentType, + [SCREENS.PRIVATE_NOTES.LIST]: () => require('../../../pages/PrivateNotes/PrivateNotesListPage').default as React.ComponentType, + [SCREENS.PRIVATE_NOTES.EDIT]: () => require('../../../pages/PrivateNotes/PrivateNotesEditPage').default as React.ComponentType, +}); + +const SignInModalStackNavigator = createModalStackNavigator({ + [SCREENS.SIGN_IN_ROOT]: () => require('../../../pages/signin/SignInModal').default as React.ComponentType, +}); + +const ReferralModalStackNavigator = createModalStackNavigator({ + [SCREENS.REFERRAL_DETAILS]: () => require('../../../pages/ReferralDetailsPage').default as React.ComponentType, +}); + +export { + MoneyRequestModalStackNavigator, + SplitDetailsModalStackNavigator, + DetailsModalStackNavigator, + ProfileModalStackNavigator, + ReportDetailsModalStackNavigator, + TaskModalStackNavigator, + ReportSettingsModalStackNavigator, + ReportWelcomeMessageModalStackNavigator, + ReportParticipantsModalStackNavigator, + SearchModalStackNavigator, + NewChatModalStackNavigator, + NewTaskModalStackNavigator, + SettingsModalStackNavigator, + EnablePaymentsStackNavigator, + AddPersonalBankAccountModalStackNavigator, + ReimbursementAccountModalStackNavigator, + WalletStatementStackNavigator, + FlagCommentStackNavigator, + EditRequestStackNavigator, + PrivateNotesModalStackNavigator, + NewTeachersUniteNavigator, + SignInModalStackNavigator, + RoomMembersModalStackNavigator, + RoomInviteModalStackNavigator, + ReferralModalStackNavigator, +}; diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx similarity index 89% rename from src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js rename to src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index d23b03c8c73e..228ea6bd3dce 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -2,10 +2,11 @@ import {createStackNavigator} from '@react-navigation/stack'; import React from 'react'; import ReportScreenWrapper from '@libs/Navigation/AppNavigator/ReportScreenWrapper'; import getCurrentUrl from '@libs/Navigation/currentUrl'; +import type {CentralPaneNavigatorParamList} from '@navigation/types'; import useThemeStyles from '@styles/useThemeStyles'; import SCREENS from '@src/SCREENS'; -const Stack = createStackNavigator(); +const Stack = createStackNavigator(); const url = getCurrentUrl(); const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : undefined; diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.native.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.native.tsx similarity index 100% rename from src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.native.js rename to src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.native.tsx diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.tsx similarity index 100% rename from src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.js rename to src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/index.tsx diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx similarity index 79% rename from src/libs/Navigation/AppNavigator/Navigators/Overlay.js rename to src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx index 44d996282617..31eb818b60dc 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/Overlay.js +++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx @@ -1,5 +1,4 @@ import {useCardAnimation} from '@react-navigation/stack'; -import PropTypes from 'prop-types'; import React from 'react'; import {Animated, View} from 'react-native'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -7,12 +6,12 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -const propTypes = { +type OverlayProps = { /* Callback to close the modal */ - onPress: PropTypes.func.isRequired, + onPress: () => void; }; -function Overlay(props) { +function Overlay({onPress}: OverlayProps) { const styles = useThemeStyles(); const {current} = useCardAnimation(); const {translate} = useLocalize(); @@ -20,20 +19,20 @@ function Overlay(props) { return ( - {/* In the latest Electron version buttons can't be both clickable and draggable. - That's why we added this workaround. Because of two Pressable components on the desktop app + {/* In the latest Electron version buttons can't be both clickable and draggable. + That's why we added this workaround. Because of two Pressable components on the desktop app we have 30px draggable ba at the top and the rest of the dimmed area is clickable. On other devices, everything behaves normally like one big pressable */} ; -const propTypes = { - /* Navigation functions provided by React Navigation */ - navigation: PropTypes.shape({ - goBack: PropTypes.func.isRequired, - }).isRequired, -}; +const Stack = createStackNavigator(); -function RightModalNavigator(props) { +function RightModalNavigator({navigation}: RightModalNavigatorProps) { const styles = useThemeStyles(); const {isSmallScreenWidth} = useWindowDimensions(); const screenOptions = useMemo(() => RHPScreenOptions(styles), [styles]); return ( - {!isSmallScreenWidth && } + {!isSmallScreenWidth && } (); function PublicScreens() { return ( @@ -55,4 +56,5 @@ function PublicScreens() { } PublicScreens.displayName = 'PublicScreens'; + export default PublicScreens; diff --git a/src/libs/Navigation/AppNavigator/RHPScreenOptions.js b/src/libs/Navigation/AppNavigator/RHPScreenOptions.js deleted file mode 100644 index 02354b90591f..000000000000 --- a/src/libs/Navigation/AppNavigator/RHPScreenOptions.js +++ /dev/null @@ -1,17 +0,0 @@ -import {CardStyleInterpolators} from '@react-navigation/stack'; - -/** - * RHP stack navigator screen options generator function - * @function - * @param {Object} styles - The styles object - * @returns {Object} - The screen options object - */ -const RHPScreenOptions = (styles) => ({ - headerShown: false, - animationEnabled: true, - gestureDirection: 'horizontal', - cardStyle: styles.navigationScreenCardStyle, - cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, -}); - -export default RHPScreenOptions; diff --git a/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts b/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts new file mode 100644 index 000000000000..6b56bb00cf56 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/RHPScreenOptions.ts @@ -0,0 +1,17 @@ +import {CardStyleInterpolators, StackNavigationOptions} from '@react-navigation/stack'; +import styles from '@styles/styles'; + +/** + * RHP stack navigator screen options generator function + * @param themeStyles - The styles object + * @returns The screen options object + */ +const RHPScreenOptions = (themeStyles: typeof styles): StackNavigationOptions => ({ + headerShown: false, + animationEnabled: true, + gestureDirection: 'horizontal', + cardStyle: themeStyles.navigationScreenCardStyle, + cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, +}); + +export default RHPScreenOptions; diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.js b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.js deleted file mode 100644 index bb7acddb188c..000000000000 --- a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.js +++ /dev/null @@ -1,112 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import {useEffect} from 'react'; -import {withOnyx} from 'react-native-onyx'; -import usePermissions from '@hooks/usePermissions'; -import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as App from '@userActions/App'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /** Available reports that would be displayed in this navigator */ - reports: PropTypes.objectOf(reportPropTypes), - - /** The policies which the user has access to */ - policies: PropTypes.objectOf( - PropTypes.shape({ - /** The policy name */ - name: PropTypes.string, - - /** The type of the policy */ - type: PropTypes.string, - }), - ), - - isFirstTimeNewExpensifyUser: PropTypes.bool, - - /** Navigation route context info provided by react navigation */ - route: PropTypes.shape({ - /** Route specific parameters used on this screen */ - params: PropTypes.shape({ - /** If the admin room should be opened */ - openOnAdminRoom: PropTypes.bool, - - /** The ID of the report this screen should display */ - reportID: PropTypes.string, - }), - }).isRequired, - - /* Navigation functions provided by React Navigation */ - navigation: PropTypes.shape({ - setParams: PropTypes.func.isRequired, - }).isRequired, -}; - -const defaultProps = { - reports: {}, - policies: {}, - isFirstTimeNewExpensifyUser: false, -}; - -/** - * Get the most recently accessed report for the user - * - * @param {Object} reports - * @param {Boolean} ignoreDefaultRooms - * @param {Object} policies - * @param {Boolean} isFirstTimeNewExpensifyUser - * @param {Boolean} openOnAdminRoom - * @returns {Number} - */ -const getLastAccessedReportID = (reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom) => { - const lastReport = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, isFirstTimeNewExpensifyUser, openOnAdminRoom); - return lodashGet(lastReport, 'reportID'); -}; - -// This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params -function ReportScreenIDSetter({route, reports, policies, isFirstTimeNewExpensifyUser, navigation}) { - const {canUseDefaultRooms} = usePermissions(); - - useEffect(() => { - // Don't update if there is a reportID in the params already - if (lodashGet(route, 'params.reportID', null)) { - App.confirmReadyToOpenApp(); - return; - } - - // If there is no reportID in route, try to find last accessed and use it for setParams - const reportID = getLastAccessedReportID(reports, !canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, lodashGet(route, 'params.openOnAdminRoom', false)); - - // It's possible that reports aren't fully loaded yet - // in that case the reportID is undefined - if (reportID) { - navigation.setParams({reportID: String(reportID)}); - } else { - App.confirmReadyToOpenApp(); - } - }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser]); - - // The ReportScreen without the reportID set will display a skeleton - // until the reportID is loaded and set in the route param - return null; -} - -ReportScreenIDSetter.propTypes = propTypes; -ReportScreenIDSetter.defaultProps = defaultProps; -ReportScreenIDSetter.displayName = 'ReportScreenIDSetter'; - -export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - allowStaleData: true, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - allowStaleData: true, - }, - isFirstTimeNewExpensifyUser: { - key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, - initialValue: false, - }, -})(ReportScreenIDSetter); diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts new file mode 100644 index 000000000000..8be512962981 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts @@ -0,0 +1,80 @@ +import {useEffect} from 'react'; +import {OnyxCollection, OnyxEntry, withOnyx} from 'react-native-onyx'; +import usePermissions from '@hooks/usePermissions'; +import * as ReportUtils from '@libs/ReportUtils'; +import * as App from '@userActions/App'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report} from '@src/types/onyx'; +import type {ReportScreenWrapperProps} from './ReportScreenWrapper'; + +type ReportScreenIDSetterComponentProps = { + /** Available reports that would be displayed in this navigator */ + reports: OnyxCollection; + + /** The policies which the user has access to */ + policies: OnyxCollection; + + /** Whether user is a new user */ + isFirstTimeNewExpensifyUser: OnyxEntry; +}; + +type ReportScreenIDSetterProps = ReportScreenIDSetterComponentProps & ReportScreenWrapperProps; + +/** + * Get the most recently accessed report for the user + */ +const getLastAccessedReportID = ( + reports: OnyxCollection, + ignoreDefaultRooms: boolean, + policies: OnyxCollection, + isFirstTimeNewExpensifyUser: OnyxEntry, + openOnAdminRoom: boolean, +): string | undefined => { + const lastReport = ReportUtils.findLastAccessedReport(reports, ignoreDefaultRooms, policies, !!isFirstTimeNewExpensifyUser, openOnAdminRoom); + return lastReport?.reportID; +}; + +// This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params +function ReportScreenIDSetter({route, reports, policies, navigation, isFirstTimeNewExpensifyUser = false}: ReportScreenIDSetterProps) { + const {canUseDefaultRooms} = usePermissions(); + + useEffect(() => { + // Don't update if there is a reportID in the params already + if (route?.params?.reportID) { + App.confirmReadyToOpenApp(); + return; + } + + // If there is no reportID in route, try to find last accessed and use it for setParams + const reportID = getLastAccessedReportID(reports, !canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, !!reports?.params?.openOnAdminRoom); + + // It's possible that reports aren't fully loaded yet + // in that case the reportID is undefined + if (reportID) { + navigation.setParams({reportID: String(reportID)}); + } else { + App.confirmReadyToOpenApp(); + } + }, [route, navigation, reports, canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser]); + + // The ReportScreen without the reportID set will display a skeleton + // until the reportID is loaded and set in the route param + return null; +} + +ReportScreenIDSetter.displayName = 'ReportScreenIDSetter'; + +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + allowStaleData: true, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + allowStaleData: true, + }, + isFirstTimeNewExpensifyUser: { + key: ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER, + initialValue: false, + }, +})(ReportScreenIDSetter); diff --git a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js deleted file mode 100644 index 87a8a4abc687..000000000000 --- a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.js +++ /dev/null @@ -1,45 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ReportScreen from '@pages/home/ReportScreen'; -import ReportScreenIDSetter from './ReportScreenIDSetter'; - -const propTypes = { - /** Navigation route context info provided by react navigation */ - route: PropTypes.shape({ - /** Route specific parameters used on this screen */ - params: PropTypes.shape({ - /** If the admin room should be opened */ - openOnAdminRoom: PropTypes.bool, - - /** The ID of the report this screen should display */ - reportID: PropTypes.string, - }), - }).isRequired, - - /* Navigation functions provided by React Navigation */ - navigation: PropTypes.shape({ - setParams: PropTypes.func.isRequired, - }).isRequired, -}; - -const defaultProps = {}; - -function ReportScreenWrapper(props) { - // The ReportScreen without the reportID set will display a skeleton - // until the reportID is loaded and set in the route param - return ( - <> - - - - ); -} - -ReportScreenWrapper.propTypes = propTypes; -ReportScreenWrapper.defaultProps = defaultProps; -ReportScreenWrapper.displayName = 'ReportScreenWrapper'; - -export default ReportScreenWrapper; diff --git a/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx new file mode 100644 index 000000000000..20922fd785ce --- /dev/null +++ b/src/libs/Navigation/AppNavigator/ReportScreenWrapper.tsx @@ -0,0 +1,28 @@ +import {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import ReportScreen from '@pages/home/ReportScreen'; +import SCREENS from '@src/SCREENS'; +import ReportScreenIDSetter from './ReportScreenIDSetter'; + +type ReportScreenWrapperProps = StackScreenProps; + +function ReportScreenWrapper({route, navigation}: ReportScreenWrapperProps) { + // The ReportScreen without the reportID set will display a skeleton + // until the reportID is loaded and set in the route param + return ( + <> + {/* @ts-expect-error Error will be resolved after ReportScreen migration to TypeScript */} + + + + ); +} + +ReportScreenWrapper.displayName = 'ReportScreenWrapper'; + +export default ReportScreenWrapper; +export type {ReportScreenWrapperProps}; diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.js deleted file mode 100644 index 5d3eb38d49dc..000000000000 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.js +++ /dev/null @@ -1,84 +0,0 @@ -import {StackRouter} from '@react-navigation/native'; -import lodashFindLast from 'lodash/findLast'; -import _ from 'underscore'; -import NAVIGATORS from '@src/NAVIGATORS'; -import SCREENS from '@src/SCREENS'; - -/** - * @param {Object} state - react-navigation state - * @returns {Boolean} - */ -const isAtLeastOneCentralPaneNavigatorInState = (state) => _.find(state.routes, (r) => r.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR); - -/** - * @param {Object} state - react-navigation state - * @returns {String} - */ -const getTopMostReportIDFromRHP = (state) => { - if (!state) { - return ''; - } - const topmostRightPane = lodashFindLast(state.routes, (route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR); - - if (topmostRightPane) { - return getTopMostReportIDFromRHP(topmostRightPane.state); - } - - const topmostRoute = lodashFindLast(state.routes); - - if (topmostRoute.state) { - return getTopMostReportIDFromRHP(topmostRoute.state); - } - - if (topmostRoute.params && topmostRoute.params.reportID) { - return topmostRoute.params.reportID; - } - - return ''; -}; -/** - * Adds report route without any specific reportID to the state. - * The report screen will self set proper reportID param based on the helper function findLastAccessedReport (look at ReportScreenWrapper for more info) - * - * @param {Object} state - react-navigation state - */ -const addCentralPaneNavigatorRoute = (state) => { - const reportID = getTopMostReportIDFromRHP(state); - const centralPaneNavigatorRoute = { - name: NAVIGATORS.CENTRAL_PANE_NAVIGATOR, - state: { - routes: [ - { - name: SCREENS.REPORT, - params: { - reportID, - }, - }, - ], - }, - }; - state.routes.splice(1, 0, centralPaneNavigatorRoute); - // eslint-disable-next-line no-param-reassign - state.index = state.routes.length - 1; -}; - -function CustomRouter(options) { - const stackRouter = StackRouter(options); - - return { - ...stackRouter, - getRehydratedState(partialState, {routeNames, routeParamList}) { - // Make sure that there is at least one CentralPaneNavigator (ReportScreen by default) in the state if this is a wide layout - if (!isAtLeastOneCentralPaneNavigatorInState(partialState) && !options.getIsSmallScreenWidth()) { - // If we added a route we need to make sure that the state.stale is true to generate new key for this route - // eslint-disable-next-line no-param-reassign - partialState.stale = true; - addCentralPaneNavigatorRoute(partialState); - } - const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList}); - return state; - }, - }; -} - -export default CustomRouter; diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts new file mode 100644 index 000000000000..435ebc00362b --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/CustomRouter.ts @@ -0,0 +1,82 @@ +import {NavigationState, PartialState, RouterConfigOptions, StackNavigationState, StackRouter} from '@react-navigation/native'; +import {ParamListBase} from '@react-navigation/routers'; +import getIsSmallScreenWidth from '@libs/getIsSmallScreenWidth'; +import NAVIGATORS from '@src/NAVIGATORS'; +import SCREENS from '@src/SCREENS'; +import type {ResponsiveStackNavigatorRouterOptions} from './types'; + +type State = NavigationState | PartialState; + +const isAtLeastOneCentralPaneNavigatorInState = (state: State): boolean => !!state.routes.find((route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR); + +const getTopMostReportIDFromRHP = (state: State): string => { + if (!state) { + return ''; + } + + const topmostRightPane = state.routes.filter((route) => route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR).at(-1); + + if (topmostRightPane?.state) { + return getTopMostReportIDFromRHP(topmostRightPane.state); + } + + const topmostRoute = state.routes.at(-1); + + if (topmostRoute?.state) { + return getTopMostReportIDFromRHP(topmostRoute.state); + } + + if (topmostRoute?.params && 'reportID' in topmostRoute.params && typeof topmostRoute.params.reportID === 'string' && topmostRoute.params.reportID) { + return topmostRoute.params.reportID; + } + + return ''; +}; +/** + * Adds report route without any specific reportID to the state. + * The report screen will self set proper reportID param based on the helper function findLastAccessedReport (look at ReportScreenWrapper for more info) + * + * @param state - react-navigation state + */ +const addCentralPaneNavigatorRoute = (state: State) => { + const reportID = getTopMostReportIDFromRHP(state); + const centralPaneNavigatorRoute = { + name: NAVIGATORS.CENTRAL_PANE_NAVIGATOR, + state: { + routes: [ + { + name: SCREENS.REPORT, + params: { + reportID, + }, + }, + ], + }, + }; + state.routes.splice(1, 0, centralPaneNavigatorRoute); + // eslint-disable-next-line no-param-reassign, @typescript-eslint/non-nullable-type-assertion-style + (state.index as number) = state.routes.length - 1; +}; + +function CustomRouter(options: ResponsiveStackNavigatorRouterOptions) { + const stackRouter = StackRouter(options); + + return { + ...stackRouter, + getRehydratedState(partialState: StackNavigationState, {routeNames, routeParamList, routeGetIdList}: RouterConfigOptions): StackNavigationState { + const isSmallScreenWidth = getIsSmallScreenWidth(); + // Make sure that there is at least one CentralPaneNavigator (ReportScreen by default) in the state if this is a wide layout + if (!isAtLeastOneCentralPaneNavigatorInState(partialState) && !isSmallScreenWidth) { + // If we added a route we need to make sure that the state.stale is true to generate new key for this route + + // eslint-disable-next-line no-param-reassign + (partialState.stale as boolean) = true; + addCentralPaneNavigatorRoute(partialState); + } + const state = stackRouter.getRehydratedState(partialState, {routeNames, routeParamList, routeGetIdList}); + return state; + }, + }; +} + +export default CustomRouter; diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js deleted file mode 100644 index ae36f4aff9ad..000000000000 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js +++ /dev/null @@ -1,60 +0,0 @@ -import {createNavigatorFactory, useNavigationBuilder} from '@react-navigation/native'; -import {StackView} from '@react-navigation/stack'; -import PropTypes from 'prop-types'; -import React, {useRef} from 'react'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import CustomRouter from './CustomRouter'; - -const propTypes = { - /* Determines if the navigator should render the StackView (narrow) or ThreePaneView (wide) */ - isSmallScreenWidth: PropTypes.bool.isRequired, - - /* Children for the useNavigationBuilder hook */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /* initialRouteName for this navigator */ - initialRouteName: PropTypes.oneOf([PropTypes.string, PropTypes.undefined]), - - /* Screen options defined for this navigator */ - // eslint-disable-next-line react/forbid-prop-types - screenOptions: PropTypes.object, -}; - -const defaultProps = { - initialRouteName: undefined, - screenOptions: undefined, -}; - -function ResponsiveStackNavigator(props) { - const {isSmallScreenWidth} = useWindowDimensions(); - - const isSmallScreenWidthRef = useRef(isSmallScreenWidth); - - isSmallScreenWidthRef.current = isSmallScreenWidth; - - const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder(CustomRouter, { - children: props.children, - screenOptions: props.screenOptions, - initialRouteName: props.initialRouteName, - // Options for useNavigationBuilder won't update on prop change, so we need to pass a getter for the router to have the current state of isSmallScreenWidth. - getIsSmallScreenWidth: () => isSmallScreenWidthRef.current, - }); - - return ( - - - - ); -} - -ResponsiveStackNavigator.defaultProps = defaultProps; -ResponsiveStackNavigator.propTypes = propTypes; -ResponsiveStackNavigator.displayName = 'ResponsiveStackNavigator'; - -export default createNavigatorFactory(ResponsiveStackNavigator); diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.tsx b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.tsx new file mode 100644 index 000000000000..a55c74f3a479 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.tsx @@ -0,0 +1,42 @@ +import {createNavigatorFactory, ParamListBase, StackActionHelpers, StackNavigationState, useNavigationBuilder} from '@react-navigation/native'; +import {StackNavigationEventMap, StackNavigationOptions, StackView} from '@react-navigation/stack'; +import React, {useRef} from 'react'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import CustomRouter from './CustomRouter'; +import type {ResponsiveStackNavigatorProps, ResponsiveStackNavigatorRouterOptions} from './types'; + +function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) { + const {isSmallScreenWidth} = useWindowDimensions(); + + const isSmallScreenWidthRef = useRef(isSmallScreenWidth); + + isSmallScreenWidthRef.current = isSmallScreenWidth; + + const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder< + StackNavigationState, + ResponsiveStackNavigatorRouterOptions, + StackActionHelpers, + StackNavigationOptions, + StackNavigationEventMap + >(CustomRouter, { + children: props.children, + screenOptions: props.screenOptions, + initialRouteName: props.initialRouteName, + }); + + return ( + + + + ); +} + +ResponsiveStackNavigator.displayName = 'ResponsiveStackNavigator'; + +export default createNavigatorFactory, StackNavigationOptions, StackNavigationEventMap, typeof ResponsiveStackNavigator>(ResponsiveStackNavigator); diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx similarity index 52% rename from src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js rename to src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx index 8924b01e2acb..dd2e548064c4 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.tsx @@ -1,33 +1,14 @@ -import {createNavigatorFactory, useNavigationBuilder} from '@react-navigation/native'; -import {StackView} from '@react-navigation/stack'; -import PropTypes from 'prop-types'; +import {createNavigatorFactory, ParamListBase, StackActionHelpers, StackNavigationState, useNavigationBuilder} from '@react-navigation/native'; +import {StackNavigationEventMap, StackNavigationOptions, StackView} from '@react-navigation/stack'; import React, {useMemo, useRef} from 'react'; import useWindowDimensions from '@hooks/useWindowDimensions'; import NAVIGATORS from '@src/NAVIGATORS'; import CustomRouter from './CustomRouter'; +import type {ResponsiveStackNavigatorProps, ResponsiveStackNavigatorRouterOptions} from './types'; -const propTypes = { - /* Determines if the navigator should render the StackView (narrow) or ThreePaneView (wide) */ - isSmallScreenWidth: PropTypes.bool.isRequired, - - /* Children for the useNavigationBuilder hook */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, - - /* initialRouteName for this navigator */ - initialRouteName: PropTypes.oneOf([PropTypes.string, PropTypes.undefined]), - - /* Screen options defined for this navigator */ - // eslint-disable-next-line react/forbid-prop-types - screenOptions: PropTypes.object, -}; - -const defaultProps = { - initialRouteName: undefined, - screenOptions: undefined, -}; - -function reduceReportRoutes(routes) { - const result = []; +type Routes = StackNavigationState['routes']; +function reduceReportRoutes(routes: Routes): Routes { + const result: Routes = []; let count = 0; const reverseRoutes = [...routes].reverse(); @@ -46,19 +27,23 @@ function reduceReportRoutes(routes) { return result.reverse(); } -function ResponsiveStackNavigator(props) { +function ResponsiveStackNavigator(props: ResponsiveStackNavigatorProps) { const {isSmallScreenWidth} = useWindowDimensions(); - const isSmallScreenWidthRef = useRef(isSmallScreenWidth); + const isSmallScreenWidthRef = useRef(isSmallScreenWidth); isSmallScreenWidthRef.current = isSmallScreenWidth; - const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder(CustomRouter, { + const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder< + StackNavigationState, + ResponsiveStackNavigatorRouterOptions, + StackActionHelpers, + StackNavigationOptions, + StackNavigationEventMap + >(CustomRouter, { children: props.children, screenOptions: props.screenOptions, initialRouteName: props.initialRouteName, - // Options for useNavigationBuilder won't update on prop change, so we need to pass a getter for the router to have the current state of isSmallScreenWidth. - getIsSmallScreenWidth: () => isSmallScreenWidthRef.current, }); const stateToRender = useMemo(() => { @@ -84,8 +69,6 @@ function ResponsiveStackNavigator(props) { ); } -ResponsiveStackNavigator.defaultProps = defaultProps; -ResponsiveStackNavigator.propTypes = propTypes; ResponsiveStackNavigator.displayName = 'ResponsiveStackNavigator'; -export default createNavigatorFactory(ResponsiveStackNavigator); +export default createNavigatorFactory, StackNavigationOptions, StackNavigationEventMap, typeof ResponsiveStackNavigator>(ResponsiveStackNavigator); diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/types.ts b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/types.ts new file mode 100644 index 000000000000..707a0ff4498d --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/types.ts @@ -0,0 +1,13 @@ +import {DefaultNavigatorOptions, ParamListBase, StackNavigationState, StackRouterOptions} from '@react-navigation/native'; +import {StackNavigationEventMap, StackNavigationOptions} from '@react-navigation/stack'; + +type ResponsiveStackNavigatorConfig = { + isSmallScreenWidth: boolean; +}; + +type ResponsiveStackNavigatorRouterOptions = StackRouterOptions; + +type ResponsiveStackNavigatorProps = DefaultNavigatorOptions, StackNavigationOptions, StackNavigationEventMap> & + ResponsiveStackNavigatorConfig; + +export type {ResponsiveStackNavigatorRouterOptions, ResponsiveStackNavigatorProps, ResponsiveStackNavigatorConfig}; diff --git a/src/libs/Navigation/AppNavigator/defaultScreenOptions.js b/src/libs/Navigation/AppNavigator/defaultScreenOptions.ts similarity index 58% rename from src/libs/Navigation/AppNavigator/defaultScreenOptions.js rename to src/libs/Navigation/AppNavigator/defaultScreenOptions.ts index 3ccffb5f09ab..65a6bd052742 100644 --- a/src/libs/Navigation/AppNavigator/defaultScreenOptions.js +++ b/src/libs/Navigation/AppNavigator/defaultScreenOptions.ts @@ -1,4 +1,6 @@ -const defaultScreenOptions = { +import {StackNavigationOptions} from '@react-navigation/stack'; + +const defaultScreenOptions: StackNavigationOptions = { cardStyle: { overflow: 'visible', flex: 1, diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts similarity index 67% rename from src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js rename to src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts index 44fa7b6c0b09..08f18ce3ab9d 100644 --- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.js +++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts @@ -1,9 +1,13 @@ +import {StackCardInterpolationProps, StackNavigationOptions} from '@react-navigation/stack'; import getNavigationModalCardStyle from '@styles/getNavigationModalCardStyles'; +import styles from '@styles/styles'; import variables from '@styles/variables'; import CONFIG from '@src/CONFIG'; import modalCardStyleInterpolator from './modalCardStyleInterpolator'; -const commonScreenOptions = { +type ScreenOptions = Record; + +const commonScreenOptions: StackNavigationOptions = { headerShown: false, gestureDirection: 'horizontal', animationEnabled: true, @@ -11,10 +15,10 @@ const commonScreenOptions = { animationTypeForReplace: 'push', }; -export default (isSmallScreenWidth, styles) => ({ +export default (isSmallScreenWidth: boolean, themeStyles: typeof styles): ScreenOptions => ({ rightModalNavigator: { ...commonScreenOptions, - cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), + cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), presentation: 'transparentModal', // We want pop in RHP since there are some flows that would work weird otherwise @@ -32,7 +36,7 @@ export default (isSmallScreenWidth, styles) => ({ homeScreen: { title: CONFIG.SITE_TITLE, ...commonScreenOptions, - cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), + cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), cardStyle: { ...getNavigationModalCardStyle(), @@ -40,13 +44,13 @@ export default (isSmallScreenWidth, styles) => ({ // We need to translate the sidebar to not be covered by the StackNavigator so it can be clickable. transform: [{translateX: isSmallScreenWidth ? 0 : -variables.sideBarWidth}], - ...(isSmallScreenWidth ? {} : styles.borderRight), + ...(isSmallScreenWidth ? {} : themeStyles.borderRight), }, }, - // eslint-disable-next-line rulesdir/no-negated-variables + fullScreen: { ...commonScreenOptions, - cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, true, props), + cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props), cardStyle: { ...getNavigationModalCardStyle(), @@ -59,7 +63,7 @@ export default (isSmallScreenWidth, styles) => ({ title: CONFIG.SITE_TITLE, ...commonScreenOptions, animationEnabled: isSmallScreenWidth, - cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, true, props), + cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props), cardStyle: { ...getNavigationModalCardStyle(), diff --git a/src/libs/Navigation/AppNavigator/index.js b/src/libs/Navigation/AppNavigator/index.tsx similarity index 68% rename from src/libs/Navigation/AppNavigator/index.js rename to src/libs/Navigation/AppNavigator/index.tsx index 0d03badf37bc..8d65f5166060 100644 --- a/src/libs/Navigation/AppNavigator/index.js +++ b/src/libs/Navigation/AppNavigator/index.tsx @@ -1,13 +1,12 @@ -import PropTypes from 'prop-types'; import React from 'react'; -const propTypes = { +type AppNavigatorProps = { /** If we have an authToken this is true */ - authenticated: PropTypes.bool.isRequired, + authenticated: boolean; }; -function AppNavigator(props) { - if (props.authenticated) { +function AppNavigator({authenticated}: AppNavigatorProps) { + if (authenticated) { const AuthScreens = require('./AuthScreens').default; // These are the protected screens and only accessible when an authToken is present @@ -17,6 +16,5 @@ function AppNavigator(props) { return ; } -AppNavigator.propTypes = propTypes; AppNavigator.displayName = 'AppNavigator'; export default AppNavigator; diff --git a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts similarity index 69% rename from src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js rename to src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts index 446d195fc466..f7e772148e79 100644 --- a/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.js +++ b/src/libs/Navigation/AppNavigator/modalCardStyleInterpolator.ts @@ -1,8 +1,9 @@ +import type {StackCardInterpolatedStyle, StackCardInterpolationProps} from '@react-navigation/stack'; import {Animated} from 'react-native'; import getCardStyles from '@styles/cardStyles'; import variables from '@styles/variables'; -export default (isSmallScreenWidth, isFullScreenModal, {current: {progress}, inverted, layouts: {screen}}) => { +export default (isSmallScreenWidth: boolean, isFullScreenModal: boolean, {current: {progress}, inverted, layouts: {screen}}: StackCardInterpolationProps): StackCardInterpolatedStyle => { const translateX = Animated.multiply( progress.interpolate({ inputRange: [0, 1], diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 41df21d8e237..7aa5bb32a4e4 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -37,318 +37,326 @@ type CentralPaneNavigatorParamList = { [SCREENS.REPORT]: { reportActionID: string; reportID: string; + openOnAdminRoom?: boolean; }; }; type SettingsNavigatorParamList = { [SCREENS.SETTINGS.ROOT]: undefined; - Settings_Share_Code: undefined; + [SCREENS.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.PROFILE]: undefined; + [SCREENS.SETTINGS.PRONOUNS]: undefined; + [SCREENS.SETTINGS.DISPLAY_NAME]: undefined; + [SCREENS.SETTINGS.TIMEZONE]: undefined; + [SCREENS.SETTINGS.TIMEZONE_SELECT]: undefined; + [SCREENS.SETTINGS.PERSONAL_DETAILS_INITIAL]: undefined; + [SCREENS.SETTINGS.PERSONAL_DETAILS_LEGAL_NAME]: undefined; + [SCREENS.SETTINGS.PERSONAL_DETAILS_DATE_OF_BIRTH]: undefined; + [SCREENS.SETTINGS.PERSONAL_DETAILS_ADDRESS]: undefined; + [SCREENS.SETTINGS.PERSONAL_DETAILS_ADDRESS_COUNTRY]: undefined; + [SCREENS.SETTINGS.CONTACT_METHODS]: undefined; + [SCREENS.SETTINGS.CONTACT_METHOD_DETAILS]: undefined; + [SCREENS.SETTINGS.NEW_CONTACT_METHOD]: undefined; [SCREENS.SETTINGS.PREFERENCES]: undefined; - Settings_Preferences_PriorityMode: undefined; - Settings_Preferences_Language: undefined; - Settings_Preferences_Theme: undefined; - Settings_Close: undefined; + [SCREENS.SETTINGS.PREFERENCES_PRIORITY_MODE]: undefined; + [SCREENS.SETTINGS.PREFERENCES_LANGUAGE]: undefined; + [SCREENS.SETTINGS.PREFERENCES_THEME]: undefined; + [SCREENS.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.ABOUT]: undefined; + [SCREENS.SETTINGS.APP_DOWNLOAD_LINKS]: undefined; + [SCREENS.SETTINGS.LOUNGE_ACCESS]: undefined; + [SCREENS.SETTINGS.WALLET]: undefined; + [SCREENS.SETTINGS.WALLET_CARDS_DIGITAL_DETAILS_UPDATE_ADDRESS]: undefined; + [SCREENS.SETTINGS.WALLET_DOMAIN_CARD]: undefined; + [SCREENS.SETTINGS.WALLET_REPORT_VIRTUAL_CARD_FRAUD]: undefined; + [SCREENS.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.WALLET_TRANSFER_BALANCE]: undefined; + [SCREENS.SETTINGS.WALLET_CHOOSE_TRANSFER_ACCOUNT]: undefined; + [SCREENS.SETTINGS.WALLET_ENABLE_PAYMENTS]: undefined; + [SCREENS.SETTINGS.ADD_DEBIT_CARD]: undefined; + [SCREENS.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: { + [SCREENS.SETTINGS.STATUS_SET]: undefined; + [SCREENS.WORKSPACE.INITIAL]: undefined; + [SCREENS.WORKSPACE.SETTINGS]: undefined; + [SCREENS.WORKSPACE.CURRENCY]: undefined; + [SCREENS.WORKSPACE.CARD]: { policyID: string; }; - Workspace_Reimburse: { + [SCREENS.WORKSPACE.REIMBURSE]: { policyID: string; }; - Workspace_RateAndUnit: undefined; - Workspace_Bills: { + [SCREENS.WORKSPACE.RATE_AND_UNIT]: undefined; + [SCREENS.WORKSPACE.BILLS]: { policyID: string; }; - Workspace_Invoices: { + [SCREENS.WORKSPACE.INVOICES]: { policyID: string; }; - Workspace_Travel: { + [SCREENS.WORKSPACE.TRAVEL]: { policyID: string; }; - Workspace_Members: { + [SCREENS.WORKSPACE.MEMBERS]: { policyID: string; }; - Workspace_Invite: { + [SCREENS.WORKSPACE.INVITE]: { policyID: string; }; - Workspace_Invite_Message: { + [SCREENS.WORKSPACE.INVITE_MESSAGE]: { policyID: string; }; - ReimbursementAccount: { + [SCREENS.REIMBURSEMENT_ACCOUNT]: { stepToOpen: string; policyID: string; }; - GetAssistance: { + [SCREENS.GET_ASSISTANCE]: { taskID: string; }; - Settings_TwoFactorAuth: undefined; - Settings_ReportCardLostOrDamaged: undefined; - KeyboardShortcuts: undefined; + [SCREENS.SETTINGS.TWO_FACTOR_AUTH]: undefined; + [SCREENS.SETTINGS.REPORT_CARD_LOST_OR_DAMAGED]: undefined; + [SCREENS.KEYBOARD_SHORTCUTS]: undefined; }; type NewChatNavigatorParamList = { - NewChat_Root: undefined; + [SCREENS.NEW_CHAT_ROOT]: undefined; }; type SearchNavigatorParamList = { - Search_Root: undefined; + [SCREENS.SEARCH_ROOT]: undefined; }; type DetailsNavigatorParamList = { - Details_Root: { + [SCREENS.DETAILS_ROOT]: { login: string; reportID: string; }; }; type ProfileNavigatorParamList = { - Profile_Root: { + [SCREENS.PROFILE_ROOT]: { accountID: string; reportID: string; }; }; type ReportDetailsNavigatorParamList = { - Report_Details_Root: undefined; - Report_Details_Share_Code: { + [SCREENS.REPORT_DETAILS.ROOT]: undefined; + [SCREENS.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; + [SCREENS.REPORT_SETTINGS.ROOT]: undefined; + [SCREENS.REPORT_SETTINGS.ROOM_NAME]: undefined; + [SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: undefined; + [SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: undefined; }; type ReportWelcomeMessageNavigatorParamList = { - Report_WelcomeMessage_Root: {reportID: string}; + [SCREENS.REPORT_WELCOME_MESSAGE_ROOT]: {reportID: string}; }; type ParticipantsNavigatorParamList = { - ReportParticipants_Root: {reportID: string}; + [SCREENS.REPORT_PARTICIPANTS_ROOT]: {reportID: string}; }; type RoomMembersNavigatorParamList = { - RoomMembers_Root: undefined; + [SCREENS.ROOM_MEMBERS_ROOT]: undefined; }; type RoomInviteNavigatorParamList = { - RoomInvite_Root: undefined; + [SCREENS.ROOM_INVITE_ROOT]: undefined; }; type MoneyRequestNavigatorParamList = { - Money_Request: undefined; - Money_Request_Amount: undefined; - Money_Request_Participants: { + [SCREENS.MONEY_REQUEST.ROOT]: undefined; + [SCREENS.MONEY_REQUEST.AMOUNT]: undefined; + [SCREENS.MONEY_REQUEST.PARTICIPANTS]: { iouType: string; reportID: string; }; - Money_Request_Confirmation: { + [SCREENS.MONEY_REQUEST.CONFIRMATION]: { iouType: string; reportID: string; }; - Money_Request_Currency: { + [SCREENS.MONEY_REQUEST.CURRENCY]: { iouType: string; reportID: string; currency: string; backTo: string; }; - Money_Request_Date: { + [SCREENS.MONEY_REQUEST.DATE]: { iouType: string; reportID: string; field: string; threadReportID: string; }; - Money_Request_Description: { + [SCREENS.MONEY_REQUEST.DESCRIPTION]: { iouType: string; reportID: string; field: string; threadReportID: string; }; - Money_Request_Category: { + [SCREENS.MONEY_REQUEST.CATEGORY]: { iouType: string; reportID: string; }; - Money_Request_Tag: { + [SCREENS.MONEY_REQUEST.TAG]: { iouType: string; reportID: string; }; - Money_Request_Merchant: { + [SCREENS.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: { + [SCREENS.IOU_SEND.ENABLE_PAYMENTS]: undefined; + [SCREENS.IOU_SEND.ADD_BANK_ACCOUNT]: undefined; + [SCREENS.IOU_SEND.ADD_DEBIT_CARD]: undefined; + [SCREENS.MONEY_REQUEST.WAYPOINT]: { iouType: string; transactionID: string; waypointIndex: string; threadReportID: number; }; - Money_Request_Edit_Waypoint: { + [SCREENS.MONEY_REQUEST.EDIT_WAYPOINT]: { iouType: string; transactionID: string; waypointIndex: string; threadReportID: number; }; - Money_Request_Distance: { + [SCREENS.MONEY_REQUEST.DISTANCE]: { iouType: ValueOf; reportID: string; }; - Money_Request_Receipt: { + [SCREENS.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; + [SCREENS.NEW_TASK.ROOT]: undefined; + [SCREENS.NEW_TASK.TASK_ASSIGNEE_SELECTOR]: undefined; + [SCREENS.NEW_TASK.TASK_SHARE_DESTINATION_SELECTOR]: undefined; + [SCREENS.NEW_TASK.DETAILS]: undefined; + [SCREENS.NEW_TASK.TITLE]: undefined; + [SCREENS.NEW_TASK.DESCRIPTION]: undefined; }; type TeachersUniteNavigatorParamList = { [SCREENS.SAVE_THE_WORLD.ROOT]: undefined; - I_Know_A_Teacher: undefined; - Intro_School_Principal: undefined; - I_Am_A_Teacher: undefined; + [SCREENS.I_KNOW_A_TEACHER]: undefined; + [SCREENS.INTRO_SCHOOL_PRINCIPAL]: undefined; + [SCREENS.I_AM_A_TEACHER]: undefined; }; type TaskDetailsNavigatorParamList = { - Task_Title: undefined; - Task_Description: undefined; - Task_Assignee: { + [SCREENS.TASK.TITLE]: undefined; + [SCREENS.TASK.DESCRIPTION]: undefined; + [SCREENS.TASK.ASSIGNEE]: { reportID: string; }; }; type EnablePaymentsNavigatorParamList = { - EnablePayments_Root: undefined; + [SCREENS.ENABLE_PAYMENTS_ROOT]: undefined; }; type SplitDetailsNavigatorParamList = { - SplitDetails_Root: { + [SCREENS.SPLIT_DETAILS.ROOT]: { reportActionID: string; }; - SplitDetails_Edit_Request: undefined; - SplitDetails_Edit_Currency: undefined; + [SCREENS.SPLIT_DETAILS.EDIT_REQUEST]: undefined; + [SCREENS.SPLIT_DETAILS.EDIT_CURRENCY]: undefined; }; type AddPersonalBankAccountNavigatorParamList = { - AddPersonalBankAccount_Root: undefined; + [SCREENS.ADD_PERSONAL_BANK_ACCOUNT_ROOT]: undefined; +}; + +type ReimbursementAccountNavigatorParamList = { + [SCREENS.REIMBURSEMENT_ACCOUNT_ROOT]: { + stepToOpen: string; + policyID: string; + }; }; type WalletStatementNavigatorParamList = { - WalletStatement_Root: undefined; + [SCREENS.WALLET_STATEMENT_ROOT]: undefined; }; type FlagCommentNavigatorParamList = { - FlagComment_Root: { + [SCREENS.FLAG_COMMENT_ROOT]: { reportID: string; reportActionID: string; }; }; type EditRequestNavigatorParamList = { - EditRequest_Root: { + [SCREENS.EDIT_REQUEST.ROOT]: { field: string; threadReportID: string; }; - EditRequest_Currency: undefined; + [SCREENS.EDIT_REQUEST.CURRENCY]: undefined; }; type SignInNavigatorParamList = { - SignIn_Root: undefined; + [SCREENS.SIGN_IN_ROOT]: undefined; }; type ReferralDetailsNavigatorParamList = { - Referral_Details: undefined; + [SCREENS.REFERRAL_DETAILS]: undefined; }; type PrivateNotesNavigatorParamList = { - PrivateNotes_View: { + [SCREENS.PRIVATE_NOTES.VIEW]: { reportID: string; accountID: string; }; - PrivateNotes_List: { + [SCREENS.PRIVATE_NOTES.LIST]: { reportID: string; accountID: string; }; - PrivateNotes_Edit: { + [SCREENS.PRIVATE_NOTES.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; + [SCREENS.RIGHT_MODAL.SETTINGS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.NEW_CHAT]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.SEARCH]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.REPORT_WELCOME_MESSAGE]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.PARTICIPANTS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.ROOM_MEMBERS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.ROOM_INVITE]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.MONEY_REQUEST]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.NEW_TASK]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.TASK_DETAILS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.ENABLE_PAYMENTS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.SPLIT_DETAILS]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.ADD_PERSONAL_BANK_ACCOUNT]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.WALLET_STATEMENT]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.FLAG_COMMENT]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.EDIT_REQUEST]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.SIGN_IN]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.REFERRAL]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: NavigatorScreenParams; }; type PublicScreensParamList = { @@ -390,6 +398,12 @@ type AuthScreensParamList = { reportID: string; source: string; }; + [CONST.DEMO_PAGES.SAASTR]: { + name: string; + }; + [CONST.DEMO_PAGES.SBE]: { + name: string; + }; [SCREENS.NOT_FOUND]: undefined; [NAVIGATORS.RIGHT_MODAL_NAVIGATOR]: NavigatorScreenParams; [SCREENS.DESKTOP_SIGN_IN_REDIRECT]: undefined; @@ -398,4 +412,40 @@ type AuthScreensParamList = { type RootStackParamList = PublicScreensParamList & AuthScreensParamList; -export type {NavigationRef, StackNavigationAction, CentralPaneNavigatorParamList, RootStackParamList, StateOrRoute, NavigationStateRoute, NavigationRoot}; +export type { + NavigationRef, + StackNavigationAction, + CentralPaneNavigatorParamList, + RootStackParamList, + StateOrRoute, + NavigationStateRoute, + NavigationRoot, + AuthScreensParamList, + RightModalNavigatorParamList, + PublicScreensParamList, + MoneyRequestNavigatorParamList, + SplitDetailsNavigatorParamList, + DetailsNavigatorParamList, + ProfileNavigatorParamList, + ReportDetailsNavigatorParamList, + ReportSettingsNavigatorParamList, + TaskDetailsNavigatorParamList, + ReportWelcomeMessageNavigatorParamList, + ParticipantsNavigatorParamList, + RoomMembersNavigatorParamList, + RoomInviteNavigatorParamList, + SearchNavigatorParamList, + NewChatNavigatorParamList, + NewTaskNavigatorParamList, + TeachersUniteNavigatorParamList, + SettingsNavigatorParamList, + EnablePaymentsNavigatorParamList, + AddPersonalBankAccountNavigatorParamList, + WalletStatementNavigatorParamList, + FlagCommentNavigatorParamList, + EditRequestNavigatorParamList, + PrivateNotesNavigatorParamList, + SignInNavigatorParamList, + ReferralDetailsNavigatorParamList, + ReimbursementAccountNavigatorParamList, +}; diff --git a/src/libs/NetworkConnection.ts b/src/libs/NetworkConnection.ts index e150b8b650c1..f5c391aad09c 100644 --- a/src/libs/NetworkConnection.ts +++ b/src/libs/NetworkConnection.ts @@ -13,7 +13,7 @@ let hasPendingNetworkCheck = false; // Holds all of the callbacks that need to be triggered when the network reconnects let callbackID = 0; -const reconnectionCallbacks: Record Promise> = {}; +const reconnectionCallbacks: Record void> = {}; /** * Loop over all reconnection callbacks and fire each one @@ -122,7 +122,7 @@ function listenForReconnect() { * Register callback to fire when we reconnect * @returns unsubscribe method */ -function onReconnect(callback: () => Promise): () => void { +function onReconnect(callback: () => void): () => void { const currentID = callbackID; callbackID++; reconnectionCallbacks[currentID] = callback; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts similarity index 77% rename from src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts index b36c0d0c7d18..5eef0b44a7d1 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.js +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.android.ts @@ -1,5 +1,6 @@ import Airship from '@ua/react-native-airship'; import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification'; +import ForegroundNotificationsModule from './types'; function configureForegroundNotifications() { Airship.push.android.setForegroundDisplayPredicate((pushPayload) => Promise.resolve(shouldShowPushNotification(pushPayload))); @@ -9,7 +10,9 @@ function disableForegroundNotifications() { Airship.push.android.setForegroundDisplayPredicate(() => Promise.resolve(false)); } -export default { +const ForegroundNotifications: ForegroundNotificationsModule = { configureForegroundNotifications, disableForegroundNotifications, }; + +export default ForegroundNotifications; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts similarity index 88% rename from src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts index 0f0929951a90..e5e5665d1ea2 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.js +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ios.ts @@ -1,5 +1,6 @@ import Airship, {iOS} from '@ua/react-native-airship'; import shouldShowPushNotification from '@libs/Notification/PushNotification/shouldShowPushNotification'; +import ForegroundNotificationsModule from './types'; function configureForegroundNotifications() { // Set our default iOS foreground presentation to be loud with a banner @@ -20,7 +21,9 @@ function disableForegroundNotifications() { Airship.push.iOS.setForegroundPresentationOptionsCallback(() => Promise.resolve([])); } -export default { +const ForegroundNotifications: ForegroundNotificationsModule = { configureForegroundNotifications, disableForegroundNotifications, }; + +export default ForegroundNotifications; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/index.js b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts similarity index 58% rename from src/libs/Notification/PushNotification/ForegroundNotifications/index.js rename to src/libs/Notification/PushNotification/ForegroundNotifications/index.ts index acb116f7bc43..25baa34099b6 100644 --- a/src/libs/Notification/PushNotification/ForegroundNotifications/index.js +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/index.ts @@ -1,7 +1,11 @@ +import ForegroundNotificationsModule from './types'; + /** * Configures notification handling while in the foreground on iOS and Android. This is a no-op on other platforms. */ -export default { +const ForegroundNotifications: ForegroundNotificationsModule = { configureForegroundNotifications: () => {}, disableForegroundNotifications: () => {}, }; + +export default ForegroundNotifications; diff --git a/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts b/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts new file mode 100644 index 000000000000..f84934651259 --- /dev/null +++ b/src/libs/Notification/PushNotification/ForegroundNotifications/types.ts @@ -0,0 +1,6 @@ +type ForegroundNotificationsModule = { + configureForegroundNotifications: () => void; + disableForegroundNotifications: () => void; +}; + +export default ForegroundNotificationsModule; diff --git a/src/libs/Notification/PushNotification/NotificationType.js b/src/libs/Notification/PushNotification/NotificationType.js deleted file mode 100644 index 092a48fe7815..000000000000 --- a/src/libs/Notification/PushNotification/NotificationType.js +++ /dev/null @@ -1,7 +0,0 @@ -/** - * See https://github.com/Expensify/Web-Expensify/blob/main/lib/MobilePushNotifications.php for the various - * types of push notifications sent by our API. - */ -export default { - REPORT_COMMENT: 'reportComment', -}; diff --git a/src/libs/Notification/PushNotification/NotificationType.ts b/src/libs/Notification/PushNotification/NotificationType.ts new file mode 100644 index 000000000000..91eec6895394 --- /dev/null +++ b/src/libs/Notification/PushNotification/NotificationType.ts @@ -0,0 +1,28 @@ +import {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; + +const NotificationType = { + REPORT_COMMENT: 'reportComment', +} as const; + +type NotificationDataMap = { + [NotificationType.REPORT_COMMENT]: ReportCommentNotificationData; +}; + +type NotificationData = ReportCommentNotificationData; + +type ReportCommentNotificationData = { + title: string; + type: typeof NotificationType.REPORT_COMMENT; + reportID: number; + reportActionID: string; + shouldScrollToLastUnread?: boolean; + roomName?: string; + onyxData?: OnyxServerUpdate[]; +}; + +/** + * See https://github.com/Expensify/Web-Expensify/blob/main/lib/MobilePushNotifications.php for the various + * types of push notifications sent by our API. + */ +export default NotificationType; +export type {NotificationDataMap, NotificationData, ReportCommentNotificationData}; diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/index.android.js b/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts similarity index 86% rename from src/libs/Notification/PushNotification/backgroundRefresh/index.android.js rename to src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts index 4502011b459e..2b3c6ebf21b4 100644 --- a/src/libs/Notification/PushNotification/backgroundRefresh/index.android.js +++ b/src/libs/Notification/PushNotification/backgroundRefresh/index.android.ts @@ -4,8 +4,9 @@ import Visibility from '@libs/Visibility'; import * as App from '@userActions/App'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import BackgroundRefresh from './types'; -function getLastOnyxUpdateID() { +function getLastOnyxUpdateID(): Promise { return new Promise((resolve) => { const connectionID = Onyx.connect({ key: ONYXKEYS.ONYX_UPDATES_LAST_UPDATE_ID_APPLIED_TO_CLIENT, @@ -23,7 +24,7 @@ function getLastOnyxUpdateID() { * We use this to refresh the app in the background after receiving a push notification (native only). Since the full app * wakes on iOS and by extension runs reconnectApp already, this is a no-op on everything but Android. */ -export default function backgroundRefresh() { +const backgroundRefresh: BackgroundRefresh = () => { if (Visibility.isVisible()) { return; } @@ -38,9 +39,11 @@ export default function backgroundRefresh() { * See more here: https://reactnative.dev/docs/headless-js-android */ App.confirmReadyToOpenApp(); - App.reconnectApp(lastUpdateIDAppliedToClient); + App.reconnectApp(lastUpdateIDAppliedToClient ?? undefined); }) .catch((error) => { Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} [PushNotification] backgroundRefresh failed. This should never happen.`, {error}); }); -} +}; + +export default backgroundRefresh; diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/index.js b/src/libs/Notification/PushNotification/backgroundRefresh/index.ts similarity index 69% rename from src/libs/Notification/PushNotification/backgroundRefresh/index.js rename to src/libs/Notification/PushNotification/backgroundRefresh/index.ts index 657fb15ee429..c7f47a532d89 100644 --- a/src/libs/Notification/PushNotification/backgroundRefresh/index.js +++ b/src/libs/Notification/PushNotification/backgroundRefresh/index.ts @@ -1,7 +1,11 @@ +import BackgroundRefresh from './types'; + /** * Runs our reconnectApp action if the app is in the background. * * We use this to refresh the app in the background after receiving a push notification (native only). Since the full app * wakes on iOS and by extension runs reconnectApp already, this is a no-op on everything but Android. */ -export default function backgroundRefresh() {} +const backgroundRefresh: BackgroundRefresh = () => {}; + +export default backgroundRefresh; diff --git a/src/libs/Notification/PushNotification/backgroundRefresh/types.ts b/src/libs/Notification/PushNotification/backgroundRefresh/types.ts new file mode 100644 index 000000000000..d3d1ee44a1fd --- /dev/null +++ b/src/libs/Notification/PushNotification/backgroundRefresh/types.ts @@ -0,0 +1,3 @@ +type BackgroundRefresh = () => void; + +export default BackgroundRefresh; diff --git a/src/libs/Notification/PushNotification/index.native.js b/src/libs/Notification/PushNotification/index.native.ts similarity index 57% rename from src/libs/Notification/PushNotification/index.native.js rename to src/libs/Notification/PushNotification/index.native.ts index 8513a88e46d3..7b2571eea368 100644 --- a/src/libs/Notification/PushNotification/index.native.js +++ b/src/libs/Notification/PushNotification/index.native.ts @@ -1,57 +1,59 @@ -import Airship, {EventType} from '@ua/react-native-airship'; -import lodashGet from 'lodash/get'; +import Airship, {EventType, PushPayload} from '@ua/react-native-airship'; import Onyx from 'react-native-onyx'; -import _ from 'underscore'; import Log from '@libs/Log'; -import * as PushNotification from '@userActions/PushNotification'; +import * as PushNotificationActions from '@userActions/PushNotification'; import ONYXKEYS from '@src/ONYXKEYS'; import ForegroundNotifications from './ForegroundNotifications'; -import NotificationType from './NotificationType'; +import NotificationType, {NotificationData} from './NotificationType'; +import PushNotificationType, {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register} from './types'; + +type NotificationEventActionCallback = (data: NotificationData) => void; + +type NotificationEventActionMap = Partial>>; let isUserOptedInToPushNotifications = false; Onyx.connect({ key: ONYXKEYS.PUSH_NOTIFICATIONS_ENABLED, - callback: (val) => (isUserOptedInToPushNotifications = val), + callback: (value) => (isUserOptedInToPushNotifications = value ?? false), }); -const notificationEventActionMap = {}; +const notificationEventActionMap: NotificationEventActionMap = {}; /** * Handle a push notification event, and trigger and bound actions. - * - * @param {String} eventType - * @param {Object} notification */ -function pushNotificationEventCallback(eventType, notification) { - const actionMap = notificationEventActionMap[eventType] || {}; - let payload = lodashGet(notification, 'extras.payload'); +function pushNotificationEventCallback(eventType: EventType, notification: PushPayload) { + const actionMap = notificationEventActionMap[eventType] ?? {}; + let payload = notification.extras.payload; // On Android, some notification payloads are sent as a JSON string rather than an object - if (_.isString(payload)) { + if (typeof payload === 'string') { payload = JSON.parse(payload); } + const data = payload as NotificationData; + Log.info(`[PushNotification] Callback triggered for ${eventType}`); - if (!payload) { + if (!data) { Log.warn('[PushNotification] Notification has null or undefined payload, not executing any callback.'); return; } - if (!payload.type) { + if (!data.type) { Log.warn('[PushNotification] No type value provided in payload, not executing any callback.'); return; } - const action = actionMap[payload.type]; + const action = actionMap[data.type]; if (!action) { Log.warn('[PushNotification] No callback set up: ', { event: eventType, - notificationType: payload.type, + notificationType: data.type, }); return; } - action(payload); + action(data); } /** @@ -65,7 +67,7 @@ function refreshNotificationOptInStatus() { } Log.info('[PushNotification] Push notification opt-in status changed.', false, {isOptedIn}); - PushNotification.setPushNotificationOptInStatus(isOptedIn); + PushNotificationActions.setPushNotificationOptInStatus(isOptedIn); }); } @@ -76,12 +78,12 @@ function refreshNotificationOptInStatus() { * WARNING: Moving or changing this code could break Push Notification processing in non-obvious ways. * DO NOT ALTER UNLESS YOU KNOW WHAT YOU'RE DOING. See this PR for details: https://github.com/Expensify/App/pull/3877 */ -function init() { +const init: Init = () => { // Setup event listeners Airship.addListener(EventType.PushReceived, (notification) => { // By default, refresh notification opt-in status to true if we receive a notification if (!isUserOptedInToPushNotifications) { - PushNotification.setPushNotificationOptInStatus(true); + PushNotificationActions.setPushNotificationOptInStatus(true); } pushNotificationEventCallback(EventType.PushReceived, notification.pushPayload); @@ -97,47 +99,52 @@ function init() { Airship.addListener(EventType.NotificationOptInStatus, refreshNotificationOptInStatus); ForegroundNotifications.configureForegroundNotifications(); -} +}; /** * Register this device for push notifications for the given notificationID. - * - * @param {String|Number} notificationID */ -function register(notificationID) { - if (Airship.contact.getNamedUserId() === notificationID.toString()) { - // No need to register again for this notificationID. - return; - } - - // Get permissions to display push notifications (prompts user on iOS, but not Android) - Airship.push.enableUserNotifications().then((isEnabled) => { - if (isEnabled) { - return; - } - - Log.info('[PushNotification] User has disabled visible push notifications for this app.'); - }); - - // Register this device as a named user in AirshipAPI. - // Regardless of the user's opt-in status, we still want to receive silent push notifications. - Log.info(`[PushNotification] Subscribing to notifications`); - Airship.contact.identify(notificationID.toString()); - - // Refresh notification opt-in status NVP for the new user. - refreshNotificationOptInStatus(); -} +const register: Register = (notificationID) => { + Airship.contact + .getNamedUserId() + .then((userID) => { + if (userID === notificationID.toString()) { + // No need to register again for this notificationID. + return; + } + + // Get permissions to display push notifications (prompts user on iOS, but not Android) + Airship.push.enableUserNotifications().then((isEnabled) => { + if (isEnabled) { + return; + } + + Log.info('[PushNotification] User has disabled visible push notifications for this app.'); + }); + + // Register this device as a named user in AirshipAPI. + // Regardless of the user's opt-in status, we still want to receive silent push notifications. + Log.info(`[PushNotification] Subscribing to notifications`); + Airship.contact.identify(notificationID.toString()); + + // Refresh notification opt-in status NVP for the new user. + refreshNotificationOptInStatus(); + }) + .catch((error) => { + Log.warn('[PushNotification] Failed to register for push notifications! Reason: ', error); + }); +}; /** * Deregister this device from push notifications. */ -function deregister() { +const deregister: Deregister = () => { Log.info('[PushNotification] Unsubscribing from push notifications.'); Airship.contact.reset(); Airship.removeAllListeners(EventType.PushReceived); Airship.removeAllListeners(EventType.NotificationResponse); ForegroundNotifications.disableForegroundNotifications(); -} +}; /** * Bind a callback to a push notification of a given type. @@ -148,45 +155,41 @@ function deregister() { * if we attempt to bind two callbacks to the PushReceived event for reportComment notifications, * the second will overwrite the first. * - * @param {String} notificationType - * @param {Function} callback - * @param {String} [triggerEvent] - The event that should trigger this callback. Should be one of UrbanAirship.EventType + * @param triggerEvent - The event that should trigger this callback. Should be one of UrbanAirship.EventType */ -function bind(notificationType, callback, triggerEvent) { - if (!notificationEventActionMap[triggerEvent]) { - notificationEventActionMap[triggerEvent] = {}; +function bind(notificationType: string, callback: NotificationEventActionCallback, triggerEvent: EventType) { + let actionMap = notificationEventActionMap[triggerEvent]; + + if (!actionMap) { + actionMap = {}; } - notificationEventActionMap[triggerEvent][notificationType] = callback; + + actionMap[notificationType] = callback; + notificationEventActionMap[triggerEvent] = actionMap; } /** * Bind a callback to be executed when a push notification of a given type is received. - * - * @param {String} notificationType - * @param {Function} callback */ -function onReceived(notificationType, callback) { +const onReceived: OnReceived = (notificationType, callback) => { bind(notificationType, callback, EventType.PushReceived); -} +}; /** * Bind a callback to be executed when a push notification of a given type is tapped by the user. - * - * @param {String} notificationType - * @param {Function} callback */ -function onSelected(notificationType, callback) { +const onSelected: OnSelected = (notificationType, callback) => { bind(notificationType, callback, EventType.NotificationResponse); -} +}; /** * Clear all push notifications */ -function clearNotifications() { +const clearNotifications: ClearNotifications = () => { Airship.push.clearNotifications(); -} +}; -export default { +const PushNotification: PushNotificationType = { init, register, deregister, @@ -195,3 +198,5 @@ export default { TYPE: NotificationType, clearNotifications, }; + +export default PushNotification; diff --git a/src/libs/Notification/PushNotification/index.js b/src/libs/Notification/PushNotification/index.ts similarity index 71% rename from src/libs/Notification/PushNotification/index.js rename to src/libs/Notification/PushNotification/index.ts index 88136ff5dc72..1e5499d1fe7d 100644 --- a/src/libs/Notification/PushNotification/index.js +++ b/src/libs/Notification/PushNotification/index.ts @@ -1,7 +1,8 @@ import NotificationType from './NotificationType'; +import PushNotificationType from './types'; // Push notifications are only supported on mobile, so we'll just noop here -export default { +const PushNotification: PushNotificationType = { init: () => {}, register: () => {}, deregister: () => {}, @@ -10,3 +11,5 @@ export default { TYPE: NotificationType, clearNotifications: () => {}, }; + +export default PushNotification; diff --git a/src/libs/Notification/PushNotification/shouldShowPushNotification.js b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts similarity index 61% rename from src/libs/Notification/PushNotification/shouldShowPushNotification.js rename to src/libs/Notification/PushNotification/shouldShowPushNotification.ts index f25d452a77d5..46f99fcc9271 100644 --- a/src/libs/Notification/PushNotification/shouldShowPushNotification.js +++ b/src/libs/Notification/PushNotification/shouldShowPushNotification.ts @@ -1,30 +1,31 @@ -import _ from 'underscore'; +import {PushPayload} from '@ua/react-native-airship'; import Log from '@libs/Log'; import * as ReportActionUtils from '@libs/ReportActionsUtils'; import * as Report from '@userActions/Report'; +import {NotificationData} from './NotificationType'; /** * Returns whether the given Airship notification should be shown depending on the current state of the app - * @param {PushPayload} pushPayload - * @returns {Boolean} */ -export default function shouldShowPushNotification(pushPayload) { +export default function shouldShowPushNotification(pushPayload: PushPayload): boolean { Log.info('[PushNotification] push notification received', false, {pushPayload}); - let pushData = pushPayload.extras.payload; + let payload = pushPayload.extras.payload; // The payload is string encoded on Android - if (_.isString(pushData)) { - pushData = JSON.parse(pushData); + if (typeof payload === 'string') { + payload = JSON.parse(payload); } - if (!pushData.reportID) { + const data = payload as NotificationData; + + if (!data.reportID) { Log.info('[PushNotification] Not a report action notification. Showing notification'); return true; } - const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(pushData.onyxData); - const shouldShow = Report.shouldShowReportActionNotification(String(pushData.reportID), reportAction, true); + const reportAction = ReportActionUtils.getLatestReportActionFromOnyxData(data.onyxData ?? null); + const shouldShow = Report.shouldShowReportActionNotification(String(data.reportID), reportAction, true); Log.info(`[PushNotification] ${shouldShow ? 'Showing' : 'Not showing'} notification`); return shouldShow; } diff --git a/src/libs/Notification/PushNotification/subscribePushNotification/index.js b/src/libs/Notification/PushNotification/subscribePushNotification/index.ts similarity index 100% rename from src/libs/Notification/PushNotification/subscribePushNotification/index.js rename to src/libs/Notification/PushNotification/subscribePushNotification/index.ts diff --git a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts similarity index 86% rename from src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js rename to src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts index ede873f79c6e..547ecb1de5b2 100644 --- a/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.js +++ b/src/libs/Notification/PushNotification/subscribeToReportCommentPushNotifications.ts @@ -12,7 +12,7 @@ import PushNotification from './index'; export default function subscribeToReportCommentPushNotifications() { PushNotification.onReceived(PushNotification.TYPE.REPORT_COMMENT, ({reportID, reportActionID, onyxData}) => { Log.info(`[PushNotification] received report comment notification in the ${Visibility.isVisible() ? 'foreground' : 'background'}`, false, {reportID, reportActionID}); - Onyx.update(onyxData); + Onyx.update(onyxData ?? []); backgroundRefresh(); }); @@ -33,9 +33,14 @@ export default function subscribeToReportCommentPushNotifications() { } Log.info('[PushNotification] onSelected() - Navigation is ready. Navigating...', false, {reportID, reportActionID}); - Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(String(reportID))); } catch (error) { - Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: error.message}); + let errorMessage = String(error); + if (error instanceof Error) { + errorMessage = error.message; + } + + Log.alert('[PushNotification] onSelected() - failed', {reportID, reportActionID, error: errorMessage}); } }); }); diff --git a/src/libs/Notification/PushNotification/types.ts b/src/libs/Notification/PushNotification/types.ts new file mode 100644 index 000000000000..f72ee1af887a --- /dev/null +++ b/src/libs/Notification/PushNotification/types.ts @@ -0,0 +1,22 @@ +import {ValueOf} from 'type-fest'; +import NotificationType, {NotificationDataMap} from './NotificationType'; + +type Init = () => void; +type Register = (notificationID: string | number) => void; +type Deregister = () => void; +type OnReceived = >(notificationType: T, callback: (data: NotificationDataMap[T]) => void) => void; +type OnSelected = >(notificationType: T, callback: (data: NotificationDataMap[T]) => void) => void; +type ClearNotifications = () => void; + +type PushNotification = { + init: Init; + register: Register; + deregister: Deregister; + onReceived: OnReceived; + onSelected: OnSelected; + TYPE: typeof NotificationType; + clearNotifications: ClearNotifications; +}; + +export default PushNotification; +export type {ClearNotifications, Deregister, Init, OnReceived, OnSelected, Register}; diff --git a/src/libs/PaymentUtils.ts b/src/libs/PaymentUtils.ts index 488041d2061f..ee7a293f6ad1 100644 --- a/src/libs/PaymentUtils.ts +++ b/src/libs/PaymentUtils.ts @@ -40,7 +40,7 @@ function getPaymentMethodDescription(accountType: AccountType, account: BankAcco /** * Get the PaymentMethods list */ -function formatPaymentMethods(bankAccountList: Record, fundList: Record, themeStyles: ThemeStyles): PaymentMethod[] { +function formatPaymentMethods(bankAccountList: Record, fundList: Record, styles: ThemeStyles): PaymentMethod[] { const combinedPaymentMethods: PaymentMethod[] = []; Object.values(bankAccountList).forEach((bankAccount) => { @@ -52,7 +52,7 @@ function formatPaymentMethods(bankAccountList: Record, fund const {icon, iconSize, iconHeight, iconWidth, iconStyles} = getBankIcon({ bankName: bankAccount?.accountData?.additionalData?.bankName, isCard: false, - themeStyles, + styles, }); combinedPaymentMethods.push({ ...bankAccount, @@ -66,7 +66,7 @@ function formatPaymentMethods(bankAccountList: Record, fund }); Object.values(fundList).forEach((card) => { - const {icon, iconSize, iconHeight, iconWidth, iconStyles} = getBankIcon({bankName: card?.accountData?.bank, isCard: true, themeStyles}); + const {icon, iconSize, iconHeight, iconWidth, iconStyles} = getBankIcon({bankName: card?.accountData?.bank, isCard: true, styles}); combinedPaymentMethods.push({ ...card, description: getPaymentMethodDescription(card?.accountType, card.accountData), diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 560480dcec9d..8a4151391453 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -197,6 +197,18 @@ function getFormattedAddress(privatePersonalDetails) { return formattedAddress.trim().replace(/,$/, ''); } +/** + * @param {Object} personalDetail - details object + * @returns {String | undefined} - The effective display name + */ +function getEffectiveDisplayName(personalDetail) { + if (personalDetail) { + return LocalePhoneNumber.formatPhoneNumber(personalDetail.login) || personalDetail.displayName; + } + + return undefined; +} + export { getDisplayNameOrDefault, getPersonalDetailsByIDs, @@ -206,4 +218,5 @@ export { getFormattedAddress, getFormattedStreet, getStreetLines, + getEffectiveDisplayName, }; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f58021e17064..ff36a2ac3401 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -5,14 +5,17 @@ import OnyxUtils from 'react-native-onyx/lib/utils'; import {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {ActionName} from '@src/types/onyx/OriginalMessage'; +import {ActionName, ChangeLog} from '@src/types/onyx/OriginalMessage'; import Report from '@src/types/onyx/Report'; -import ReportAction, {ReportActions} from '@src/types/onyx/ReportAction'; +import ReportAction, {Message, ReportActions} from '@src/types/onyx/ReportAction'; import {EmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; import * as CollectionUtils from './CollectionUtils'; import * as Environment from './Environment/Environment'; import isReportMessageAttachment from './isReportMessageAttachment'; +import * as Localize from './Localize'; import Log from './Log'; +import {MessageElementBase, MessageTextElement} from './MessageElement'; +import * as PersonalDetailsUtils from './PersonalDetailsUtils'; type LastVisibleMessage = { lastMessageTranslationKey?: string; @@ -20,6 +23,19 @@ type LastVisibleMessage = { lastMessageHtml?: string; }; +type MemberChangeMessageUserMentionElement = { + readonly kind: 'userMention'; + readonly accountID: number; +} & MessageElementBase; + +type MemberChangeMessageRoomReferenceElement = { + readonly kind: 'roomReference'; + readonly roomName: string; + readonly roomID: number; +} & MessageElementBase; + +type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement; + const allReports: OnyxCollection = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, @@ -100,7 +116,7 @@ function isReimbursementQueuedAction(reportAction: OnyxEntry) { return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED; } -function isChannelLogMemberAction(reportAction: OnyxEntry) { +function isMemberChangeAction(reportAction: OnyxEntry) { return ( reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.REMOVE_FROM_ROOM || @@ -109,6 +125,10 @@ function isChannelLogMemberAction(reportAction: OnyxEntry) { ); } +function isInviteMemberAction(reportAction: OnyxEntry) { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM; +} + function isReimbursementDeQueuedAction(reportAction: OnyxEntry): boolean { return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED; } @@ -639,6 +659,89 @@ function isNotifiableReportAction(reportAction: OnyxEntry): boolea return actions.includes(reportAction.actionName); } +function getMemberChangeMessageElements(reportAction: OnyxEntry): readonly MemberChangeMessageElement[] { + const isInviteAction = isInviteMemberAction(reportAction); + + // Currently, we only render messages when members are invited + const verb = isInviteAction ? Localize.translateLocal('workspace.invite.invited') : Localize.translateLocal('workspace.invite.removed'); + + const originalMessage = reportAction?.originalMessage as ChangeLog; + const targetAccountIDs: number[] = originalMessage?.targetAccountIDs ?? []; + const personalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs(targetAccountIDs, 0); + + const mentionElements = targetAccountIDs.map((accountID): MemberChangeMessageUserMentionElement => { + const personalDetail = personalDetails.find((personal) => personal.accountID === accountID); + const handleText = PersonalDetailsUtils.getEffectiveDisplayName(personalDetail) ?? Localize.translateLocal('common.hidden'); + + return { + kind: 'userMention', + content: `@${handleText}`, + accountID, + }; + }); + + const buildRoomElements = (): readonly MemberChangeMessageElement[] => { + const roomName = originalMessage?.roomName; + + if (roomName) { + const preposition = isInviteAction ? ` ${Localize.translateLocal('workspace.invite.to')} ` : ` ${Localize.translateLocal('workspace.invite.from')} `; + + if (originalMessage.reportID) { + return [ + { + kind: 'text', + content: preposition, + }, + { + kind: 'roomReference', + roomName, + roomID: originalMessage.reportID, + content: roomName, + }, + ]; + } + } + + return []; + }; + + return [ + { + kind: 'text', + content: `${verb} `, + }, + ...Localize.formatMessageElementList(mentionElements), + ...buildRoomElements(), + ]; +} + +function getMemberChangeMessageFragment(reportAction: OnyxEntry): Message { + const messageElements: readonly MemberChangeMessageElement[] = getMemberChangeMessageElements(reportAction); + const html = messageElements + .map((messageElement) => { + switch (messageElement.kind) { + case 'userMention': + return ``; + case 'roomReference': + return `${messageElement.roomName}`; + default: + return messageElement.content; + } + }) + .join(''); + + return { + html: `${html}`, + text: reportAction?.message ? reportAction?.message[0].text : '', + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + }; +} + +function getMemberChangeMessagePlainText(reportAction: OnyxEntry): string { + const messageElements = getMemberChangeMessageElements(reportAction); + return messageElements.map((element) => element.content).join(''); +} + /** * Helper method to determine if the provided accountID has made a request on the specified report. * @@ -701,7 +804,9 @@ export { shouldReportActionBeVisibleAsLastAction, hasRequestFromCurrentAccount, getFirstVisibleReportActionID, - isChannelLogMemberAction, + isMemberChangeAction, + getMemberChangeMessageFragment, + getMemberChangeMessagePlainText, isReimbursementDeQueuedAction, }; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 23382e87936a..b50b4611a249 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -18,7 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import {Beta, Login, PersonalDetails, Policy, PolicyTags, Report, ReportAction, Session, Transaction} from '@src/types/onyx'; import {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import {ChangeLog, IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; +import {IOUMessage, OriginalMessageActionName} from '@src/types/onyx/OriginalMessage'; import {Message, ReportActions} from '@src/types/onyx/ReportAction'; import {Receipt, WaypointCollection} from '@src/types/onyx/Transaction'; import DeepValueOf from '@src/types/utils/DeepValueOf'; @@ -4174,44 +4174,6 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) }); } -/** - * Return room channel log display message - */ -function getChannelLogMemberMessage(reportAction: OnyxEntry): string { - const verb = - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM - ? 'invited' - : 'removed'; - - const mentions = (reportAction?.originalMessage as ChangeLog)?.targetAccountIDs?.map(() => { - const personalDetail = allPersonalDetails?.accountID; - const displayNameOrLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail?.login ?? '') || (personalDetail?.displayName ?? '') || Localize.translateLocal('common.hidden'); - return `@${displayNameOrLogin}`; - }); - - const lastMention = mentions?.pop(); - let message = ''; - - if (mentions?.length === 0) { - message = `${verb} ${lastMention}`; - } else if (mentions?.length === 1) { - message = `${verb} ${mentions?.[0]} and ${lastMention}`; - } else { - message = `${verb} ${mentions?.join(', ')}, and ${lastMention}`; - } - - const roomName = (reportAction?.originalMessage as ChangeLog)?.roomName ?? ''; - if (roomName) { - const preposition = - reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM - ? ' to' - : ' from'; - message += `${preposition} ${roomName}`; - } - - return message; -} - /** * Checks if a report is a group chat. * @@ -4446,7 +4408,6 @@ export { getReimbursementQueuedActionMessage, getReimbursementDeQueuedActionMessage, getPersonalDetailsForAccountID, - getChannelLogMemberMessage, getRoom, shouldDisableWelcomeMessage, navigateToPrivateNotes, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index bace29e06d28..6e382e11b49b 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -375,17 +375,17 @@ function getOptionData( const targetAccountIDs = lastAction?.originalMessage?.targetAccountIDs ?? []; const verb = lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM - ? 'invited' - : 'removed'; - const users = targetAccountIDs.length > 1 ? 'users' : 'user'; + ? Localize.translate(preferredLocale, 'workspace.invite.invited') + : Localize.translate(preferredLocale, 'workspace.invite.removed'); + const users = Localize.translate(preferredLocale, targetAccountIDs.length > 1 ? 'workspace.invite.users' : 'workspace.invite.user'); result.alternateText = `${verb} ${targetAccountIDs.length} ${users}`; const roomName = lastAction?.originalMessage?.roomName ?? ''; if (roomName) { const preposition = lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.ROOMCHANGELOG.INVITE_TO_ROOM || lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG.INVITE_TO_ROOM - ? ' to' - : ' from'; + ? ` ${Localize.translate(preferredLocale, 'workspace.invite.to')}` + : ` ${Localize.translate(preferredLocale, 'workspace.invite.from')}`; result.alternateText += `${preposition} ${roomName}`; } } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) { diff --git a/src/libs/actions/App.ts b/src/libs/actions/App.ts index 4de8f1c1f171..ec43d4358134 100644 --- a/src/libs/actions/App.ts +++ b/src/libs/actions/App.ts @@ -1,7 +1,7 @@ // Issue - https://github.com/Expensify/App/issues/26719 import Str from 'expensify-common/lib/str'; import {AppState, AppStateStatus} from 'react-native'; -import Onyx, {OnyxCollection, OnyxUpdate} from 'react-native-onyx'; +import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import * as Browser from '@libs/Browser'; @@ -228,7 +228,7 @@ function openApp() { * Fetches data when the app reconnects to the network * @param [updateIDFrom] the ID of the Onyx update that we want to start fetching from */ -function reconnectApp(updateIDFrom = 0) { +function reconnectApp(updateIDFrom: OnyxEntry = 0) { console.debug(`[OnyxUpdates] App reconnecting with updateIDFrom: ${updateIDFrom}`); getPolicyParamsForOpenOrReconnect().then((policyParams) => { type ReconnectParams = { @@ -384,7 +384,7 @@ function savePolicyDraftByNewWorkspace(policyID?: string, policyName?: string, p * pass it in as a parameter. withOnyx guarantees that the value has been read * from Onyx because it will not render the AuthScreens until that point. */ -function setUpPoliciesAndNavigate(session: OnyxTypes.Session) { +function setUpPoliciesAndNavigate(session: OnyxEntry) { const currentUrl = getCurrentUrl(); if (!session || !currentUrl || !currentUrl.includes('exitTo')) { return; diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 1233bcd5d707..6161fd2066ff 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1743,7 +1743,7 @@ function setIsComposerFullSize(reportID, isComposerFullSize) { /** * @param {String} reportID - * @param {Object} action the associated report action (optional) + * @param {Object|null} action the associated report action (optional) * @param {Boolean} isRemote whether or not this notification is a remote push notification * @returns {Boolean} */ diff --git a/src/libs/getIsSmallScreenWidth.ts b/src/libs/getIsSmallScreenWidth.ts new file mode 100644 index 000000000000..6fba45ea1319 --- /dev/null +++ b/src/libs/getIsSmallScreenWidth.ts @@ -0,0 +1,6 @@ +import {Dimensions} from 'react-native'; +import variables from '@styles/variables'; + +export default function getIsSmallScreenWidth(windowWidth = Dimensions.get('window').width) { + return windowWidth <= variables.mobileResponsiveWidthBreakpoint; +} diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 66345107dbb1..6d3f0198bbfe 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -134,7 +134,6 @@ function DetailsPage(props) { {({show}) => ( diff --git a/src/pages/ErrorPage/NotFoundPage.js b/src/pages/ErrorPage/NotFoundPage.js index aac2e6d613f9..e10ec32732ea 100644 --- a/src/pages/ErrorPage/NotFoundPage.js +++ b/src/pages/ErrorPage/NotFoundPage.js @@ -13,12 +13,12 @@ const defaultProps = { }; // eslint-disable-next-line rulesdir/no-negated-variables -function NotFoundPage({onBackButtonPress}) { +function NotFoundPage(props) { return ( ); diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 97ec3f99da3c..ece75b7f6918 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -159,7 +159,6 @@ function ProfilePage(props) { diff --git a/src/pages/ReimbursementAccount/EnableStep.js b/src/pages/ReimbursementAccount/EnableStep.js index 13ab2794ea09..e7bdcf4082d2 100644 --- a/src/pages/ReimbursementAccount/EnableStep.js +++ b/src/pages/ReimbursementAccount/EnableStep.js @@ -50,7 +50,7 @@ function EnableStep(props) { const styles = useThemeStyles(); const isUsingExpensifyCard = props.user.isUsingExpensifyCard; const achData = lodashGet(props.reimbursementAccount, 'achData') || {}; - const {icon, iconSize} = getBankIcon({bankName: achData.bankName, themeStyles: styles}); + const {icon, iconSize} = getBankIcon({bankName: achData.bankName, styles}); const formattedBankAccountNumber = achData.accountNumber ? `${props.translate('paymentMethodList.accountLastFour')} ${achData.accountNumber.slice(-4)}` : ''; const bankName = achData.addressName; diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 4f35926c5957..6c645bc87486 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -281,8 +281,8 @@ export default [ } else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction); Clipboard.setString(displayMessage); - } else if (ReportActionsUtils.isChannelLogMemberAction(reportAction)) { - const logMessage = ReportUtils.getChannelLogMemberMessage(reportAction); + } else if (ReportActionsUtils.isMemberChangeAction(reportAction)) { + const logMessage = ReportActionsUtils.getMemberChangeMessagePlainText(reportAction); Clipboard.setString(logMessage); } else if (content) { const parser = new ExpensiMark(); diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js index 2265530f29a1..46e0438f250a 100644 --- a/src/pages/home/report/ReportActionItemMessage.js +++ b/src/pages/home/report/ReportActionItemMessage.js @@ -8,6 +8,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; +import TextCommentFragment from './comment/TextCommentFragment'; import ReportActionItemFragment from './ReportActionItemFragment'; import reportActionPropTypes from './reportActionPropTypes'; @@ -40,6 +41,20 @@ function ReportActionItemMessage(props) { const styles = useThemeStyles(); const fragments = _.compact(props.action.previousMessage || props.action.message); const isIOUReport = ReportActionsUtils.isMoneyRequestAction(props.action); + if (ReportActionsUtils.isMemberChangeAction(props.action)) { + const fragment = ReportActionsUtils.getMemberChangeMessageFragment(props.action); + + return ( + + ); + } + let iouMessage; if (isIOUReport) { const iouReportID = lodashGet(props.action, 'originalMessage.IOUReportID'); diff --git a/src/pages/settings/Wallet/PaymentMethodList.js b/src/pages/settings/Wallet/PaymentMethodList.js index 5af4129aefbc..60605311b1ab 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.js +++ b/src/pages/settings/Wallet/PaymentMethodList.js @@ -224,7 +224,7 @@ function PaymentMethodList({ return _.map(assignedCards, (card) => { const isExpensifyCard = CardUtils.isExpensifyCard(card.cardID); - const icon = getBankIcon({bankName: card.bank, isCard: true, themeStyles: styles}); + const icon = getBankIcon({bankName: card.bank, isCard: true, styles}); // In the case a user has been assigned multiple physical Expensify Cards under one domain, display the Card with PAN const expensifyCardDescription = numberPhysicalExpensifyCards > 1 ? CardUtils.getCardDescription(card.cardID) : translate('walletPage.expensifyCard'); diff --git a/src/styles/getContextMenuItemStyles/types.ts b/src/styles/getContextMenuItemStyles/types.ts index 60b25acec0cb..376fb8d46d7c 100644 --- a/src/styles/getContextMenuItemStyles/types.ts +++ b/src/styles/getContextMenuItemStyles/types.ts @@ -1,6 +1,6 @@ import {ViewStyle} from 'react-native'; import {type ThemeStyles} from '@styles/styles'; -type GetContextMenuItemStyle = (themeStyles: ThemeStyles, windowWidth?: number) => ViewStyle[]; +type GetContextMenuItemStyle = (styles: ThemeStyles, windowWidth?: number) => ViewStyle[]; export default GetContextMenuItemStyle; diff --git a/src/styles/getNavigationModalCardStyles/types.ts b/src/styles/getNavigationModalCardStyles/types.ts index 877981dd4dd2..e0dba07dc908 100644 --- a/src/styles/getNavigationModalCardStyles/types.ts +++ b/src/styles/getNavigationModalCardStyles/types.ts @@ -1,7 +1,5 @@ import {ViewStyle} from 'react-native'; -type GetNavigationModalCardStylesParams = {isSmallScreenWidth: number}; - -type GetNavigationModalCardStyles = (params: GetNavigationModalCardStylesParams) => ViewStyle; +type GetNavigationModalCardStyles = () => ViewStyle; export default GetNavigationModalCardStyles; diff --git a/src/styles/getTooltipStyles.ts b/src/styles/getTooltipStyles.ts index fa2b5ee308e2..1adfa1969ab9 100644 --- a/src/styles/getTooltipStyles.ts +++ b/src/styles/getTooltipStyles.ts @@ -113,7 +113,7 @@ type TooltipParams = { tooltipContentWidth: number; tooltipWrapperHeight: number; theme: ThemeColors; - themeStyles: ThemeStyles; + styles: ThemeStyles; manualShiftHorizontal?: number; manualShiftVertical?: number; }; @@ -151,7 +151,7 @@ export default function getTooltipStyles({ tooltipContentWidth, tooltipWrapperHeight, theme, - themeStyles, + styles, manualShiftHorizontal = 0, manualShiftVertical = 0, }: TooltipParams): TooltipStyles { @@ -245,7 +245,7 @@ export default function getTooltipStyles({ // at the center of the hovered component. pointerWrapperLeft = horizontalShiftPointer + (tooltipWidth / 2 - POINTER_WIDTH / 2); - pointerAdditionalStyle = shouldShowBelow ? themeStyles.flipUpsideDown : {}; + pointerAdditionalStyle = shouldShowBelow ? styles.flipUpsideDown : {}; } return { @@ -268,8 +268,8 @@ export default function getTooltipStyles({ left: rootWrapperLeft, // We are adding this to prevent the tooltip text from being selected and copied on CTRL + A. - ...themeStyles.userSelectNone, - ...themeStyles.pointerEventsNone, + ...styles.userSelectNone, + ...styles.pointerEventsNone, }, textStyle: { color: theme.textReversed, diff --git a/src/styles/optionRowStyles/types.ts b/src/styles/optionRowStyles/types.ts index 8dc5d5cc9419..3425d7812be2 100644 --- a/src/styles/optionRowStyles/types.ts +++ b/src/styles/optionRowStyles/types.ts @@ -1,6 +1,6 @@ import {ViewStyle} from 'react-native'; import {type ThemeStyles} from '@styles/styles'; -type CompactContentContainerStyles = (themeStyles: ThemeStyles) => ViewStyle; +type CompactContentContainerStyles = (styles: ThemeStyles) => ViewStyle; export default CompactContentContainerStyles; diff --git a/src/styles/styles.ts b/src/styles/styles.ts index 7ff7240f4ed0..b88119beae74 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -1492,6 +1492,8 @@ const styles = (theme: ThemeColors) => flexDirection: 'row', paddingLeft: 8, paddingRight: 8, + marginHorizontal: 12, + borderRadius: variables.componentBorderRadiusNormal, }, sidebarLinkText: { diff --git a/src/types/onyx/DemoInfo.ts b/src/types/onyx/DemoInfo.ts new file mode 100644 index 000000000000..dcd7efc44d8d --- /dev/null +++ b/src/types/onyx/DemoInfo.ts @@ -0,0 +1,8 @@ +type DemoInfo = { + money2020: { + /** If the beginning demo should be shown */ + isBeginningDemo?: boolean; + }; +}; + +export default DemoInfo; diff --git a/src/types/onyx/OnyxUpdatesFromServer.ts b/src/types/onyx/OnyxUpdatesFromServer.ts index 50b1503b90bd..843d3ae86e46 100644 --- a/src/types/onyx/OnyxUpdatesFromServer.ts +++ b/src/types/onyx/OnyxUpdatesFromServer.ts @@ -2,9 +2,11 @@ import {OnyxUpdate} from 'react-native-onyx'; import Request from './Request'; import Response from './Response'; +type OnyxServerUpdate = OnyxUpdate & {shouldNotify?: boolean}; + type OnyxUpdateEvent = { eventType: string; - data: OnyxUpdate[]; + data: OnyxServerUpdate[]; }; type OnyxUpdatesFromServer = { @@ -16,4 +18,4 @@ type OnyxUpdatesFromServer = { updates?: OnyxUpdateEvent[]; }; -export type {OnyxUpdatesFromServer, OnyxUpdateEvent}; +export type {OnyxUpdatesFromServer, OnyxUpdateEvent, OnyxServerUpdate}; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index f76fbd5ffd7d..72ea275e3ba3 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -140,6 +140,7 @@ type ChronosOOOTimestamp = { type ChangeLog = { targetAccountIDs?: number[]; roomName?: string; + reportID?: number; }; type ChronosOOOEvent = { diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index b49599913543..a077bf1a3281 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -94,6 +94,9 @@ type Report = { /** The report type */ type?: string; + /** If the admin room should be opened */ + openOnAdminRoom?: boolean; + /** The report visibility */ visibility?: ValueOf; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 8329b56dc4b8..dcaa4ee3d623 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -7,6 +7,7 @@ import Card from './Card'; import Credentials from './Credentials'; import Currency from './Currency'; import CustomStatusDraft from './CustomStatusDraft'; +import DemoInfo from './DemoInfo'; import Download from './Download'; import Form, {AddDebitCardForm, DateOfBirthForm} from './Form'; import FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; @@ -66,6 +67,7 @@ export type { Currency, CustomStatusDraft, DateOfBirthForm, + DemoInfo, Download, Form, FrequentlyUsedEmoji, diff --git a/tests/unit/LocalizeTests.js b/tests/unit/LocalizeTests.js index 4c89d587fc06..7693a0a4a88d 100644 --- a/tests/unit/LocalizeTests.js +++ b/tests/unit/LocalizeTests.js @@ -15,7 +15,7 @@ describe('localize', () => { afterEach(() => Onyx.clear()); - describe('arrayToString', () => { + describe('formatList', () => { test.each([ [ [], @@ -52,9 +52,9 @@ describe('localize', () => { [CONST.LOCALES.ES]: 'rory, vit e ionatan', }, ], - ])('arrayToSpokenList(%s)', (input, {[CONST.LOCALES.DEFAULT]: expectedOutput, [CONST.LOCALES.ES]: expectedOutputES}) => { - expect(Localize.arrayToString(input)).toBe(expectedOutput); - return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(Localize.arrayToString(input)).toBe(expectedOutputES)); + ])('formatList(%s)', (input, {[CONST.LOCALES.DEFAULT]: expectedOutput, [CONST.LOCALES.ES]: expectedOutputES}) => { + expect(Localize.formatList(input)).toBe(expectedOutput); + return Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.ES).then(() => expect(Localize.formatList(input)).toBe(expectedOutputES)); }); }); });