diff --git a/android/app/build.gradle b/android/app/build.gradle
index fc376ad08862..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 1001044309
- versionName "1.4.43-9"
+ versionCode 1001044311
+ versionName "1.4.43-11"
}
flavorDimensions "default"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 93faff6ab427..1a2581512eda 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.43.9
+ 1.4.43.11
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 85d5f45e4184..7b789718fd70 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.43.9
+ 1.4.43.11
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 6b0cc0c08d14..ad4e309ee295 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -13,7 +13,7 @@
CFBundleShortVersionString
1.4.43
CFBundleVersion
- 1.4.43.9
+ 1.4.43.11
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index c114de61408f..cec28a395431 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.43-9",
+ "version": "1.4.43-11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.43-9",
+ "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 66d60bcd87cd..379612854781 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.43-9",
+ "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/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/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/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 3af123a74910..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],
);
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/ReportUtils.ts b/src/libs/ReportUtils.ts
index d9e7fb8e7e6b..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
@@ -4711,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:
@@ -4725,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 ?? '');
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/actions/IOU.ts b/src/libs/actions/IOU.ts
index 39ce9dd6d2bb..f39728e7d31c 100644
--- a/src/libs/actions/IOU.ts
+++ b/src/libs/actions/IOU.ts
@@ -1010,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;
}
@@ -1134,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({
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 6efe0860e9b5..1276207e37c3 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -86,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;
}
@@ -168,7 +170,6 @@ Onyx.connect({
});
const allReports: OnyxCollection = {};
-let conciergeChatReportID: string | undefined;
const typingWatchTimers: Record = {};
let reportIDDeeplinkedFromOldDot: string | undefined;
@@ -1716,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));
}
}
@@ -2213,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;
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/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/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/settings/Report/VisibilityPage.tsx b/src/pages/settings/Report/VisibilityPage.tsx
index a03068832637..d3b8b2656d50 100644
--- a/src/pages/settings/Report/VisibilityPage.tsx
+++ b/src/pages/settings/Report/VisibilityPage.tsx
@@ -5,6 +5,7 @@ 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';
@@ -72,6 +73,7 @@ function VisibilityPage({report}: VisibilityProps) {
changeVisibility(option.value);
}}
initiallyFocusedOptionKey={visibilityOptions.find((visibility) => visibility.isSelected)?.keyForList}
+ ListItem={RadioListItem}
/>
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 (
- {!_.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/utils/index.ts b/src/styles/utils/index.ts
index 69e74bb54e63..833907549133 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -1227,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/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/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.js
index 07100d7a5f0f..6051f04f570e 100644
--- a/tests/ui/UnreadIndicatorsTest.js
+++ b/tests/ui/UnreadIndicatorsTest.js
@@ -229,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();
@@ -388,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,
},