diff --git a/android/app/build.gradle b/android/app/build.gradle index ddda3199f51a..e01e62f4b6b9 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001044307 - versionName "1.4.43-7" + versionCode 1001044311 + versionName "1.4.43-11" } flavorDimensions "default" diff --git a/docs/redirects.csv b/docs/redirects.csv index 76b7bac3fc99..8e160e3bcdf2 100644 --- a/docs/redirects.csv +++ b/docs/redirects.csv @@ -54,3 +54,4 @@ https://help.expensify.com/articles/expensify-classic/getting-started/Employees, https://help.expensify.com/articles/expensify-classic/getting-started/Using-The-App,https://help.expensify.com/articles/expensify-classic/getting-started/Join-your-company's-workspace https://help.expensify.com/articles/expensify-classic/getting-started/support/Expensify-Support,https://use.expensify.com/support https://help.expensify.com/articles/expensify-classic/getting-started/Plan-Types,https://use.expensify.com/ +https://help.expensify.com/articles/new-expensify/payments/Referral-Program,https://help.expensify.com/articles/expensify-classic/get-paid-back/Referral-Program diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index f082f213a415..1a2581512eda 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.43.7 + 1.4.43.11 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 98e9859ff5dc..7b789718fd70 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.43.7 + 1.4.43.11 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 196d25080609..ad4e309ee295 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -13,7 +13,7 @@ CFBundleShortVersionString 1.4.43 CFBundleVersion - 1.4.43.7 + 1.4.43.11 NSExtension NSExtensionPointIdentifier diff --git a/jest.config.js b/jest.config.js index 95ecc350ed9f..441507af4228 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,7 @@ module.exports = { `/?(*.)+(spec|test).${testFileExtension}`, ], transform: { - '^.+\\.jsx?$': 'babel-jest', + '^.+\\.[jt]sx?$': 'babel-jest', '^.+\\.svg?$': 'jest-transformer-svg', }, transformIgnorePatterns: ['/node_modules/(?!react-native)/'], diff --git a/package-lock.json b/package-lock.json index 4ddd24c2ad47..cec28a395431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.43-7", + "version": "1.4.43-11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.43-7", + "version": "1.4.43-11", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -38,6 +38,7 @@ "@react-native-picker/picker": "2.5.1", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.8", + "@react-navigation/native-stack": "^6.9.17", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.1.11", @@ -10258,6 +10259,17 @@ "react": "*" } }, + "node_modules/@react-navigation/elements": { + "version": "1.3.21", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.21.tgz", + "integrity": "sha512-eyS2C6McNR8ihUoYfc166O1D8VYVh9KIl0UQPI8/ZJVsStlfSTgeEEh+WXge6+7SFPnZ4ewzEJdSAHH+jzcEfg==", + "peerDependencies": { + "@react-navigation/native": "^6.0.0", + "react": "*", + "react-native": "*", + "react-native-safe-area-context": ">= 3.0.0" + } + }, "node_modules/@react-navigation/material-top-tabs": { "version": "6.6.3", "resolved": "https://registry.npmjs.org/@react-navigation/material-top-tabs/-/material-top-tabs-6.6.3.tgz", @@ -10289,6 +10301,22 @@ "react-native": "*" } }, + "node_modules/@react-navigation/native-stack": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-6.9.17.tgz", + "integrity": "sha512-X8p8aS7JptQq7uZZNFEvfEcPf6tlK4PyVwYDdryRbG98B4bh2wFQYMThxvqa+FGEN7USEuHdv2mF0GhFKfX0ew==", + "dependencies": { + "@react-navigation/elements": "^1.3.21", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "@react-navigation/native": "^6.0.0", + "react": "*", + "react-native": "*", + "react-native-safe-area-context": ">= 3.0.0", + "react-native-screens": ">= 3.0.0" + } + }, "node_modules/@react-navigation/routers": { "version": "6.1.9", "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-6.1.9.tgz", @@ -10315,17 +10343,6 @@ "react-native-screens": ">= 3.0.0" } }, - "node_modules/@react-navigation/stack/node_modules/@react-navigation/elements": { - "version": "1.3.17", - "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-1.3.17.tgz", - "integrity": "sha512-sui8AzHm6TxeEvWT/NEXlz3egYvCUog4tlXA4Xlb2Vxvy3purVXDq/XsM56lJl344U5Aj/jDzkVanOTMWyk4UA==", - "peerDependencies": { - "@react-navigation/native": "^6.0.0", - "react": "*", - "react-native": "*", - "react-native-safe-area-context": ">= 3.0.0" - } - }, "node_modules/@react-ng/bounds-observer": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/@react-ng/bounds-observer/-/bounds-observer-0.2.1.tgz", diff --git a/package.json b/package.json index 92698102e064..379612854781 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.43-7", + "version": "1.4.43-11", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -86,6 +86,7 @@ "@react-native-picker/picker": "2.5.1", "@react-navigation/material-top-tabs": "^6.6.3", "@react-navigation/native": "6.1.8", + "@react-navigation/native-stack": "^6.9.17", "@react-navigation/stack": "6.3.16", "@react-ng/bounds-observer": "^0.2.1", "@rnmapbox/maps": "^10.1.11", diff --git a/patches/@react-navigation+native-stack+6.9.17.patch b/patches/@react-navigation+native-stack+6.9.17.patch new file mode 100644 index 000000000000..933ca6ce792e --- /dev/null +++ b/patches/@react-navigation+native-stack+6.9.17.patch @@ -0,0 +1,39 @@ +diff --git a/node_modules/@react-navigation/native-stack/src/types.tsx b/node_modules/@react-navigation/native-stack/src/types.tsx +index 206fb0b..7a34a8e 100644 +--- a/node_modules/@react-navigation/native-stack/src/types.tsx ++++ b/node_modules/@react-navigation/native-stack/src/types.tsx +@@ -490,6 +490,14 @@ export type NativeStackNavigationOptions = { + * Only supported on iOS and Android. + */ + freezeOnBlur?: boolean; ++ // partial changes from https://github.com/react-navigation/react-navigation/commit/90cfbf23bcc5259f3262691a9eec6c5b906e5262 ++ // patch can be removed when new version of `native-stack` will be released ++ /** ++ * Whether the keyboard should hide when swiping to the previous screen. Defaults to `false`. ++ * ++ * Only supported on iOS ++ */ ++ keyboardHandlingEnabled?: boolean; + }; + + export type NativeStackNavigatorProps = DefaultNavigatorOptions< +diff --git a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx +index a005c43..03d8b50 100644 +--- a/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx ++++ b/node_modules/@react-navigation/native-stack/src/views/NativeStackView.native.tsx +@@ -161,6 +161,7 @@ const SceneView = ({ + statusBarTranslucent, + statusBarColor, + freezeOnBlur, ++ keyboardHandlingEnabled, + } = options; + + let { +@@ -289,6 +290,7 @@ const SceneView = ({ + onNativeDismissCancelled={onNativeDismissCancelled} + // this prop is available since rn-screens 3.16 + freezeOnBlur={freezeOnBlur} ++ hideKeyboardOnSwipe={keyboardHandlingEnabled} + > + + diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c5480d363019..c41ef521661c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -84,28 +84,28 @@ const ROUTES = { SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARD: { - route: '/settings/wallet/card/:domain', - getRoute: (domain: string) => `/settings/wallet/card/${domain}` as const, + route: 'settings/wallet/card/:domain', + getRoute: (domain: string) => `settings/wallet/card/${domain}` as const, }, SETTINGS_REPORT_FRAUD: { - route: '/settings/wallet/card/:domain/report-virtual-fraud', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-virtual-fraud` as const, + route: 'settings/wallet/card/:domain/report-virtual-fraud', + getRoute: (domain: string) => `settings/wallet/card/${domain}/report-virtual-fraud` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME: { - route: '/settings/wallet/card/:domain/get-physical/name', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/name` as const, + route: 'settings/wallet/card/:domain/get-physical/name', + getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/name` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE: { - route: '/settings/wallet/card/:domain/get-physical/phone', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/phone` as const, + route: 'settings/wallet/card/:domain/get-physical/phone', + getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/phone` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_ADDRESS: { - route: '/settings/wallet/card/:domain/get-physical/address', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/address` as const, + route: 'settings/wallet/card/:domain/get-physical/address', + getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/address` as const, }, SETTINGS_WALLET_CARD_GET_PHYSICAL_CONFIRM: { - route: '/settings/wallet/card/:domain/get-physical/confirm', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/get-physical/confirm` as const, + route: 'settings/wallet/card/:domain/get-physical/confirm', + getRoute: (domain: string) => `settings/wallet/card/${domain}/get-physical/confirm` as const, }, SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', @@ -117,8 +117,8 @@ const ROUTES = { SETTINGS_WALLET_TRANSFER_BALANCE: 'settings/wallet/transfer-balance', SETTINGS_WALLET_CHOOSE_TRANSFER_ACCOUNT: 'settings/wallet/choose-transfer-account', SETTINGS_WALLET_REPORT_CARD_LOST_OR_DAMAGED: { - route: '/settings/wallet/card/:domain/report-card-lost-or-damaged', - getRoute: (domain: string) => `/settings/wallet/card/${domain}/report-card-lost-or-damaged` as const, + route: 'settings/wallet/card/:domain/report-card-lost-or-damaged', + getRoute: (domain: string) => `settings/wallet/card/${domain}/report-card-lost-or-damaged` as const, }, SETTINGS_WALLET_CARD_ACTIVATE: { route: 'settings/wallet/card/:domain/activate', @@ -219,6 +219,10 @@ const ROUTES = { route: 'r/:reportID/settings/who-can-post', getRoute: (reportID: string) => `r/${reportID}/settings/who-can-post` as const, }, + REPORT_SETTINGS_VISIBILITY: { + route: 'r/:reportID/settings/visibility', + getRoute: (reportID: string) => `r/${reportID}/settings/visibility` as const, + }, SPLIT_BILL_DETAILS: { route: 'r/:reportID/split/:reportActionID', getRoute: (reportID: string, reportActionID: string) => `r/${reportID}/split/${reportActionID}` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ee3c64e8d804..18754a3513c1 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -167,6 +167,7 @@ const SCREENS = { ROOM_NAME: 'Report_Settings_Room_Name', NOTIFICATION_PREFERENCES: 'Report_Settings_Notification_Preferences', WRITE_CAPABILITY: 'Report_Settings_Write_Capability', + VISIBILITY: 'Report_Settings_Visibility', }, NEW_TASK: { diff --git a/src/components/CountrySelector.tsx b/src/components/CountrySelector.tsx index 25dc99459064..5b5e99ac0621 100644 --- a/src/components/CountrySelector.tsx +++ b/src/components/CountrySelector.tsx @@ -1,4 +1,5 @@ -import React, {forwardRef, useEffect} from 'react'; +import {useIsFocused} from '@react-navigation/native'; +import React, {forwardRef, useEffect, useRef} from 'react'; import type {ForwardedRef} from 'react'; import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; @@ -23,15 +24,28 @@ type CountrySelectorProps = { /** inputID used by the Form component */ // eslint-disable-next-line react/no-unused-prop-types inputID: string; + + /** Callback to call when the picker modal is dismissed */ + onBlur?: () => void; }; -function CountrySelector({errorText = '', value: countryCode, onInputChange}: CountrySelectorProps, ref: ForwardedRef) { +function CountrySelector({errorText = '', value: countryCode, onInputChange, onBlur}: CountrySelectorProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); const title = countryCode ? translate(`allCountries.${countryCode}`) : ''; const countryTitleDescStyle = title.length === 0 ? styles.textNormal : null; + const didOpenContrySelector = useRef(false); + const isFocused = useIsFocused(); + useEffect(() => { + if (!isFocused || !didOpenContrySelector.current) { + return; + } + didOpenContrySelector.current = false; + onBlur?.(); + }, [isFocused, onBlur]); + useEffect(() => { // This will cause the form to revalidate and remove any error related to country name onInputChange(countryCode); @@ -48,6 +62,7 @@ function CountrySelector({errorText = '', value: countryCode, onInputChange}: Co description={translate('common.country')} onPress={() => { const activeRoute = Navigation.getActiveRouteWithoutParams(); + didOpenContrySelector.current = true; Navigation.navigate(ROUTES.SETTINGS_ADDRESS_COUNTRY.getRoute(countryCode ?? '', activeRoute)); }} /> diff --git a/src/components/Image/index.js b/src/components/Image/index.js index ef1a69e19c12..59fcde8273fd 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -3,12 +3,15 @@ import React, {useEffect, useMemo} from 'react'; import {Image as RNImage} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import useNetwork from '@hooks/useNetwork'; import ONYXKEYS from '@src/ONYXKEYS'; import {defaultProps, imagePropTypes} from './imagePropTypes'; import RESIZE_MODES from './resizeModes'; function Image(props) { const {source: propsSource, isAuthTokenRequired, onLoad, session} = props; + const {isOffline} = useNetwork(); + /** * Check if the image source is a URL - if so the `encryptedAuthToken` is appended * to the source. @@ -39,7 +42,7 @@ function Image(props) { RNImage.getSize(source.uri, (width, height) => { onLoad({nativeEvent: {width, height}}); }); - }, [onLoad, source]); + }, [onLoad, source, isOffline]); // Omit the props which the underlying RNImage won't use const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']); diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index b3fc1dc91c16..0ca4a0456e33 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -2,6 +2,7 @@ import delay from 'lodash/delay'; import React, {useEffect, useMemo, useRef, useState} from 'react'; import type {ImageSourcePropType, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import FullscreenLoadingIndicator from './FullscreenLoadingIndicator'; @@ -44,16 +45,27 @@ function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthT const isLoadedRef = useRef(null); const [isImageCached, setIsImageCached] = useState(true); const [isLoading, setIsLoading] = useState(false); + const {isOffline} = useNetwork(); const source = useMemo(() => ({uri: url}), [url]); const onError = () => { Log.hmmm('Unable to fetch image to calculate size', {url}); onLoadFailure?.(); + if (isLoadedRef.current) { + isLoadedRef.current = false; + setIsImageCached(false); + } + if (isOffline) { + return; + } + setIsLoading(false); }; const imageLoadedSuccessfully = (event: OnLoadNativeEvent) => { isLoadedRef.current = true; + setIsLoading(false); + setIsImageCached(true); onMeasure({ width: event.nativeEvent.width, height: event.nativeEvent.height, @@ -87,10 +99,6 @@ function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthT } setIsLoading(true); }} - onLoadEnd={() => { - setIsLoading(false); - setIsImageCached(true); - }} onError={onError} onLoad={imageLoadedSuccessfully} /> diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 48e9aa49d0de..7313bb4aa7bb 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -45,7 +45,7 @@ type LocaleContextProps = { /** Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions */ - formatPhoneNumber: (phoneNumber: string | undefined) => string; + formatPhoneNumber: (phoneNumber: string) => string; /** Gets the locale digit corresponding to a standard digit */ toLocaleDigit: (digit: string) => string; diff --git a/src/components/MagicCodeInput.tsx b/src/components/MagicCodeInput.tsx index 46c96fd706a9..584b349c508f 100644 --- a/src/components/MagicCodeInput.tsx +++ b/src/components/MagicCodeInput.tsx @@ -430,3 +430,4 @@ function MagicCodeInput( MagicCodeInput.displayName = 'MagicCodeInput'; export default forwardRef(MagicCodeInput); +export type {MagicCodeInputHandle}; diff --git a/src/components/QRShare/index.tsx b/src/components/QRShare/index.tsx index 45a4a4fd4964..c7e9e7637a6c 100644 --- a/src/components/QRShare/index.tsx +++ b/src/components/QRShare/index.tsx @@ -9,15 +9,12 @@ import QRCode from '@components/QRCode'; import Text from '@components/Text'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import variables from '@styles/variables'; -import CONST from '@src/CONST'; import type {QRShareHandle, QRShareProps} from './types'; function QRShare({url, title, subtitle, logo, logoRatio, logoMarginRatio}: QRShareProps, ref: ForwardedRef) { const styles = useThemeStyles(); const theme = useTheme(); - const {isSmallScreenWidth} = useWindowDimensions(); const [qrCodeSize, setQrCodeSize] = useState(1); const svgRef = useRef(); @@ -32,11 +29,7 @@ function QRShare({url, title, subtitle, logo, logoRatio, logoMarginRatio}: QRSha const onLayout = (event: LayoutChangeEvent) => { const containerWidth = event.nativeEvent.layout.width - variables.qrShareHorizontalPadding * 2 || 0; - if (isSmallScreenWidth) { - setQrCodeSize(Math.max(1, containerWidth)); - return; - } - setQrCodeSize(Math.max(1, Math.min(containerWidth, CONST.CENTRAL_PANE_ANIMATION_HEIGHT))); + setQrCodeSize(Math.max(1, containerWidth)); }; return ( diff --git a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx index e2f7314afd73..4137b259f362 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx +++ b/src/components/ReportActionItem/MoneyRequestPreview/MoneyRequestPreviewContent.tsx @@ -4,6 +4,7 @@ import lodashSortBy from 'lodash/sortBy'; import React from 'react'; import {View} from 'react-native'; import type {GestureResponderEvent} from 'react-native'; +import ConfirmedRoute from '@components/ConfirmedRoute'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MoneyRequestSkeletonView from '@components/MoneyRequestSkeletonView'; @@ -114,6 +115,9 @@ function MoneyRequestPreviewContent({ const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(transaction)] : []; + const hasPendingWaypoints = transaction?.pendingFields?.waypoints; + const showMapAsImage = isDistanceRequest && hasPendingWaypoints; + const getSettledMessage = (): string => { if (isCardTransaction) { return translate('common.done'); @@ -206,7 +210,12 @@ function MoneyRequestPreviewContent({ !onPreviewPressed ? [styles.moneyRequestPreviewBox, containerStyles] : {}, ]} > - {hasReceipt && ( + {showMapAsImage && ( + + + + )} + {!showMapAsImage && hasReceipt && ( - {hasReceipt && ( + {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} + {(showMapAsImage || hasReceipt) && ( - + {showMapAsImage ? ( + + ) : ( + + )} )} diff --git a/src/components/__mocks__/ConfirmedRoute.tsx b/src/components/__mocks__/ConfirmedRoute.tsx new file mode 100644 index 000000000000..3c78e764ebea --- /dev/null +++ b/src/components/__mocks__/ConfirmedRoute.tsx @@ -0,0 +1,8 @@ +import {View} from 'react-native'; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any +function ConfirmedRoute(props: any) { + return ; +} + +export default ConfirmedRoute; diff --git a/src/components/withToggleVisibilityView.tsx b/src/components/withToggleVisibilityView.tsx index 17fda7fd5e30..9da862ecdebe 100644 --- a/src/components/withToggleVisibilityView.tsx +++ b/src/components/withToggleVisibilityView.tsx @@ -7,7 +7,7 @@ import getComponentDisplayName from '@libs/getComponentDisplayName'; type WithToggleVisibilityViewProps = { /** Whether the content is visible. */ - isVisible?: boolean; + isVisible: boolean; }; export default function withToggleVisibilityView( diff --git a/src/libs/API/parameters/UpdateRoomVisibilityParams.ts b/src/libs/API/parameters/UpdateRoomVisibilityParams.ts new file mode 100644 index 000000000000..a69559f0ce47 --- /dev/null +++ b/src/libs/API/parameters/UpdateRoomVisibilityParams.ts @@ -0,0 +1,8 @@ +import type {RoomVisibility} from '@src/types/onyx/Report'; + +type UpdateRoomVisibilityParams = { + reportID: string; + visibility: RoomVisibility; +}; + +export default UpdateRoomVisibilityParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 371fb8ddb404..2633d795b561 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -84,6 +84,7 @@ export type {default as DeleteCommentParams} from './DeleteCommentParams'; export type {default as UpdateCommentParams} from './UpdateCommentParams'; export type {default as UpdateReportNotificationPreferenceParams} from './UpdateReportNotificationPreferenceParams'; export type {default as UpdateRoomDescriptionParams} from './UpdateRoomDescriptionParams'; +export type {default as UpdateRoomVisibilityParams} from './UpdateRoomVisibilityParams'; export type {default as UpdateReportWriteCapabilityParams} from './UpdateReportWriteCapabilityParams'; export type {default as AddWorkspaceRoomParams} from './AddWorkspaceRoomParams'; export type {default as UpdatePolicyRoomNameParams} from './UpdatePolicyRoomNameParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 9c0d57b1cf14..35b03f21c841 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -85,6 +85,7 @@ const WRITE_COMMANDS = { DELETE_COMMENT: 'DeleteComment', UPDATE_COMMENT: 'UpdateComment', UPDATE_REPORT_NOTIFICATION_PREFERENCE: 'UpdateReportNotificationPreference', + UPDATE_ROOM_VISIBILITY: 'UpdateRoomVisibility', UPDATE_ROOM_DESCRIPTION: 'UpdateRoomDescription', UPDATE_REPORT_WRITE_CAPABILITY: 'UpdateReportWriteCapability', ADD_WORKSPACE_ROOM: 'AddWorkspaceRoom', @@ -226,6 +227,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.DELETE_COMMENT]: Parameters.DeleteCommentParams; [WRITE_COMMANDS.UPDATE_COMMENT]: Parameters.UpdateCommentParams; [WRITE_COMMANDS.UPDATE_REPORT_NOTIFICATION_PREFERENCE]: Parameters.UpdateReportNotificationPreferenceParams; + [WRITE_COMMANDS.UPDATE_ROOM_VISIBILITY]: Parameters.UpdateRoomVisibilityParams; [WRITE_COMMANDS.UPDATE_ROOM_DESCRIPTION]: Parameters.UpdateRoomDescriptionParams; [WRITE_COMMANDS.UPDATE_REPORT_WRITE_CAPABILITY]: Parameters.UpdateReportWriteCapabilityParams; [WRITE_COMMANDS.ADD_WORKSPACE_ROOM]: Parameters.AddWorkspaceRoomParams; diff --git a/src/libs/GetPhysicalCardUtils.ts b/src/libs/GetPhysicalCardUtils.ts index cf49ba03f287..24437da48953 100644 --- a/src/libs/GetPhysicalCardUtils.ts +++ b/src/libs/GetPhysicalCardUtils.ts @@ -3,17 +3,18 @@ import ROUTES from '@src/ROUTES'; import type {Route} from '@src/ROUTES'; import type {GetPhysicalCardForm} from '@src/types/form'; import type {LoginList, PrivatePersonalDetails} from '@src/types/onyx'; +import * as LoginUtils from './LoginUtils'; import Navigation from './Navigation/Navigation'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as UserUtils from './UserUtils'; -function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry): Route { +function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry): Route { const {address, legalFirstName, legalLastName, phoneNumber} = privatePersonalDetails ?? {}; if (!legalFirstName && !legalLastName) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_NAME.getRoute(domain); } - if (!phoneNumber && !UserUtils.getSecondaryPhoneLogin(loginList)) { + if (!phoneNumber || !LoginUtils.validateNumber(phoneNumber)) { return ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_PHONE.getRoute(domain); } if (!(address?.street && address?.city && address?.state && address?.country && address?.zip)) { @@ -23,8 +24,8 @@ function getCurrentRoute(domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) { - Navigation.navigate(getCurrentRoute(domain, privatePersonalDetails, loginList)); +function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: OnyxEntry) { + Navigation.navigate(getCurrentRoute(domain, privatePersonalDetails)); } /** @@ -35,8 +36,8 @@ function goToNextPhysicalCardRoute(domain: string, privatePersonalDetails: OnyxE * @param loginList * @returns */ -function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDetails: OnyxEntry, loginList: OnyxEntry) { - const expectedRoute = getCurrentRoute(domain, privatePersonalDetails, loginList); +function setCurrentRoute(currentRoute: string, domain: string, privatePersonalDetails: OnyxEntry) { + const expectedRoute = getCurrentRoute(domain, privatePersonalDetails); // If the user is on the current route or the current route is confirmation, then he's allowed to stay on the current step if ([currentRoute, ROUTES.SETTINGS_WALLET_CARD_GET_PHYSICAL_CONFIRM.getRoute(domain)].includes(expectedRoute)) { diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index 9aacc6968e1e..933aa7937560 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -13,7 +13,7 @@ Onyx.connect({ * Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions */ -function formatPhoneNumber(number: string | undefined): string { +function formatPhoneNumber(number: string): string { if (!number) { return ''; } diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index d9835b01ceff..9f4edd897f66 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -1,8 +1,8 @@ import type {ParamListBase} from '@react-navigation/routers'; import type {StackNavigationOptions} from '@react-navigation/stack'; -import {CardStyleInterpolators, createStackNavigator} from '@react-navigation/stack'; import React, {useMemo} from 'react'; import useThemeStyles from '@hooks/useThemeStyles'; +import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type { AddPersonalBankAccountNavigatorParamList, DetailsNavigatorParamList, @@ -35,6 +35,7 @@ import type { import type {ThemeStyles} from '@styles/index'; import type {Screen} from '@src/SCREENS'; import SCREENS from '@src/SCREENS'; +import subRouteOptions from './modalStackNavigatorOptions'; type Screens = Partial React.ComponentType>>; @@ -45,16 +46,15 @@ type Screens = Partial React.ComponentType>>; * @param getScreenOptions optional function that returns the screen options, override the default options */ function createModalStackNavigator(screens: Screens, getScreenOptions?: (styles: ThemeStyles) => StackNavigationOptions): React.ComponentType { - const ModalStackNavigator = createStackNavigator(); + const ModalStackNavigator = createPlatformStackNavigator(); function ModalStack() { const styles = useThemeStyles(); const defaultSubRouteOptions = useMemo( (): StackNavigationOptions => ({ + ...subRouteOptions, cardStyle: styles.navigationScreenCardStyle, - headerShown: false, - cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, }), [styles], ); @@ -133,6 +133,7 @@ const ReportSettingsModalStackNavigator = createModalStackNavigator 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, + [SCREENS.REPORT_SETTINGS.VISIBILITY]: () => require('../../../pages/settings/Report/VisibilityPage').default as React.ComponentType, }); const TaskModalStackNavigator = createModalStackNavigator({ @@ -191,7 +192,6 @@ const AccountSettingsModalStackNavigator = createModalStackNavigator( [SCREENS.SETTINGS.PREFERENCES.ROOT]: () => require('../../../pages/settings/Preferences/PreferencesPage').default as React.ComponentType, [SCREENS.SETTINGS.SECURITY]: () => require('../../../pages/settings/Security/SecuritySettingsPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.ROOT]: () => require('../../../pages/settings/Profile/ProfilePage').default as React.ComponentType, - [SCREENS.SETTINGS.SHARE_CODE]: () => require('../../../pages/ShareCodePage').default as React.ComponentType, [SCREENS.SETTINGS.WALLET.ROOT]: () => require('../../../pages/settings/Wallet/WalletPage').default as React.ComponentType, [SCREENS.SETTINGS.ABOUT]: () => require('../../../pages/settings/AboutPage/AboutPage').default as React.ComponentType, }, @@ -203,6 +203,7 @@ const WorkspaceSwitcherModalStackNavigator = createModalStackNavigator({ + [SCREENS.SETTINGS.SHARE_CODE]: () => require('../../../pages/ShareCodePage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.PRONOUNS]: () => require('../../../pages/settings/Profile/PronounsPage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.DISPLAY_NAME]: () => require('../../../pages/settings/Profile/DisplayNamePage').default as React.ComponentType, [SCREENS.SETTINGS.PROFILE.TIMEZONE]: () => require('../../../pages/settings/Profile/TimezoneInitialPage').default as React.ComponentType, diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index 087e963b3892..14aa6de27116 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -1,12 +1,12 @@ -import {createStackNavigator} from '@react-navigation/stack'; import React from 'react'; import useThemeStyles from '@hooks/useThemeStyles'; import ReportScreenWrapper from '@libs/Navigation/AppNavigator/ReportScreenWrapper'; import getCurrentUrl from '@libs/Navigation/currentUrl'; +import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type {CentralPaneNavigatorParamList} from '@navigation/types'; import SCREENS from '@src/SCREENS'; -const Stack = createStackNavigator(); +const Stack = createPlatformStackNavigator(); const url = getCurrentUrl(); const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : undefined; diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.tsx new file mode 100644 index 000000000000..30651e32cbd6 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.native.tsx @@ -0,0 +1,7 @@ +function Overlay() { + return null; +} + +Overlay.displayName = 'Overlay'; + +export default Overlay; diff --git a/src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx b/src/libs/Navigation/AppNavigator/Navigators/Overlay/index.tsx similarity index 100% rename from src/libs/Navigation/AppNavigator/Navigators/Overlay.tsx rename to src/libs/Navigation/AppNavigator/Navigators/Overlay/index.tsx diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index c421bdc82028..550fb947a4e6 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -1,5 +1,4 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import {createStackNavigator} from '@react-navigation/stack'; import React, {useMemo, useRef} from 'react'; import {View} from 'react-native'; import NoDropZone from '@components/DragAndDrop/NoDropZone'; @@ -7,6 +6,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import ModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/ModalNavigatorScreenOptions'; import * as ModalStackNavigators from '@libs/Navigation/AppNavigator/ModalStackNavigators'; +import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type {AuthScreensParamList, RightModalNavigatorParamList} from '@navigation/types'; import type NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; @@ -14,7 +14,7 @@ import Overlay from './Overlay'; type RightModalNavigatorProps = StackScreenProps; -const Stack = createStackNavigator(); +const Stack = createPlatformStackNavigator(); function RightModalNavigator({navigation}: RightModalNavigatorProps) { const styles = useThemeStyles(); diff --git a/src/libs/Navigation/AppNavigator/PublicScreens.tsx b/src/libs/Navigation/AppNavigator/PublicScreens.tsx index 6b1557994627..792a538cfd39 100644 --- a/src/libs/Navigation/AppNavigator/PublicScreens.tsx +++ b/src/libs/Navigation/AppNavigator/PublicScreens.tsx @@ -1,5 +1,5 @@ -import {createStackNavigator} from '@react-navigation/stack'; import React from 'react'; +import createPlatformStackNavigator from '@libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator'; import type {PublicScreensParamList} from '@navigation/types'; import LogInWithShortLivedAuthTokenPage from '@pages/LogInWithShortLivedAuthTokenPage'; import AppleSignInDesktopPage from '@pages/signin/AppleSignInDesktopPage'; @@ -12,7 +12,7 @@ import NAVIGATORS from '@src/NAVIGATORS'; import SCREENS from '@src/SCREENS'; import defaultScreenOptions from './defaultScreenOptions'; -const RootStack = createStackNavigator(); +const RootStack = createPlatformStackNavigator(); function PublicScreens() { return ( diff --git a/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts new file mode 100644 index 000000000000..17100bc71bda --- /dev/null +++ b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.native.ts @@ -0,0 +1,11 @@ +const defaultScreenOptions = { + contentStyle: { + overflow: 'visible', + flex: 1, + }, + headerShown: false, + animationTypeForReplace: 'push', + animation: 'slide_from_right', +}; + +export default defaultScreenOptions; diff --git a/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.ts b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.ts new file mode 100644 index 000000000000..4015c43c679e --- /dev/null +++ b/src/libs/Navigation/AppNavigator/defaultScreenOptions/index.ts @@ -0,0 +1,12 @@ +import type {StackNavigationOptions} from '@react-navigation/stack'; + +const defaultScreenOptions: StackNavigationOptions = { + cardStyle: { + overflow: 'visible', + flex: 1, + }, + headerShown: false, + animationTypeForReplace: 'push', +}; + +export default defaultScreenOptions; diff --git a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts new file mode 100644 index 000000000000..2b062fd2f2be --- /dev/null +++ b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.native.ts @@ -0,0 +1,8 @@ +import type {NativeStackNavigationOptions} from '@react-navigation/native-stack'; + +const rightModalNavigatorOptions = (): NativeStackNavigationOptions => ({ + presentation: 'card', + animation: 'slide_from_right', +}); + +export default rightModalNavigatorOptions; diff --git a/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts new file mode 100644 index 000000000000..935c0041b794 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/getRightModalNavigatorOptions/index.ts @@ -0,0 +1,20 @@ +import type {StackNavigationOptions} from '@react-navigation/stack'; +// eslint-disable-next-line no-restricted-imports +import getNavigationModalCardStyle from '@styles/utils/getNavigationModalCardStyles'; + +const rightModalNavigatorOptions = (isSmallScreenWidth: boolean): StackNavigationOptions => ({ + presentation: 'transparentModal', + + // We want pop in RHP since there are some flows that would work weird otherwise + animationTypeForReplace: 'pop', + cardStyle: { + ...getNavigationModalCardStyle(), + + // This is necessary to cover translated sidebar with overlay. + width: isSmallScreenWidth ? '100%' : '200%', + // Excess space should be on the left so we need to position from right. + right: 0, + }, +}); + +export default rightModalNavigatorOptions; diff --git a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts index c3a69bbd7ccf..5685afec5459 100644 --- a/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts +++ b/src/libs/Navigation/AppNavigator/getRootNavigatorScreenOptions.ts @@ -4,6 +4,7 @@ import type {StyleUtilsType} from '@styles/utils'; import variables from '@styles/variables'; import CONFIG from '@src/CONFIG'; import createModalCardStyleInterpolator from './createModalCardStyleInterpolator'; +import getRightModalNavigatorOptions from './getRightModalNavigatorOptions'; type ScreenOptions = Record; @@ -25,23 +26,12 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr return { rightModalNavigator: { ...commonScreenOptions, + ...getRightModalNavigatorOptions(isSmallScreenWidth), cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), - presentation: 'transparentModal', - - // We want pop in RHP since there are some flows that would work weird otherwise - animationTypeForReplace: 'pop', - cardStyle: { - ...StyleUtils.getNavigationModalCardStyle(), - - // This is necessary to cover translated sidebar with overlay. - width: isSmallScreenWidth ? '100%' : '200%', - // Excess space should be on the left so we need to position from right. - right: 0, - }, }, leftModalNavigator: { ...commonScreenOptions, - cardStyleInterpolator: (props) => modalCardStyleInterpolator(isSmallScreenWidth, false, props, SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER), + cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props, SLIDE_LEFT_OUTPUT_RANGE_MULTIPLIER), presentation: 'transparentModal', // We want pop in LHP since there are some flows that would work weird otherwise @@ -59,8 +49,8 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr homeScreen: { title: CONFIG.SITE_TITLE, ...commonScreenOptions, + // Note: The card* properties won't be applied on mobile platforms, as they use the native defaults. cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, false, props), - cardStyle: { ...StyleUtils.getNavigationModalCardStyle(), width: isSmallScreenWidth ? '100%' : variables.sideBarWidth, @@ -73,6 +63,7 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr fullScreen: { ...commonScreenOptions, + cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props), cardStyle: { ...StyleUtils.getNavigationModalCardStyle(), @@ -87,7 +78,9 @@ const getRootNavigatorScreenOptions: GetRootNavigatorScreenOptions = (isSmallScr ...commonScreenOptions, animationEnabled: isSmallScreenWidth, cardStyleInterpolator: (props: StackCardInterpolationProps) => modalCardStyleInterpolator(isSmallScreenWidth, true, props), - + // temporary solution - better to hide a keyboard than see keyboard flickering + // see https://github.com/software-mansion/react-native-screens/issues/2021 for more details + keyboardHandlingEnabled: true, cardStyle: { ...StyleUtils.getNavigationModalCardStyle(), paddingRight: isSmallScreenWidth ? 0 : variables.sideBarWidth, diff --git a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts new file mode 100644 index 000000000000..ca9769fa9972 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.native.ts @@ -0,0 +1,8 @@ +import type {NativeStackNavigationOptions} from '@react-navigation/native-stack'; + +const defaultSubRouteOptions: NativeStackNavigationOptions = { + headerShown: false, + animation: 'slide_from_right', +}; + +export default defaultSubRouteOptions; diff --git a/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts new file mode 100644 index 000000000000..280a38b263b7 --- /dev/null +++ b/src/libs/Navigation/AppNavigator/modalStackNavigatorOptions/index.ts @@ -0,0 +1,9 @@ +import type {StackNavigationOptions} from '@react-navigation/stack'; +import {CardStyleInterpolators} from '@react-navigation/stack'; + +const defaultSubRouteOptions: StackNavigationOptions = { + headerShown: false, + cardStyleInterpolator: CardStyleInterpolators.forHorizontalIOS, +}; + +export default defaultSubRouteOptions; diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.native.ts b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.native.ts new file mode 100644 index 000000000000..ef44cefc13c9 --- /dev/null +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.native.ts @@ -0,0 +1,7 @@ +import {createNativeStackNavigator} from '@react-navigation/native-stack'; + +function createPlatformStackNavigator() { + return createNativeStackNavigator(); +} + +export default createPlatformStackNavigator; diff --git a/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.ts b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.ts new file mode 100644 index 000000000000..51228295572f --- /dev/null +++ b/src/libs/Navigation/PlatformStackNavigation/createPlatformStackNavigator.ts @@ -0,0 +1,5 @@ +import {createStackNavigator} from '@react-navigation/stack'; + +const createPlatformStackNavigator: typeof createStackNavigator = () => createStackNavigator(); + +export default createPlatformStackNavigator; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 74a00dec0a1f..2640025efa09 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -261,6 +261,9 @@ const config: LinkingOptions['config'] = { path: ROUTES.KEYBOARD_SHORTCUTS, }, [SCREENS.WORKSPACE.NAME]: ROUTES.WORKSPACE_PROFILE_NAME.route, + [SCREENS.SETTINGS.SHARE_CODE]: { + path: ROUTES.SETTINGS_SHARE_CODE, + }, }, }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { @@ -289,6 +292,9 @@ const config: LinkingOptions['config'] = { [SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: { path: ROUTES.REPORT_SETTINGS_WRITE_CAPABILITY.route, }, + [SCREENS.REPORT_SETTINGS.VISIBILITY]: { + path: ROUTES.REPORT_SETTINGS_VISIBILITY.route, + }, }, }, [SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: { @@ -495,10 +501,6 @@ const config: LinkingOptions['config'] = { }, [SCREENS.SETTINGS_CENTRAL_PANE]: { screens: { - [SCREENS.SETTINGS.SHARE_CODE]: { - path: ROUTES.SETTINGS_SHARE_CODE, - exact: true, - }, [SCREENS.SETTINGS.PROFILE.ROOT]: { path: ROUTES.SETTINGS_PROFILE, exact: true, diff --git a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts index 8e246d82ff72..e7c5466852cf 100644 --- a/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts +++ b/src/libs/Navigation/linkingConfig/getAdaptedStateFromPath.ts @@ -70,14 +70,16 @@ function createCentralPaneNavigator(route: NavigationPartialRoute): NavigationPartialRoute { +function createFullScreenNavigator(route?: NavigationPartialRoute): NavigationPartialRoute { const routes = []; routes.push({name: SCREENS.SETTINGS.ROOT}); - routes.push({ - name: SCREENS.SETTINGS_CENTRAL_PANE, - state: getRoutesWithIndex([route]), - }); + if (route) { + routes.push({ + name: SCREENS.SETTINGS_CENTRAL_PANE, + state: getRoutesWithIndex([route]), + }); + } return { name: NAVIGATORS.FULL_SCREEN_NAVIGATOR, @@ -129,6 +131,11 @@ function getMatchingRootRouteForRHPRoute( return createFullScreenNavigator({name: fullScreenName as FullScreenName, params: route.params}); } } + + // This screen is opened from the LHN of the FullStackNavigator, so in this case we shouldn't push any CentralPane screen + if (route.name === SCREENS.SETTINGS.SHARE_CODE) { + return createFullScreenNavigator(); + } } function getAdaptedState(state: PartialState>, policyID?: string): GetAdaptedStateReturnType { diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 04bc25736887..81229f353e52 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -203,6 +203,9 @@ type ReportSettingsNavigatorParamList = { [SCREENS.REPORT_SETTINGS.ROOM_NAME]: undefined; [SCREENS.REPORT_SETTINGS.NOTIFICATION_PREFERENCES]: undefined; [SCREENS.REPORT_SETTINGS.WRITE_CAPABILITY]: undefined; + [SCREENS.REPORT_SETTINGS.VISIBILITY]: { + reportID: string; + }; }; type ReportDescriptionNavigatorParamList = { @@ -416,6 +419,7 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.NEW_CHAT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.DETAILS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.PROFILE]: NavigatorScreenParams; + [SCREENS.SETTINGS.SHARE_CODE]: undefined; [SCREENS.RIGHT_MODAL.REPORT_DETAILS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_SETTINGS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.REPORT_DESCRIPTION]: NavigatorScreenParams; @@ -440,7 +444,6 @@ type RightModalNavigatorParamList = { }; type SettingsCentralPaneNavigatorParamList = { - [SCREENS.SETTINGS.SHARE_CODE]: undefined; [SCREENS.SETTINGS.PROFILE.ROOT]: undefined; [SCREENS.SETTINGS.PREFERENCES.ROOT]: undefined; [SCREENS.SETTINGS.SECURITY]: undefined; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 80081061f340..97b4fc0144c8 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -1494,6 +1494,10 @@ function getOptions( return; } + if (!accountIDs || accountIDs.length === 0) { + return; + } + // Save the report in the map if this is a single participant so we can associate the reportID with the // personal detail option later. Individuals should not be associated with single participant // policyExpenseChats or chatRooms since those are not people. diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 05e2db66d629..ae6e02e70d29 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2370,7 +2370,9 @@ function getReportPreviewMessage( if (isSettled(report.reportID) || (report.isWaitingOnBankAccount && isPreviewMessageForParentChatReport)) { // A settled report preview message can come in three formats "paid ... elsewhere" or "paid ... with Expensify" let translatePhraseKey: TranslationPaths = 'iou.paidElsewhereWithAmount'; - if ( + if (isPreviewMessageForParentChatReport) { + translatePhraseKey = 'iou.payerPaidAmount'; + } else if ( [CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].some((paymentType) => paymentType === originalMessage?.paymentType) || !!reportActionMessage.match(/ (with Expensify|using Expensify)$/) || report.isWaitingOnBankAccount @@ -4525,6 +4527,13 @@ function canEditWriteCapability(report: OnyxEntry, policy: OnyxEntry, policy: OnyxEntry): boolean { + return PolicyUtils.isPolicyAdmin(policy) && !isArchivedRoom(report); +} + /** * Returns the onyx data needed for the task assignee chat */ @@ -4704,7 +4713,6 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) // property. If it does, it indicates that this is a 'Send money' action. const {amount, currency} = originalMessage.IOUDetails ?? originalMessage; const formattedAmount = CurrencyUtils.convertToDisplayString(Math.abs(amount), currency) ?? ''; - const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true); switch (originalMessage.paymentType) { case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: @@ -4718,7 +4726,7 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) translationKey = 'iou.payerPaidAmount'; break; } - return Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName ?? ''}); + return Localize.translateLocal(translationKey, {amount: formattedAmount, payer: ''}); } const transaction = TransactionUtils.getTransaction(originalMessage.IOUTransactionID ?? ''); @@ -5215,6 +5223,7 @@ export { getAvailableReportFields, reportFieldsEnabled, getAllAncestorReportActionIDs, + canEditRoomVisibility, canEditPolicyDescription, getPolicyDescriptionText, }; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 49436576295c..d9298817f6b7 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -242,7 +242,9 @@ function getOptionData({ result.policyID = report.policyID; result.stateNum = report.stateNum; result.statusNum = report.statusNum; - result.isUnread = ReportUtils.isUnread(report); + // When the only message of a report is deleted lastVisibileActionCreated is not reset leading to wrongly + // setting it Unread so we add additional condition here to avoid empty chat LHN from being bold. + result.isUnread = ReportUtils.isUnread(report) && !!report.lastActorAccountID; result.isUnreadWithMention = ReportUtils.isUnreadWithMention(report); result.hasDraftComment = report.hasDraft; result.isPinned = report.isPinned; diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index d3eafc6554db..0a13d561891c 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -6,8 +6,9 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {RecentWaypoint, Report, ReportAction, Transaction, TransactionViolation} from '@src/types/onyx'; import type {PolicyTaxRate, PolicyTaxRates} from '@src/types/onyx/PolicyTaxRates'; -import type {Comment, Receipt, TransactionChanges, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; +import type {Comment, Receipt, TransactionChanges, TransactionPendingFieldsKey, Waypoint, WaypointCollection} from '@src/types/onyx/Transaction'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {isCorporateCard, isExpensifyCard} from './CardUtils'; import DateUtils from './DateUtils'; import * as NumberUtils from './NumberUtils'; @@ -94,6 +95,7 @@ function buildOptimisticTransaction( category = '', tag = '', billable = false, + pendingFields: Partial<{[K in TransactionPendingFieldsKey]: ValueOf}> | undefined = undefined, ): Transaction { // transactionIDs are random, positive, 64-bit numeric strings. // Because JS can only handle 53-bit numbers, transactionIDs are strings in the front-end (just like reportActionID) @@ -108,6 +110,7 @@ function buildOptimisticTransaction( } return { + ...(!isEmptyObject(pendingFields) ? {pendingFields} : {}), transactionID, amount, currency, diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index a941be0dbb75..f39728e7d31c 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -794,6 +794,8 @@ function getMoneyRequestInformation( receiptObject.state = receipt.state ?? CONST.IOU.RECEIPT_STATE.SCANREADY; filename = receipt.name; } + const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; + const isDistanceRequest = existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE; let optimisticTransaction = TransactionUtils.buildOptimisticTransaction( ReportUtils.isExpenseReport(iouReport) ? -amount : amount, currency, @@ -809,6 +811,7 @@ function getMoneyRequestInformation( category, tag, billable, + isDistanceRequest ? {waypoints: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : undefined, ); const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category); @@ -819,8 +822,7 @@ function getMoneyRequestInformation( // data. This is a big can of worms to change it to `Onyx.merge()` as explored in https://expensify.slack.com/archives/C05DWUDHVK7/p1692139468252109. // I want to clean this up at some point, but it's possible this will live in the code for a while so I've created https://github.com/Expensify/App/issues/25417 // to remind me to do this. - const existingTransaction = allTransactionDrafts[`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`]; - if (existingTransaction && existingTransaction.iouRequestType === CONST.IOU.REQUEST_TYPE.DISTANCE) { + if (isDistanceRequest) { optimisticTransaction = fastMerge(existingTransaction, optimisticTransaction, false); } @@ -1008,7 +1010,7 @@ function calculateDiffAmount(iouReport: OnyxEntry, updatedTran // Subtract the diff from the total if we change the currency from the currency of IOU report to a different currency return -updatedAmount; } - if (updatedCurrency === iouReport?.currency && updatedTransaction?.modifiedAmount) { + if (updatedCurrency === iouReport?.currency && updatedAmount !== currentAmount) { // Calculate the diff between the updated amount and the current amount if we change the amount and the currency of the transaction is the currency of the report return updatedAmount - currentAmount; } @@ -1132,32 +1134,32 @@ function getUpdateMoneyRequestParams( }, }, }); + } - // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. - let updatedMoneyRequestReport = {...iouReport}; - const diff = calculateDiffAmount(iouReport, updatedTransaction, transaction); - - if (ReportUtils.isExpenseReport(iouReport) && typeof updatedMoneyRequestReport.total === 'number') { - // For expense report, the amount is negative so we should subtract total from diff - updatedMoneyRequestReport.total -= diff; - } else { - updatedMoneyRequestReport = iouReport - ? IOUUtils.updateIOUOwnerAndTotal(iouReport, updatedReportAction.actorAccountID ?? -1, diff, TransactionUtils.getCurrency(transaction), false, true) - : {}; - } - updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, transactionDetails?.currency); + // Step 4: Compute the IOU total and update the report preview message (and report header) so LHN amount owed is correct. + let updatedMoneyRequestReport = {...iouReport}; + const diff = calculateDiffAmount(iouReport, updatedTransaction, transaction); - optimisticData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: updatedMoneyRequestReport, - }); - successData.push({ - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, - value: {pendingAction: null}, - }); + if (ReportUtils.isExpenseReport(iouReport) && typeof updatedMoneyRequestReport.total === 'number') { + // For expense report, the amount is negative so we should subtract total from diff + updatedMoneyRequestReport.total -= diff; + } else { + updatedMoneyRequestReport = iouReport + ? IOUUtils.updateIOUOwnerAndTotal(iouReport, updatedReportAction.actorAccountID ?? -1, diff, TransactionUtils.getCurrency(transaction), false, true) + : {}; } + updatedMoneyRequestReport.cachedTotal = CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, transactionDetails?.currency); + + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: updatedMoneyRequestReport, + }); + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport?.reportID}`, + value: {pendingAction: null}, + }); // Optimistically modify the transaction and the transaction thread optimisticData.push({ @@ -3298,9 +3300,10 @@ function getSendMoneyParams( } function getPayMoneyRequestParams(chatReport: OnyxTypes.Report, iouReport: OnyxTypes.Report, recipient: Participant, paymentMethodType: PaymentMethodType): PayMoneyRequestData { + const total = iouReport.total ?? 0; const optimisticIOUReportAction = ReportUtils.buildOptimisticIOUReportAction( CONST.IOU.REPORT_ACTION_TYPE.PAY, - -(iouReport.total ?? 0), + ReportUtils.isExpenseReport(iouReport) ? -total : total, iouReport.currency ?? '', '', [recipient], diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index e236002ee704..1276207e37c3 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -40,6 +40,7 @@ import type { UpdateReportWriteCapabilityParams, UpdateRoomDescriptionParams, } from '@libs/API/parameters'; +import type UpdateRoomVisibilityParams from '@libs/API/parameters/UpdateRoomVisibilityParams'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CollectionUtils from '@libs/CollectionUtils'; import DateUtils from '@libs/DateUtils'; @@ -68,7 +69,7 @@ import type {Route} from '@src/ROUTES'; import ROUTES from '@src/ROUTES'; import type {PersonalDetails, PersonalDetailsList, PolicyReportField, RecentlyUsedReportFields, ReportActionReactions, ReportMetadata, ReportUserIsTyping} from '@src/types/onyx'; import type {Decision, OriginalMessageIOU} from '@src/types/onyx/OriginalMessage'; -import type {NotificationPreference, WriteCapability} from '@src/types/onyx/Report'; +import type {NotificationPreference, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type Report from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; @@ -85,12 +86,14 @@ type ActionSubscriber = { callback: SubscriberCallback; }; +let conciergeChatReportID: string | undefined; let currentUserAccountID = -1; Onyx.connect({ key: ONYXKEYS.SESSION, callback: (value) => { // When signed out, val is undefined if (!value?.accountID) { + conciergeChatReportID = undefined; return; } @@ -167,7 +170,6 @@ Onyx.connect({ }); const allReports: OnyxCollection = {}; -let conciergeChatReportID: string | undefined; const typingWatchTimers: Record = {}; let reportIDDeeplinkedFromOldDot: string | undefined; @@ -1442,6 +1444,38 @@ function updateNotificationPreference( } } +function updateRoomVisibility(reportID: string, previousValue: RoomVisibility | undefined, newValue: RoomVisibility, navigate: boolean, report: OnyxEntry | EmptyObject = {}) { + if (previousValue === newValue) { + if (navigate && !isEmptyObject(report) && report.reportID) { + ReportUtils.goBackToDetailsPage(report); + } + return; + } + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: {visibility: newValue}, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: {visibility: previousValue}, + }, + ]; + + const parameters: UpdateRoomVisibilityParams = {reportID, visibility: newValue}; + + API.write(WRITE_COMMANDS.UPDATE_ROOM_VISIBILITY, parameters, {optimisticData, failureData}); + if (navigate && !isEmptyObject(report)) { + ReportUtils.goBackToDetailsPage(report); + } +} + /** * This will subscribe to an existing thread, or create a new one and then subsribe to it if necessary * @@ -1683,24 +1717,29 @@ function updateWriteCapabilityAndNavigate(report: Report, newValue: WriteCapabil /** * Navigates to the 1:1 report with Concierge - * - * @param ignoreConciergeReportID - Flag to ignore conciergeChatReportID during navigation. The default behavior is to not ignore. */ -function navigateToConciergeChat(ignoreConciergeReportID = false, shouldDismissModal = false) { +function navigateToConciergeChat(shouldDismissModal = false, shouldPopCurrentScreen = false, checkIfCurrentPageActive = () => true) { // If conciergeChatReportID contains a concierge report ID, we navigate to the concierge chat using the stored report ID. // Otherwise, we would find the concierge chat and navigate to it. - // Now, when user performs sign-out and a sign-in again, conciergeChatReportID may contain a stale value. - // In order to prevent navigation to a stale value, we use ignoreConciergeReportID to forcefully find and navigate to concierge chat. - if (!conciergeChatReportID || ignoreConciergeReportID) { + if (!conciergeChatReportID) { // In order to avoid creating concierge repeatedly, // we need to ensure that the server data has been successfully pulled Welcome.serverDataIsReadyPromise().then(() => { // If we don't have a chat with Concierge then create it + if (!checkIfCurrentPageActive()) { + return; + } + if (shouldPopCurrentScreen && !shouldDismissModal) { + Navigation.goBack(); + } navigateToAndOpenReport([CONST.EMAIL.CONCIERGE], shouldDismissModal); }); } else if (shouldDismissModal) { Navigation.dismissModal(conciergeChatReportID); } else { + if (shouldPopCurrentScreen) { + Navigation.goBack(); + } Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(conciergeChatReportID)); } } @@ -2180,10 +2219,7 @@ function openReportFromDeepLink(url: string, isAuthenticated: boolean) { Session.waitForUserSignIn().then(() => { Navigation.waitForProtectedRoutes().then(() => { const route = ReportUtils.getRouteFromLink(url); - if (route === ROUTES.CONCIERGE) { - navigateToConciergeChat(true); - return; - } + if (route && Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) { Session.signOutAndRedirectToSignIn(true); return; @@ -2926,4 +2962,5 @@ export { updateReportField, updateReportName, resolveActionableMentionWhisper, + updateRoomVisibility, }; diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts index 7416b4f07e5e..f384e38f6d55 100644 --- a/src/libs/actions/Session/index.ts +++ b/src/libs/actions/Session/index.ts @@ -890,7 +890,7 @@ const canAccessRouteByAnonymousUser = (route: string) => { if (route.startsWith('/')) { routeRemovedReportId = routeRemovedReportId.slice(1); } - const routesCanAccessByAnonymousUser = [ROUTES.SIGN_IN_MODAL, ROUTES.REPORT_WITH_ID_DETAILS.route, ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.route]; + const routesCanAccessByAnonymousUser = [ROUTES.SIGN_IN_MODAL, ROUTES.REPORT_WITH_ID_DETAILS.route, ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.route, ROUTES.CONCIERGE]; if ((routesCanAccessByAnonymousUser as string[]).includes(routeRemovedReportId)) { return true; diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts b/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts index cd50938c70b9..68c750b05a5f 100644 --- a/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts +++ b/src/libs/setShouldShowComposeInputKeyboardAware/index.ios.ts @@ -1,5 +1,3 @@ import setShouldShowComposeInputKeyboardAwareBuilder from './setShouldShowComposeInputKeyboardAwareBuilder'; -// On iOS, there is a visible delay in displaying input after the keyboard has been closed with the `keyboardDidHide` event -// Because of that - on iOS we can use `keyboardWillHide` that is not available on android -export default setShouldShowComposeInputKeyboardAwareBuilder('keyboardWillHide'); +export default setShouldShowComposeInputKeyboardAwareBuilder('keyboardDidHide'); diff --git a/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts b/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts index 8d5ef578b66c..72df7a730e02 100644 --- a/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts +++ b/src/libs/setShouldShowComposeInputKeyboardAware/setShouldShowComposeInputKeyboardAwareBuilder.ts @@ -5,8 +5,6 @@ import * as Composer from '@userActions/Composer'; import type SetShouldShowComposeInputKeyboardAware from './types'; let keyboardEventListener: EmitterSubscription | null = null; -// On iOS, there is a visible delay in displaying input after the keyboard has been closed with the `keyboardDidHide` event -// Because of that - on iOS we can use `keyboardWillHide` that is not available on android const setShouldShowComposeInputKeyboardAwareBuilder: (keyboardEvent: KeyboardEventName) => SetShouldShowComposeInputKeyboardAware = (keyboardEvent: KeyboardEventName) => (shouldShow: boolean) => { diff --git a/src/pages/ConciergePage.tsx b/src/pages/ConciergePage.tsx index 251728866a54..4abf8f0d2033 100644 --- a/src/pages/ConciergePage.tsx +++ b/src/pages/ConciergePage.tsx @@ -1,11 +1,16 @@ import {useFocusEffect} from '@react-navigation/native'; import type {StackScreenProps} from '@react-navigation/stack'; -import React from 'react'; +import React, {useEffect, useRef} from 'react'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; +import ReportHeaderSkeletonView from '@components/ReportHeaderSkeletonView'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {AuthScreensParamList} from '@libs/Navigation/types'; +import * as App from '@userActions/App'; import * as Report from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; @@ -24,19 +29,39 @@ type ConciergePageProps = ConciergePageOnyxProps & StackScreenProps { if (session && 'authToken' in session) { + App.confirmReadyToOpenApp(); // Pop the concierge loading page before opening the concierge report. Navigation.isNavigationReady().then(() => { - Navigation.goBack(); - Report.navigateToConciergeChat(); + if (isUnmounted.current) { + return; + } + Report.navigateToConciergeChat(undefined, true, () => !isUnmounted.current); }); } else { Navigation.navigate(); } }); - return ; + useEffect( + () => () => { + isUnmounted.current = true; + }, + [], + ); + + return ( + + + + + + + ); } ConciergePage.displayName = 'ConciergePage'; diff --git a/src/pages/RoomMembersPage.tsx b/src/pages/RoomMembersPage.tsx index a19a815664ce..7593857536a6 100644 --- a/src/pages/RoomMembersPage.tsx +++ b/src/pages/RoomMembersPage.tsx @@ -189,7 +189,7 @@ function RoomMembersPage({report, session, policies}: RoomMembersPageProps) { isSelected: selectedMembers.includes(accountID), isDisabled: accountID === session?.accountID, text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), - alternateText: formatPhoneNumber(details.login), + alternateText: details?.login ? formatPhoneNumber(details.login) : '', icons: [ { source: UserUtils.getAvatar(details.avatar, accountID), diff --git a/src/pages/SearchPage/index.js b/src/pages/SearchPage/index.js index 6db69cea13e4..0c17e58837c1 100644 --- a/src/pages/SearchPage/index.js +++ b/src/pages/SearchPage/index.js @@ -144,6 +144,7 @@ function SearchPage({betas, reports, isSearchingForReports}) { includeSafeAreaPaddingBottom={false} testID={SearchPage.displayName} onEntryTransitionEnd={handleScreenTransitionEnd} + shouldEnableMaxHeight > {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( <> diff --git a/src/pages/ShareCodePage.tsx b/src/pages/ShareCodePage.tsx index f17b81041236..f2bba4b17a9a 100644 --- a/src/pages/ShareCodePage.tsx +++ b/src/pages/ShareCodePage.tsx @@ -6,17 +6,14 @@ import expensifyLogo from '@assets/images/expensify-logo-round-transparent.png'; import ContextMenuItem from '@components/ContextMenuItem'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; -import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; import QRShareWithDownload from '@components/QRShare/QRShareWithDownload'; import type QRShareWithDownloadHandle from '@components/QRShare/QRShareWithDownload/types'; import ScreenWrapper from '@components/ScreenWrapper'; -import Section from '@components/Section'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import Clipboard from '@libs/Clipboard'; import getPlatform from '@libs/getPlatform'; import Navigation from '@libs/Navigation/Navigation'; @@ -39,7 +36,6 @@ function ShareCodePage({report}: ShareCodePageProps) { const {translate} = useLocalize(); const {environmentURL} = useEnvironment(); const qrCodeRef = useRef(null); - const {isSmallScreenWidth} = useWindowDimensions(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const isReport = !!report?.reportID; @@ -71,72 +67,52 @@ function ShareCodePage({report}: ShareCodePageProps) { const isNative = platform === CONST.PLATFORM.IOS || platform === CONST.PLATFORM.ANDROID; return ( - + Navigation.goBack(isReport ? ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID) : undefined)} - shouldShowBackButton={isReport || isSmallScreenWidth} - icon={Illustrations.QRCode} + shouldShowBackButton /> - -
- - - + + + - - Clipboard.setString(url)} - shouldLimitWidth={false} - wrapperStyle={themeStyles.sectionMenuItemTopDescription} - /> + + Clipboard.setString(url)} + shouldLimitWidth={false} + /> - {isNative && ( - qrCodeRef.current?.download?.()} - wrapperStyle={themeStyles.sectionMenuItemTopDescription} - /> - )} + {isNative && ( + qrCodeRef.current?.download?.()} + /> + )} - - Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE, Navigation.getActiveRouteWithoutParams())) - } - wrapperStyle={themeStyles.sectionMenuItemTopDescription} - shouldShowRightIcon - /> - -
+ Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE, Navigation.getActiveRouteWithoutParams()))} + shouldShowRightIcon + />
diff --git a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx index 213d94f51f81..52b62c2d15b3 100755 --- a/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/BaseReportActionContextMenu.tsx @@ -208,6 +208,7 @@ function BaseReportActionContextMenu({ undefined, undefined, filteredContextMenuActions, + true, ); }; diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx index 9c8c6a8b37e7..0b4154a15e80 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -65,7 +65,7 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef([]); const contentRef = useRef(null); - const anchorRef = useRef(null); + const anchorRef = useRef(null); const dimensionsEventListener = useRef(null); const contextMenuAnchorRef = useRef(null); const contextMenuTargetNode = useRef(null); @@ -163,11 +163,16 @@ function PopoverReportActionContextMenu(_props: never, ref: ForwardedRef { const {pageX = 0, pageY = 0} = extractPointerEvent(event); contextMenuAnchorRef.current = contextMenuAnchor; contextMenuTargetNode.current = event.target as HTMLElement; - + if (shouldCloseOnTarget) { + anchorRef.current = event.target as HTMLDivElement; + } else { + anchorRef.current = null; + } setInstanceID(Math.random().toString(36).substr(2, 5)); onPopoverShow.current = onShow; diff --git a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts index d8570bd14510..6664a38d2e19 100644 --- a/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts +++ b/src/pages/home/report/ContextMenu/ReportActionContextMenu.ts @@ -34,6 +34,7 @@ type ShowContextMenu = ( isPinnedChat?: boolean, isUnreadChat?: boolean, disabledOptions?: ContextMenuAction[], + shouldCloseOnTarget?: boolean, ) => void; type ReportActionContextMenu = { @@ -113,6 +114,7 @@ function showContextMenu( isPinnedChat = false, isUnreadChat = false, disabledActions: ContextMenuAction[] = [], + shouldCloseOnTarget = false, ) { if (!contextMenuRef.current) { return; @@ -140,6 +142,7 @@ function showContextMenu( isPinnedChat, isUnreadChat, disabledActions, + shouldCloseOnTarget, ); } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index 8ec0bce9d1a7..4bbf3d393213 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -446,7 +446,12 @@ function ReportActionCompose({ onBlur={onBlur} measureParentContainer={measureContainer} listHeight={listHeight} - onValueChange={validateCommentMaxLength} + onValueChange={(value) => { + if (value.length === 0 && isComposerFullSize) { + Report.setIsComposerFullSize(reportID, false); + } + validateCommentMaxLength(value); + }} /> { diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 427c6ccdbfc4..2c9a4cbd21e8 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -31,6 +31,7 @@ import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManag import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import setShouldShowComposeInputKeyboardAware from '@libs/setShouldShowComposeInputKeyboardAware'; +import * as ComposerActions from '@userActions/Composer'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; import * as InputFocus from '@userActions/InputFocus'; import * as Report from '@userActions/Report'; @@ -211,6 +212,9 @@ function ReportActionItemMessageEdit( // eslint-disable-next-line react-hooks/exhaustive-deps -- this cleanup needs to be called only on unmount }, [action.reportActionID]); + // show the composer after editing is complete for devices that hide the composer during editing. + useEffect(() => () => ComposerActions.setShouldShowComposeInput(true), []); + /** * Save the draft of the comment. This debounced so that we're not ceaselessly saving your edit. Saving the draft * allows one to navigate somewhere else and come back to the comment and still have it in edit mode. diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.tsx b/src/pages/iou/steps/MoneyRequestAmountForm.tsx index 5bec1052c790..cb1f73ae2207 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.tsx +++ b/src/pages/iou/steps/MoneyRequestAmountForm.tsx @@ -104,7 +104,7 @@ function MoneyRequestAmountForm( */ const onMouseDown = (event: React.MouseEvent, ids: string[]) => { const relatedTargetId = (event.nativeEvent?.target as HTMLElement)?.id; - if (ids.includes(relatedTargetId)) { + if (!ids.includes(relatedTargetId)) { return; } @@ -127,7 +127,7 @@ function MoneyRequestAmountForm( }, []); useEffect(() => { - if (!currency || typeof amount === 'number') { + if (!currency || typeof amount !== 'number') { return; } initializeAmount(amount); diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index 38b64734f6ea..fa031fb31697 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -11,11 +11,14 @@ import cardPropTypes from '@components/cardPropTypes'; import ConfirmModal from '@components/ConfirmModal'; import CurrentUserPersonalDetailsSkeletonView from '@components/CurrentUserPersonalDetailsSkeletonView'; import HeaderPageLayout from '@components/HeaderPageLayout'; +import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {withNetwork} from '@components/OnyxProvider'; +import {PressableWithFeedback} from '@components/Pressable'; import Text from '@components/Text'; +import Tooltip from '@components/Tooltip'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; @@ -31,6 +34,7 @@ import Navigation from '@libs/Navigation/Navigation'; import * as UserUtils from '@libs/UserUtils'; import walletTermsPropTypes from '@pages/EnablePayments/walletTermsPropTypes'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; +import variables from '@styles/variables'; import * as Link from '@userActions/Link'; import * as PaymentMethods from '@userActions/PaymentMethods'; import * as PersonalDetails from '@userActions/PersonalDetails'; @@ -100,6 +104,7 @@ function InitialSettingsPage(props) { const popoverAnchor = useRef(null); const {translate} = useLocalize(); const activeRoute = useNavigationState(getTopmostSettingsCentralPaneName); + const emojiCode = lodashGet(props, 'currentUserPersonalDetails.status.emojiCode', ''); const [shouldShowSignoutConfirmModal, setShouldShowSignoutConfirmModal] = useState(false); @@ -151,11 +156,6 @@ function InitialSettingsPage(props) { ? 'error' : null, }, - { - translationKey: 'common.shareCode', - icon: Expensicons.QrCode, - routeName: ROUTES.SETTINGS_SHARE_CODE, - }, { translationKey: 'common.preferences', icon: Expensicons.Gear, @@ -332,6 +332,42 @@ function InitialSettingsPage(props) { ) : ( <> + + + Navigation.navigate(ROUTES.SETTINGS_SHARE_CODE)} + > + + + + + + + Navigation.navigate(ROUTES.SETTINGS_STATUS)} + > + + {emojiCode ? ( + {emojiCode} + ) : ( + + )} + + + + ReportUtils.canEditWriteCapability(report, linkedWorkspace), [report, linkedWorkspace]); + const shouldAllowChangeVisibility = useMemo(() => ReportUtils.canEditRoomVisibility(report, linkedWorkspace), [report, linkedWorkspace]); const shouldShowNotificationPref = !isMoneyRequestReport && report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; const roomNameLabel = translate(isMoneyRequestReport ? 'workspace.editor.nameInputLabel' : 'newRoomPage.roomName'); @@ -141,8 +142,17 @@ function ReportSettingsPage({report, policies}: ReportSettingsPageProps) { /> )} - {report?.visibility !== undefined && ( - + + {report?.visibility !== undefined && + (shouldAllowChangeVisibility ? ( + Navigation.navigate(ROUTES.REPORT_SETTINGS_VISIBILITY.getRoute(report.reportID))} + /> + ) : ( + {translate(`newRoomPage.${report.visibility}Description`)} - )} - + ))}
diff --git a/src/pages/settings/Report/VisibilityPage.tsx b/src/pages/settings/Report/VisibilityPage.tsx new file mode 100644 index 000000000000..d3b8b2656d50 --- /dev/null +++ b/src/pages/settings/Report/VisibilityPage.tsx @@ -0,0 +1,98 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useMemo, useState} from 'react'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import ConfirmModal from '@components/ConfirmModal'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import useLocalize from '@hooks/useLocalize'; +import type {ReportSettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as ReportUtils from '@libs/ReportUtils'; +import type {WithReportOrNotFoundProps} from '@pages/home/report/withReportOrNotFound'; +import withReportOrNotFound from '@pages/home/report/withReportOrNotFound'; +import * as ReportActions from '@userActions/Report'; +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; +import type {RoomVisibility} from '@src/types/onyx/Report'; + +type VisibilityProps = WithReportOrNotFoundProps & StackScreenProps; + +function VisibilityPage({report}: VisibilityProps) { + const [showConfirmModal, setShowConfirmModal] = useState(false); + + const shouldDisableVisibility = ReportUtils.isArchivedRoom(report); + const {translate} = useLocalize(); + + const visibilityOptions = useMemo( + () => + Object.values(CONST.REPORT.VISIBILITY) + .filter((visibilityOption) => visibilityOption !== CONST.REPORT.VISIBILITY.PUBLIC_ANNOUNCE) + .map((visibilityOption) => ({ + text: translate(`newRoomPage.visibilityOptions.${visibilityOption}`), + value: visibilityOption, + alternateText: translate(`newRoomPage.${visibilityOption}Description`), + keyForList: visibilityOption, + isSelected: visibilityOption === report?.visibility, + })), + [translate, report?.visibility], + ); + + const changeVisibility = useCallback( + (newVisibility: RoomVisibility) => { + if (!report) { + return; + } + ReportActions.updateRoomVisibility(report.reportID, report.visibility, newVisibility, true, report); + }, + [report], + ); + + const hideModal = useCallback(() => { + setShowConfirmModal(false); + }, []); + + return ( + + + ReportUtils.goBackToDetailsPage(report)} + /> + { + if (option.value === CONST.REPORT.VISIBILITY.PUBLIC) { + setShowConfirmModal(true); + return; + } + changeVisibility(option.value); + }} + initiallyFocusedOptionKey={visibilityOptions.find((visibility) => visibility.isSelected)?.keyForList} + ListItem={RadioListItem} + /> + { + changeVisibility(CONST.REPORT.VISIBILITY.PUBLIC); + hideModal(); + }} + onCancel={hideModal} + title={translate('common.areYouSure')} + prompt={translate('newRoomPage.publicDescription')} + confirmText={translate('common.yes')} + cancelText={translate('common.no')} + danger + /> + + + ); +} + +VisibilityPage.displayName = 'VisibilityPage'; + +export default withReportOrNotFound()(VisibilityPage); diff --git a/src/pages/settings/Security/CloseAccountPage.tsx b/src/pages/settings/Security/CloseAccountPage.tsx index 84934aa01089..bcf65ecfe837 100644 --- a/src/pages/settings/Security/CloseAccountPage.tsx +++ b/src/pages/settings/Security/CloseAccountPage.tsx @@ -67,16 +67,16 @@ function CloseAccountPage({session}: CloseAccountPageProps) { const sanitizePhoneOrEmail = (phoneOrEmail: string): string => phoneOrEmail.replace(/\s+/g, '').toLowerCase(); const validate = (values: FormOnyxValues): FormInputErrors => { - const userEmailOrPhone = formatPhoneNumber(session?.email); + const userEmailOrPhone = session?.email ? formatPhoneNumber(session.email) : null; const errors = ValidationUtils.getFieldRequiredErrors(values, ['phoneOrEmail']); - if (values.phoneOrEmail && sanitizePhoneOrEmail(userEmailOrPhone) !== sanitizePhoneOrEmail(values.phoneOrEmail)) { + if (values.phoneOrEmail && userEmailOrPhone && sanitizePhoneOrEmail(userEmailOrPhone) !== sanitizePhoneOrEmail(values.phoneOrEmail)) { errors.phoneOrEmail = 'closeAccountPage.enterYourDefaultContactMethod'; } return errors; }; - const userEmailOrPhone = formatPhoneNumber(session?.email); + const userEmailOrPhone = session?.email ? formatPhoneNumber(session.email) : null; return ( { - const updatedDraftValues = GetPhysicalCardUtils.getUpdatedDraftValues(draftValues, privatePersonalDetails, loginList); + let updatedDraftValues = draftValues; + + if (!draftValues) { + updatedDraftValues = GetPhysicalCardUtils.getUpdatedDraftValues(null, privatePersonalDetails, loginList); + // Form draft data needs to be initialized with the private personal details + // If no draft data exists + FormActions.setDraftValues(ONYXKEYS.FORMS.GET_PHYSICAL_CARD_FORM, updatedDraftValues); + } - GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(updatedDraftValues), loginList); + GetPhysicalCardUtils.goToNextPhysicalCardRoute(domain, GetPhysicalCardUtils.getUpdatedPrivatePersonalDetails(updatedDraftValues)); }; const hasDetectedDomainFraud = _.some(domainCards, (card) => card.fraud === CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN); diff --git a/src/pages/signin/ChangeExpensifyLoginLink.js b/src/pages/signin/ChangeExpensifyLoginLink.js deleted file mode 100755 index 6ba9b7fcd0f5..000000000000 --- a/src/pages/signin/ChangeExpensifyLoginLink.js +++ /dev/null @@ -1,62 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; -import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /** The credentials of the logged in person */ - credentials: PropTypes.shape({ - /** The email the user logged in with */ - login: PropTypes.string, - }), - - /** Callback to navigate back to email form */ - onPress: PropTypes.func.isRequired, - - ...withLocalizePropTypes, -}; - -const defaultProps = { - credentials: { - login: '', - }, -}; - -function ChangeExpensifyLoginLink(props) { - const styles = useThemeStyles(); - return ( - - {!_.isEmpty(props.credentials.login) && {props.translate('loginForm.notYou', {user: props.formatPhoneNumber(props.credentials.login)})}} - - - {props.translate('common.goBack')} - {'.'} - - - - ); -} - -ChangeExpensifyLoginLink.propTypes = propTypes; -ChangeExpensifyLoginLink.defaultProps = defaultProps; -ChangeExpensifyLoginLink.displayName = 'ChangeExpensifyLoginLink'; - -export default compose( - withLocalize, - withOnyx({ - credentials: {key: ONYXKEYS.CREDENTIALS}, - }), -)(ChangeExpensifyLoginLink); diff --git a/src/pages/signin/ChangeExpensifyLoginLink.tsx b/src/pages/signin/ChangeExpensifyLoginLink.tsx new file mode 100755 index 000000000000..7f6eb05ff663 --- /dev/null +++ b/src/pages/signin/ChangeExpensifyLoginLink.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Credentials} from '@src/types/onyx'; + +type ChangeExpensifyLoginLinkOnyxProps = { + /** The credentials of the person logging in */ + credentials: OnyxEntry; +}; + +type ChangeExpensifyLoginLinkProps = ChangeExpensifyLoginLinkOnyxProps & { + onPress: () => void; +}; + +function ChangeExpensifyLoginLink({credentials, onPress}: ChangeExpensifyLoginLinkProps) { + const styles = useThemeStyles(); + const {translate, formatPhoneNumber} = useLocalize(); + + return ( + + {!!credentials?.login && {translate('loginForm.notYou', {user: formatPhoneNumber(credentials.login)})}} + + {translate('common.goBack')}. + + + ); +} + +ChangeExpensifyLoginLink.displayName = 'ChangeExpensifyLoginLink'; + +export default withOnyx({ + credentials: { + key: ONYXKEYS.CREDENTIALS, + }, +})(ChangeExpensifyLoginLink); diff --git a/src/pages/signin/ChooseSSOOrMagicCode.tsx b/src/pages/signin/ChooseSSOOrMagicCode.tsx index d3140da278e8..7a39df332611 100644 --- a/src/pages/signin/ChooseSSOOrMagicCode.tsx +++ b/src/pages/signin/ChooseSSOOrMagicCode.tsx @@ -81,10 +81,7 @@ function ChooseSSOOrMagicCode({credentials, account, setIsUsingMagicCode}: Choos }} /> {!!account && !isEmptyObject(account.errors) && } - Session.clearSignInData()} - /> + Session.clearSignInData()} />
diff --git a/src/pages/signin/SignInPage.tsx b/src/pages/signin/SignInPage.tsx index a033088f7727..6672ccbd0ebc 100644 --- a/src/pages/signin/SignInPage.tsx +++ b/src/pages/signin/SignInPage.tsx @@ -267,7 +267,6 @@ function SignInPageInner({credentials, account, activeClients = [], preferredLoc /> {shouldShowValidateCodeForm && ( ; - /** Information about the network */ - network: networkPropTypes.isRequired, + /** The credentials of the person logging in */ + credentials: OnyxEntry; - /** Specifies autocomplete hints for the system, so it can provide autofill */ - autoComplete: PropTypes.oneOf(['sms-otp', 'one-time-code']).isRequired, - - /** Determines if user is switched to using recovery code instead of 2fa code */ - isUsingRecoveryCode: PropTypes.bool.isRequired, + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; - /** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */ - setIsUsingRecoveryCode: PropTypes.func.isRequired, +type BaseValidateCodeFormProps = WithToggleVisibilityViewProps & + ValidateCodeFormProps & + BaseValidateCodeFormOnyxProps & { + /** Specifies autocomplete hints for the system, so it can provide autofill */ + autoComplete: 'sms-otp' | 'one-time-code'; + }; - ...withLocalizePropTypes, -}; +type ValidateCodeFormVariant = 'validateCode' | 'twoFactorAuthCode' | 'recoveryCode'; -const defaultProps = { - account: {}, - credentials: {}, - session: { - authToken: null, - }, -}; +type FormError = Partial>; -function BaseValidateCodeForm(props) { - const theme = useTheme(); +function BaseValidateCodeForm({account, credentials, session, autoComplete, isUsingRecoveryCode, setIsUsingRecoveryCode, isVisible}: BaseValidateCodeFormProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); const isFocused = useIsFocused(); - const [formError, setFormError] = useState({}); - const [validateCode, setValidateCode] = useState(props.credentials.validateCode || ''); + const {isOffline} = useNetwork(); + const [formError, setFormError] = useState({}); + const [validateCode, setValidateCode] = useState(credentials?.validateCode ?? ''); const [twoFactorAuthCode, setTwoFactorAuthCode] = useState(''); const [timeRemaining, setTimeRemaining] = useState(30); const [recoveryCode, setRecoveryCode] = useState(''); - const [needToClearError, setNeedToClearError] = useState(props.account.errors); + const [needToClearError, setNeedToClearError] = useState(!!account?.errors); - const prevRequiresTwoFactorAuth = usePrevious(props.account.requiresTwoFactorAuth); - const prevValidateCode = usePrevious(props.credentials.validateCode); + const prevRequiresTwoFactorAuth = usePrevious(account?.requiresTwoFactorAuth); + const prevValidateCode = usePrevious(credentials?.validateCode); - const inputValidateCodeRef = useRef(); - const input2FARef = useRef(); - const timerRef = useRef(); + const inputValidateCodeRef = useRef(); + const input2FARef = useRef(); + const timerRef = useRef(); - const hasError = Boolean(props.account) && !_.isEmpty(props.account.errors) && !needToClearError; - const isLoadingResendValidationForm = props.account.loadingForm === CONST.FORMS.RESEND_VALIDATE_CODE_FORM; - const shouldDisableResendValidateCode = props.network.isOffline || props.account.isLoading; + const hasError = !!account && !isEmptyObject(account?.errors) && !needToClearError; + const isLoadingResendValidationForm = account?.loadingForm === CONST.FORMS.RESEND_VALIDATE_CODE_FORM; + const shouldDisableResendValidateCode = isOffline ?? account?.isLoading; const isValidateCodeFormSubmitting = - props.account.isLoading && props.account.loadingForm === (props.account.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM); + account?.isLoading && account?.loadingForm === (account?.requiresTwoFactorAuth ? CONST.FORMS.VALIDATE_TFA_CODE_FORM : CONST.FORMS.VALIDATE_CODE_FORM); useEffect(() => { - if (!(inputValidateCodeRef.current && hasError && (props.session.autoAuthState === CONST.AUTO_AUTH_STATE.FAILED || props.account.isLoading))) { + if (!(inputValidateCodeRef.current && hasError && (session?.autoAuthState === CONST.AUTO_AUTH_STATE.FAILED || account?.isLoading))) { return; } inputValidateCodeRef.current.blur(); - }, [props.account.isLoading, props.session.autoAuthState, hasError]); + }, [account?.isLoading, session?.autoAuthState, hasError]); useEffect(() => { - if (!inputValidateCodeRef.current || !canFocusInputOnScreenFocus() || !props.isVisible || !isFocused) { + if (!inputValidateCodeRef.current || !canFocusInputOnScreenFocus() || !isVisible || !isFocused) { return; } inputValidateCodeRef.current.focus(); - }, [props.isVisible, isFocused]); + }, [isVisible, isFocused]); useEffect(() => { - if (prevValidateCode || !props.credentials.validateCode) { + if (!!prevValidateCode || !credentials?.validateCode) { return; } - setValidateCode(props.credentials.validateCode); - }, [props.credentials.validateCode, prevValidateCode]); + setValidateCode(credentials.validateCode); + }, [credentials?.validateCode, prevValidateCode]); useEffect(() => { - if (!input2FARef.current || prevRequiresTwoFactorAuth || !props.account.requiresTwoFactorAuth) { + if (!input2FARef.current || !!prevRequiresTwoFactorAuth || !account?.requiresTwoFactorAuth) { return; } input2FARef.current.focus(); - }, [props.account.requiresTwoFactorAuth, prevRequiresTwoFactorAuth]); + }, [account?.requiresTwoFactorAuth, prevRequiresTwoFactorAuth]); useEffect(() => { if (!inputValidateCodeRef.current || validateCode.length > 0) { @@ -163,27 +134,22 @@ function BaseValidateCodeForm(props) { /** * Handle text input and clear formError upon text change - * - * @param {String} text - * @param {String} key */ - const onTextInput = (text, key) => { - let setInput; + const onTextInput = (text: string, key: ValidateCodeFormVariant) => { if (key === 'validateCode') { - setInput = setValidateCode; + setValidateCode(text); } if (key === 'twoFactorAuthCode') { - setInput = setTwoFactorAuthCode; + setTwoFactorAuthCode(text); } if (key === 'recoveryCode') { - setInput = setRecoveryCode; + setRecoveryCode(text); } - setInput(text); - setFormError((prevError) => ({...prevError, [key]: ''})); + setFormError((prevError) => ({...prevError, [key]: undefined})); - if (props.account.errors) { - Session.clearAccountMessages(); + if (account?.errors) { + SessionActions.clearAccountMessages(); } }; @@ -191,8 +157,8 @@ function BaseValidateCodeForm(props) { * Trigger the reset validate code flow and ensure the 2FA input field is reset to avoid it being permanently hidden */ const resendValidateCode = () => { - User.resendValidateCode(props.credentials.login); - inputValidateCodeRef.current.clear(); + User.resendValidateCode(credentials?.login ?? ''); + inputValidateCodeRef.current?.clear(); // Give feedback to the user to let them know the email was sent so that they don't spam the button. setTimeRemaining(30); }; @@ -204,7 +170,7 @@ function BaseValidateCodeForm(props) { setTwoFactorAuthCode(''); setFormError({}); setValidateCode(''); - props.setIsUsingRecoveryCode(false); + setIsUsingRecoveryCode(false); setRecoveryCode(''); }; @@ -213,7 +179,7 @@ function BaseValidateCodeForm(props) { */ const clearSignInData = () => { clearLocalSignInData(); - Session.clearSignInData(); + SessionActions.clearSignInData(); }; useEffect(() => { @@ -221,26 +187,26 @@ function BaseValidateCodeForm(props) { return; } - if (props.account.errors) { - Session.clearAccountMessages(); + if (account?.errors) { + SessionActions.clearAccountMessages(); return; } setNeedToClearError(false); - }, [props.account.errors, needToClearError]); + }, [account?.errors, needToClearError]); /** * Switches between 2fa and recovery code, clears inputs and errors */ const switchBetween2faAndRecoveryCode = () => { - props.setIsUsingRecoveryCode(!props.isUsingRecoveryCode); + setIsUsingRecoveryCode(!isUsingRecoveryCode); setRecoveryCode(''); setTwoFactorAuthCode(''); - setFormError((prevError) => ({...prevError, recoveryCode: '', twoFactorAuthCode: ''})); + setFormError((prevError) => ({...prevError, recoveryCode: undefined, twoFactorAuthCode: undefined})); - if (props.account.errors) { - Session.clearAccountMessages(); + if (account?.errors) { + SessionActions.clearAccountMessages(); } }; @@ -258,10 +224,10 @@ function BaseValidateCodeForm(props) { * Check that all the form fields are valid, then trigger the submit callback */ const validateAndSubmitForm = useCallback(() => { - if (props.account.isLoading) { + if (account?.isLoading) { return; } - const requiresTwoFactorAuth = props.account.requiresTwoFactorAuth; + const requiresTwoFactorAuth = account?.requiresTwoFactorAuth; if (requiresTwoFactorAuth) { if (input2FARef.current) { input2FARef.current.blur(); @@ -269,7 +235,7 @@ function BaseValidateCodeForm(props) { /** * User could be using either recovery code or 2fa code */ - if (!props.isUsingRecoveryCode) { + if (!isUsingRecoveryCode) { if (!twoFactorAuthCode.trim()) { setFormError({twoFactorAuthCode: 'validateCodeForm.error.pleaseFillTwoFactorAuth'}); return; @@ -303,30 +269,30 @@ function BaseValidateCodeForm(props) { } setFormError({}); - const recoveryCodeOr2faCode = props.isUsingRecoveryCode ? recoveryCode : twoFactorAuthCode; + const recoveryCodeOr2faCode = isUsingRecoveryCode ? recoveryCode : twoFactorAuthCode; - const accountID = lodashGet(props.credentials, 'accountID'); + const accountID = credentials?.accountID; if (accountID) { - Session.signInWithValidateCode(accountID, validateCode, recoveryCodeOr2faCode); + SessionActions.signInWithValidateCode(accountID, validateCode, recoveryCodeOr2faCode); } else { - Session.signIn(validateCode, recoveryCodeOr2faCode); + SessionActions.signIn(validateCode, recoveryCodeOr2faCode); } - }, [props.account, props.credentials, twoFactorAuthCode, validateCode, props.isUsingRecoveryCode, recoveryCode]); + }, [account, credentials, twoFactorAuthCode, validateCode, isUsingRecoveryCode, recoveryCode]); return ( <> {/* At this point, if we know the account requires 2FA we already successfully authenticated */} - {props.account.requiresTwoFactorAuth ? ( + {account?.requiresTwoFactorAuth ? ( - {props.isUsingRecoveryCode ? ( + {isUsingRecoveryCode ? ( onTextInput(text, 'recoveryCode')} maxLength={CONST.RECOVERY_CODE_LENGTH} - label={props.translate('recoveryCodeForm.recoveryCode')} - errorText={formError.recoveryCode || ''} + label={translate('recoveryCodeForm.recoveryCode')} + errorText={formError?.recoveryCode ?? ''} hasError={hasError} onSubmitEditing={validateAndSubmitForm} autoFocus @@ -334,70 +300,76 @@ function BaseValidateCodeForm(props) { ) : ( { + if (!magicCodeInput) { + return; + } + input2FARef.current = magicCodeInput; + }} name="twoFactorAuthCode" value={twoFactorAuthCode} onChangeText={(text) => onTextInput(text, 'twoFactorAuthCode')} onFulfill={validateAndSubmitForm} maxLength={CONST.TFA_CODE_LENGTH} - errorText={formError.twoFactorAuthCode || ''} + errorText={formError?.twoFactorAuthCode ?? ''} hasError={hasError} autoFocus key="twoFactorAuthCode" /> )} - {hasError && } + {hasError && } - {props.isUsingRecoveryCode ? props.translate('recoveryCodeForm.use2fa') : props.translate('recoveryCodeForm.useRecoveryCode')} + {isUsingRecoveryCode ? translate('recoveryCodeForm.use2fa') : translate('recoveryCodeForm.useRecoveryCode')} ) : ( { + if (!magicCodeInput) { + return; + } + inputValidateCodeRef.current = magicCodeInput; + }} name="validateCode" value={validateCode} onChangeText={(text) => onTextInput(text, 'validateCode')} onFulfill={validateAndSubmitForm} - errorText={formError.validateCode || ''} + errorText={formError?.validateCode ?? ''} hasError={hasError} autoFocus key="validateCode" testID="validateCode" /> - {hasError && } + {hasError && } - {timeRemaining > 0 && !props.network.isOffline ? ( + {timeRemaining > 0 && !isOffline ? ( - {props.translate('validateCodeForm.requestNewCode')} + {translate('validateCodeForm.requestNewCode')} 00:{String(timeRemaining).padStart(2, '0')} ) : ( - {hasError ? props.translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : props.translate('validateCodeForm.magicCodeNotReceived')} + {hasError ? translate('validateCodeForm.requestNewCodeAfterErrorOccurred') : translate('validateCodeForm.magicCodeNotReceived')} )} @@ -406,10 +378,10 @@ function BaseValidateCodeForm(props) { )} @@ -422,17 +394,12 @@ function BaseValidateCodeForm(props) { ); } -BaseValidateCodeForm.propTypes = propTypes; -BaseValidateCodeForm.defaultProps = defaultProps; BaseValidateCodeForm.displayName = 'BaseValidateCodeForm'; -export default compose( - withLocalize, - withOnyx({ +export default withToggleVisibilityView( + withOnyx({ account: {key: ONYXKEYS.ACCOUNT}, credentials: {key: ONYXKEYS.CREDENTIALS}, session: {key: ONYXKEYS.SESSION}, - }), - withNetwork(), - withToggleVisibilityView, -)(BaseValidateCodeForm); + })(BaseValidateCodeForm), +); diff --git a/src/pages/signin/ValidateCodeForm/index.android.js b/src/pages/signin/ValidateCodeForm/index.android.js deleted file mode 100644 index 9adddf7c92d8..000000000000 --- a/src/pages/signin/ValidateCodeForm/index.android.js +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import BaseValidateCodeForm from './BaseValidateCodeForm'; - -const defaultProps = {}; - -const propTypes = { - /** Determines if user is switched to using recovery code instead of 2fa code */ - isUsingRecoveryCode: PropTypes.bool.isRequired, - - /** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */ - setIsUsingRecoveryCode: PropTypes.func.isRequired, -}; -function ValidateCodeForm(props) { - return ( - - ); -} - -ValidateCodeForm.displayName = 'ValidateCodeForm'; -ValidateCodeForm.propTypes = propTypes; -ValidateCodeForm.defaultProps = defaultProps; - -export default ValidateCodeForm; diff --git a/src/pages/signin/ValidateCodeForm/index.android.tsx b/src/pages/signin/ValidateCodeForm/index.android.tsx new file mode 100644 index 000000000000..1edd17517539 --- /dev/null +++ b/src/pages/signin/ValidateCodeForm/index.android.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import BaseValidateCodeForm from './BaseValidateCodeForm'; +import type ValidateCodeFormProps from './types'; + +function ValidateCodeForm(props: ValidateCodeFormProps) { + return ( + + ); +} + +ValidateCodeForm.displayName = 'ValidateCodeForm'; + +export default ValidateCodeForm; diff --git a/src/pages/signin/ValidateCodeForm/index.js b/src/pages/signin/ValidateCodeForm/index.js deleted file mode 100644 index 35afc283972b..000000000000 --- a/src/pages/signin/ValidateCodeForm/index.js +++ /dev/null @@ -1,28 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import BaseValidateCodeForm from './BaseValidateCodeForm'; - -const defaultProps = {}; - -const propTypes = { - /** Determines if user is switched to using recovery code instead of 2fa code */ - isUsingRecoveryCode: PropTypes.bool.isRequired, - - /** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */ - setIsUsingRecoveryCode: PropTypes.func.isRequired, -}; -function ValidateCodeForm(props) { - return ( - - ); -} - -ValidateCodeForm.displayName = 'ValidateCodeForm'; -ValidateCodeForm.propTypes = propTypes; -ValidateCodeForm.defaultProps = defaultProps; - -export default ValidateCodeForm; diff --git a/src/pages/signin/ValidateCodeForm/index.tsx b/src/pages/signin/ValidateCodeForm/index.tsx new file mode 100644 index 000000000000..8c1528ae7409 --- /dev/null +++ b/src/pages/signin/ValidateCodeForm/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import BaseValidateCodeForm from './BaseValidateCodeForm'; +import type ValidateCodeFormProps from './types'; + +function ValidateCodeForm(props: ValidateCodeFormProps) { + return ( + + ); +} + +ValidateCodeForm.displayName = 'ValidateCodeForm'; + +export default ValidateCodeForm; diff --git a/src/pages/signin/ValidateCodeForm/types.ts b/src/pages/signin/ValidateCodeForm/types.ts new file mode 100644 index 000000000000..6edb6eace231 --- /dev/null +++ b/src/pages/signin/ValidateCodeForm/types.ts @@ -0,0 +1,11 @@ +type ValidateCodeFormProps = { + /** Determines if user is switched to using recovery code instead of 2fa code */ + isUsingRecoveryCode: boolean; + + /** Function to change `isUsingRecoveryCode` state when user toggles between 2fa code and recovery code */ + setIsUsingRecoveryCode: (value: boolean) => void; + + isVisible: boolean; +}; + +export default ValidateCodeFormProps; diff --git a/src/styles/index.ts b/src/styles/index.ts index 34123877d478..13b2015d2c9c 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3067,6 +3067,20 @@ const styles = (theme: ThemeColors) => bottom: -8, }, + primaryMediumIcon: { + alignItems: 'center', + backgroundColor: theme.buttonDefaultBG, + borderRadius: 20, + color: theme.textReversed, + height: 40, + width: 40, + justifyContent: 'center', + }, + + primaryMediumText: { + fontSize: variables.iconSizeNormal, + }, + workspaceOwnerAvatarWrapper: { margin: 6, }, diff --git a/src/styles/utils/autoCompleteSuggestion/index.android.ts b/src/styles/utils/autoCompleteSuggestion/index.android.ts new file mode 100644 index 000000000000..88b7a7c84297 --- /dev/null +++ b/src/styles/utils/autoCompleteSuggestion/index.android.ts @@ -0,0 +1,5 @@ +import type ShouldPreventScrollOnAutoCompleteSuggestion from './types'; + +const shouldPreventScrollOnAutoCompleteSuggestion: ShouldPreventScrollOnAutoCompleteSuggestion = () => false; + +export default shouldPreventScrollOnAutoCompleteSuggestion; diff --git a/src/styles/utils/autoCompleteSuggestion/index.ts b/src/styles/utils/autoCompleteSuggestion/index.ts new file mode 100644 index 000000000000..e756e7178c57 --- /dev/null +++ b/src/styles/utils/autoCompleteSuggestion/index.ts @@ -0,0 +1,5 @@ +import type ShouldPreventScrollOnAutoCompleteSuggestion from './types'; + +const shouldPreventScrollOnAutoCompleteSuggestion: ShouldPreventScrollOnAutoCompleteSuggestion = () => true; + +export default shouldPreventScrollOnAutoCompleteSuggestion; diff --git a/src/styles/utils/autoCompleteSuggestion/index.website.ts b/src/styles/utils/autoCompleteSuggestion/index.website.ts new file mode 100644 index 000000000000..badec5dfc774 --- /dev/null +++ b/src/styles/utils/autoCompleteSuggestion/index.website.ts @@ -0,0 +1,8 @@ +import * as Browser from '@libs/Browser'; +import type ShouldPreventScrollOnAutoCompleteSuggestion from './types'; + +const isMobileSafari = Browser.isMobileSafari(); + +const shouldPreventScrollOnAutoCompleteSuggestion: ShouldPreventScrollOnAutoCompleteSuggestion = () => !isMobileSafari; + +export default shouldPreventScrollOnAutoCompleteSuggestion; diff --git a/src/styles/utils/autoCompleteSuggestion/types.ts b/src/styles/utils/autoCompleteSuggestion/types.ts new file mode 100644 index 000000000000..563d305eb236 --- /dev/null +++ b/src/styles/utils/autoCompleteSuggestion/types.ts @@ -0,0 +1,3 @@ +type ShouldPreventScrollOnAutoCompleteSuggestion = () => boolean; + +export default ShouldPreventScrollOnAutoCompleteSuggestion; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index d3b2c0e9d0f5..833907549133 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -14,6 +14,7 @@ import CONST from '@src/CONST'; import type {Transaction} from '@src/types/onyx'; import {defaultStyles} from '..'; import type {ThemeStyles} from '..'; +import shouldPreventScrollOnAutoCompleteSuggestion from './autoCompleteSuggestion'; import getCardStyles from './cardStyles'; import containerComposeStyles from './containerComposeStyles'; import FontUtils from './FontUtils'; @@ -790,6 +791,8 @@ function getBaseAutoCompleteSuggestionContainerStyle({left, bottom, width}: GetB }; } +const shouldPreventScroll = shouldPreventScrollOnAutoCompleteSuggestion(); + /** * Gets the correct position for auto complete suggestion container */ @@ -797,13 +800,13 @@ function getAutoCompleteSuggestionContainerStyle(itemsHeight: number): ViewStyle 'worklet'; const borderWidth = 2; - const height = itemsHeight + 2 * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING; + const height = itemsHeight + 2 * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING + (shouldPreventScroll ? borderWidth : 0); // The suggester is positioned absolutely within the component that includes the input and RecipientLocalTime view (for non-expanded mode only). To position it correctly, // we need to shift it by the suggester's height plus its padding and, if applicable, the height of the RecipientLocalTime view. return { overflow: 'hidden', - top: -(height + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + borderWidth), + top: -(height + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + (shouldPreventScroll ? 0 : borderWidth)), height, minHeight: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, }; @@ -1224,7 +1227,7 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ /** * Returns link styles based on whether the link is disabled or not */ - getDisabledLinkStyles: (isDisabled = false): ViewStyle => { + getDisabledLinkStyles: (isDisabled = false): TextStyle => { const disabledLinkStyles = { color: theme.textSupporting, ...styles.cursorDisabled, diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index fbd61a9c5365..f5c4606fd335 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -8,6 +8,8 @@ type NotificationPreference = ValueOf; +type RoomVisibility = ValueOf; + type Note = { note: string; errors?: OnyxCommon.Errors; @@ -110,7 +112,7 @@ type Report = { openOnAdminRoom?: boolean; /** The report visibility */ - visibility?: ValueOf; + visibility?: RoomVisibility; /** Report cached total */ cachedTotal?: string; @@ -178,4 +180,4 @@ type Report = { export default Report; -export type {NotificationPreference, WriteCapability, Note}; +export type {NotificationPreference, RoomVisibility, WriteCapability, Note}; diff --git a/tests/actions/IOUTest.js b/tests/actions/IOUTest.js index cb31afbf8f8f..c43837fbfd34 100644 --- a/tests/actions/IOUTest.js +++ b/tests/actions/IOUTest.js @@ -1742,7 +1742,7 @@ describe('actions/IOU', () => { }), ]), originalMessage: expect.objectContaining({ - amount: -amount, + amount, paymentType: CONST.IOU.PAYMENT_TYPE.VBBA, type: 'pay', }), diff --git a/tests/e2e/compare/math.js b/tests/e2e/compare/math.ts similarity index 83% rename from tests/e2e/compare/math.js rename to tests/e2e/compare/math.ts index a87c58c4dff3..59a56dd3c842 100644 --- a/tests/e2e/compare/math.js +++ b/tests/e2e/compare/math.ts @@ -7,13 +7,8 @@ * * Based on :: https://github.com/v8/v8/blob/master/test/benchmarks/csuite/compare-baseline.py * - * @param {Number} baselineMean - * @param {Number} baselineStdev - * @param {Number} currentMean - * @param {Number} runs - * @returns {Number} */ -const computeZ = (baselineMean, baselineStdev, currentMean, runs) => { +const computeZ = (baselineMean: number, baselineStdev: number, currentMean: number, runs: number): number => { if (baselineStdev === 0) { return 1000; } @@ -26,10 +21,8 @@ const computeZ = (baselineMean, baselineStdev, currentMean, runs) => { * * Based on :: https://github.com/v8/v8/blob/master/test/benchmarks/csuite/compare-baseline.py * - * @param {Number} z - * @returns {Number} */ -const computeProbability = (z) => { +const computeProbability = (z: number): number => { // p 0.005: two sided < 0.01 if (z > 2.575829) { return 0; diff --git a/tests/e2e/measure/math.js b/tests/e2e/measure/math.ts similarity index 64% rename from tests/e2e/measure/math.js rename to tests/e2e/measure/math.ts index 14f75a7f980e..e1c0cb981a0c 100644 --- a/tests/e2e/measure/math.js +++ b/tests/e2e/measure/math.ts @@ -1,6 +1,13 @@ -import _ from 'underscore'; +type Entries = number[]; -const filterOutliersViaIQR = (data) => { +type Stats = { + mean: number; + stdev: number; + runs: number; + entries: Entries; +}; + +const filterOutliersViaIQR = (data: Entries): Entries => { let q1; let q3; @@ -18,22 +25,17 @@ const filterOutliersViaIQR = (data) => { const maxValue = q3 + iqr * 1.5; const minValue = q1 - iqr * 1.5; - return _.filter(values, (x) => x >= minValue && x <= maxValue); + return values.filter((x) => x >= minValue && x <= maxValue); }; -const mean = (arr) => _.reduce(arr, (a, b) => a + b, 0) / arr.length; +const mean = (arr: Entries): number => arr.reduce((a, b) => a + b, 0) / arr.length; -const std = (arr) => { +const std = (arr: Entries): number => { const avg = mean(arr); - return Math.sqrt( - _.reduce( - _.map(arr, (i) => (i - avg) ** 2), - (a, b) => a + b, - ) / arr.length, - ); + return Math.sqrt(arr.map((i) => (i - avg) ** 2).reduce((a, b) => a + b) / arr.length); }; -const getStats = (entries) => { +const getStats = (entries: Entries): Stats => { const cleanedEntries = filterOutliersViaIQR(entries); const meanDuration = mean(cleanedEntries); const stdevDuration = std(cleanedEntries); @@ -46,5 +48,4 @@ const getStats = (entries) => { }; }; -// eslint-disable-next-line import/prefer-default-export export default getStats; diff --git a/tests/perf-test/ReportActionsList.perf-test.js b/tests/perf-test/ReportActionsList.perf-test.js index 34b127c217e4..c760b81b2373 100644 --- a/tests/perf-test/ReportActionsList.perf-test.js +++ b/tests/perf-test/ReportActionsList.perf-test.js @@ -43,6 +43,8 @@ jest.mock('@react-navigation/native', () => { }; }); +jest.mock('../../src/components/ConfirmedRoute.tsx'); + beforeAll(() => Onyx.init({ keys: ONYXKEYS, diff --git a/tests/perf-test/ReportScreen.perf-test.js b/tests/perf-test/ReportScreen.perf-test.js index e86d0bf4fa09..bc127ff8a1f1 100644 --- a/tests/perf-test/ReportScreen.perf-test.js +++ b/tests/perf-test/ReportScreen.perf-test.js @@ -31,6 +31,8 @@ jest.mock('react-native-reanimated', () => ({ useAnimatedRef: jest.fn, })); +jest.mock('../../src/components/ConfirmedRoute.tsx'); + jest.mock('../../src/components/withNavigationFocus', () => (Component) => { function WithNavigationFocus(props) { return ( diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js index 6a57218fab23..6051f04f570e 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.js @@ -30,6 +30,7 @@ jest.setTimeout(30000); jest.mock('../../src/libs/Notification/LocalNotification'); jest.mock('../../src/components/Icon/Expensicons'); +jest.mock('../../src/components/ConfirmedRoute.tsx'); // Needed for: https://stackoverflow.com/questions/76903168/mocking-libraries-in-jest jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ @@ -228,6 +229,7 @@ function signInAndGetAppWithUnreadChat() { lastVisibleActionCreated: reportAction9CreatedDate, lastMessageText: 'Test', participantAccountIDs: [USER_B_ACCOUNT_ID], + lastActorAccountID: USER_B_ACCOUNT_ID, type: CONST.REPORT.TYPE.CHAT, }); const createdReportActionID = NumberUtils.rand64(); @@ -387,6 +389,7 @@ describe('Unread Indicators', () => { lastReadTime: '', lastVisibleActionCreated: DateUtils.getDBTime(utcToZonedTime(NEW_REPORT_FIST_MESSAGE_CREATED_DATE, 'UTC').valueOf()), lastMessageText: 'Comment 1', + lastActorAccountID: USER_C_ACCOUNT_ID, participantAccountIDs: [USER_C_ACCOUNT_ID], type: CONST.REPORT.TYPE.CHAT, },