diff --git a/android/app/build.gradle b/android/app/build.gradle
index 49f1b017d5e0..759abc31c058 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 1001043102
- versionName "1.4.31-2"
+ versionCode 1001043104
+ versionName "1.4.31-4"
}
flavorDimensions "default"
diff --git a/assets/animations/Update.lottie b/assets/animations/Update.lottie
deleted file mode 100644
index 363486ec2267..000000000000
Binary files a/assets/animations/Update.lottie and /dev/null differ
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 34341662d137..31e13ef3d283 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.31.2
+ 1.4.31.4
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 38073f64d814..557832679cd6 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.4.31.2
+ 1.4.31.4
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index 8550e23db7b1..6d1c222e3ab9 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -5,7 +5,7 @@
CFBundleShortVersionString
1.4.31
CFBundleVersion
- 1.4.31.2
+ 1.4.31.4
NSExtension
NSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index f05837e853ba..be3f8f18bff3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.31-2",
+ "version": "1.4.31-4",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.31-2",
+ "version": "1.4.31-4",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index 2ba358b438e6..255261713c0a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.31-2",
+ "version": "1.4.31-4",
"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.",
diff --git a/src/CONST.ts b/src/CONST.ts
index 5fee60e57617..fdd18f714ca2 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -788,7 +788,6 @@ const CONST = {
EXP_ERROR: 666,
MANY_WRITES_ERROR: 665,
UNABLE_TO_RETRY: 'unableToRetry',
- UPDATE_REQUIRED: 426,
},
HTTP_STATUS: {
// When Cloudflare throttles
@@ -819,9 +818,6 @@ const CONST = {
GATEWAY_TIMEOUT: 'Gateway Timeout',
EXPENSIFY_SERVICE_INTERRUPTED: 'Expensify service interrupted',
DUPLICATE_RECORD: 'A record already exists with this ID',
-
- // The "Upgrade" is intentional as the 426 HTTP code means "Upgrade Required" and sent by the API. We use the "Update" language everywhere else in the front end when this gets returned.
- UPDATE_REQUIRED: 'Upgrade Required',
},
ERROR_TYPE: {
SOCKET: 'Expensify\\Auth\\Error\\Socket',
@@ -979,6 +975,7 @@ const CONST = {
SMALL_EMOJI_PICKER_SIZE: {
WIDTH: '100%',
},
+ MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM: 83,
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 300,
NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT_WEB: 200,
EMOJI_PICKER_ITEM_HEIGHT: 32,
@@ -1305,6 +1302,14 @@ const CONST = {
LAST_BUSINESS_DAY_OF_MONTH: 'lastBusinessDayOfMonth',
LAST_DAY_OF_MONTH: 'lastDayOfMonth',
},
+ APPROVAL_MODE: {
+ OPTIONAL: 'OPTIONAL',
+ BASIC: 'BASIC',
+ ADVANCED: 'ADVANCED',
+ DYNAMICEXTERNAL: 'DYNAMIC_EXTERNAL',
+ SMARTREPORT: 'SMARTREPORT',
+ BILLCOM: 'BILLCOM',
+ },
ROOM_PREFIX: '#',
CUSTOM_UNIT_RATE_BASE_OFFSET: 100,
OWNER_EMAIL_FAKE: '_FAKE_',
diff --git a/src/Expensify.js b/src/Expensify.js
index 12003968b284..0707ba069241 100644
--- a/src/Expensify.js
+++ b/src/Expensify.js
@@ -13,7 +13,6 @@ import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper';
import SplashScreenHider from './components/SplashScreenHider';
import UpdateAppModal from './components/UpdateAppModal';
import withLocalize, {withLocalizePropTypes} from './components/withLocalize';
-import CONST from './CONST';
import * as EmojiPickerAction from './libs/actions/EmojiPickerAction';
import * as Report from './libs/actions/Report';
import * as User from './libs/actions/User';
@@ -77,9 +76,6 @@ const propTypes = {
/** Whether the app is waiting for the server's response to determine if a room is public */
isCheckingPublicRoom: PropTypes.bool,
- /** True when the user must update to the latest minimum version of the app */
- updateRequired: PropTypes.bool,
-
/** Whether we should display the notification alerting the user that focus mode has been auto-enabled */
focusModeNotification: PropTypes.bool,
@@ -95,7 +91,6 @@ const defaultProps = {
isSidebarLoaded: false,
screenShareRequest: null,
isCheckingPublicRoom: true,
- updateRequired: false,
focusModeNotification: false,
};
@@ -209,10 +204,6 @@ function Expensify(props) {
return null;
}
- if (props.updateRequired) {
- throw new Error(CONST.ERROR.UPDATE_REQUIRED);
- }
-
return (
{/* We include the modal for showing a new update at the top level so the option is always present. */}
- {/* If the update is required we won't show this option since a full screen update view will be displayed instead. */}
- {props.updateAvailable && !props.updateRequired ? : null}
+ {props.updateAvailable ? : null}
{props.screenShareRequest ? (
element
MAX_CANVAS_WIDTH: 'maxCanvasWidth',
- /** Indicates whether an forced upgrade is required */
- UPDATE_REQUIRED: 'updateRequired',
-
/** Collection Keys */
COLLECTION: {
DOWNLOAD: 'download_',
@@ -445,7 +442,6 @@ type OnyxValues = {
[ONYXKEYS.MAX_CANVAS_AREA]: number;
[ONYXKEYS.MAX_CANVAS_HEIGHT]: number;
[ONYXKEYS.MAX_CANVAS_WIDTH]: number;
- [ONYXKEYS.UPDATE_REQUIRED]: boolean;
// Collections
[ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download;
diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.tsx
similarity index 83%
rename from src/components/AvatarCropModal/AvatarCropModal.js
rename to src/components/AvatarCropModal/AvatarCropModal.tsx
index 2da3cf88c78c..3ac2e3e3d729 100644
--- a/src/components/AvatarCropModal/AvatarCropModal.js
+++ b/src/components/AvatarCropModal/AvatarCropModal.tsx
@@ -1,79 +1,71 @@
-import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useState} from 'react';
import {ActivityIndicator, Image, View} from 'react-native';
+import type {LayoutChangeEvent} from 'react-native';
import {Gesture, GestureHandlerRootView} from 'react-native-gesture-handler';
+import type {GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler';
import {interpolate, runOnUI, useSharedValue, useWorkletCallback} from 'react-native-reanimated';
import Button from '@components/Button';
import HeaderGap from '@components/HeaderGap';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
-import sourcePropTypes from '@components/Image/sourcePropTypes';
import Modal from '@components/Modal';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
+import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import cropOrRotateImage from '@libs/cropOrRotateImage';
+import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
import CONST from '@src/CONST';
+import type IconAsset from '@src/types/utils/IconAsset';
import ImageCropView from './ImageCropView';
import Slider from './Slider';
-const propTypes = {
+type AvatarCropModalProps = {
/** Link to image for cropping */
- imageUri: PropTypes.string,
+ imageUri?: string;
/** Name of the image */
- imageName: PropTypes.string,
+ imageName?: string;
/** Type of the image file */
- imageType: PropTypes.string,
+ imageType?: string;
/** Callback to be called when user closes the modal */
- onClose: PropTypes.func,
+ onClose?: () => void;
/** Callback to be called when user saves the image */
- onSave: PropTypes.func,
+ onSave?: (newImage: File | CustomRNImageManipulatorResult) => void;
/** Modal visibility */
- isVisible: PropTypes.bool.isRequired,
+ isVisible: boolean;
/** Image crop vector mask */
- maskImage: sourcePropTypes,
-
- ...withLocalizePropTypes,
- ...windowDimensionsPropTypes,
-};
-
-const defaultProps = {
- imageUri: '',
- imageName: '',
- imageType: '',
- onClose: () => {},
- onSave: () => {},
- maskImage: undefined,
+ maskImage?: IconAsset;
};
// This component can't be written using class since reanimated API uses hooks.
-function AvatarCropModal(props) {
+function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose, onSave, isVisible, maskImage}: AvatarCropModalProps) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
- const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
+ const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
+ const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
const translateY = useSharedValue(0);
const translateX = useSharedValue(0);
- const scale = useSharedValue(CONST.AVATAR_CROP_MODAL.MIN_SCALE);
+ const scale = useSharedValue(CONST.AVATAR_CROP_MODAL.MIN_SCALE);
const rotation = useSharedValue(0);
const translateSlider = useSharedValue(0);
const isPressableEnabled = useSharedValue(true);
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
+
// Check if image cropping, saving or uploading is in progress
const isLoading = useSharedValue(false);
@@ -82,13 +74,13 @@ function AvatarCropModal(props) {
const prevMaxOffsetX = useSharedValue(0);
const prevMaxOffsetY = useSharedValue(0);
- const [imageContainerSize, setImageContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
- const [sliderContainerSize, setSliderContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
+ const [imageContainerSize, setImageContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
+ const [sliderContainerSize, setSliderContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE);
const [isImageContainerInitialized, setIsImageContainerInitialized] = useState(false);
const [isImageInitialized, setIsImageInitialized] = useState(false);
// An onLayout callback, that initializes the image container, for proper render of an image
- const initializeImageContainer = useCallback((event) => {
+ const initializeImageContainer = useCallback((event: LayoutChangeEvent) => {
setIsImageContainerInitialized(true);
const {height, width} = event.nativeEvent.layout;
@@ -98,7 +90,7 @@ function AvatarCropModal(props) {
}, []);
// An onLayout callback, that initializes the slider container size, for proper render of a slider
- const initializeSliderContainer = useCallback((event) => {
+ const initializeSliderContainer = useCallback((event: LayoutChangeEvent) => {
setSliderContainerSize(event.nativeEvent.layout.width);
}, []);
@@ -122,7 +114,6 @@ function AvatarCropModal(props) {
// In order to calculate proper image position/size/animation, we have to know its size.
// And we have to update image size if image url changes.
- const imageUri = props.imageUri;
useEffect(() => {
if (!imageUri) {
return;
@@ -143,17 +134,11 @@ function AvatarCropModal(props) {
/**
* Validates that value is within the provided mix/max range.
- *
- * @param {Number} value
- * @param {Array} minMax
- * @returns {Number}
*/
- const clamp = useWorkletCallback((value, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []);
+ const clamp = useWorkletCallback((value: number, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []);
/**
* Returns current image size taking into account scale and rotation.
- *
- * @returns {Object}
*/
const getDisplayedImageSize = useWorkletCallback(() => {
let height = imageContainerSize * scale.value;
@@ -172,12 +157,9 @@ function AvatarCropModal(props) {
/**
* Validates the offset to prevent overflow, and updates the image offset.
- *
- * @param {Number} newX
- * @param {Number} newY
*/
const updateImageOffset = useWorkletCallback(
- (offsetX, offsetY) => {
+ (offsetX: number, offsetY: number) => {
const {height, width} = getDisplayedImageSize();
const maxOffsetX = (width - imageContainerSize) / 2;
const maxOffsetY = (height - imageContainerSize) / 2;
@@ -189,12 +171,7 @@ function AvatarCropModal(props) {
[imageContainerSize, scale, clamp],
);
- /**
- * @param {Number} newSliderValue
- * @param {Number} containerSize
- * @returns {Number}
- */
- const newScaleValue = useWorkletCallback((newSliderValue, containerSize) => {
+ const newScaleValue = useWorkletCallback((newSliderValue: number, containerSize: number) => {
const {MAX_SCALE, MIN_SCALE} = CONST.AVATAR_CROP_MODAL;
return (newSliderValue / containerSize) * (MAX_SCALE - MIN_SCALE) + MIN_SCALE;
});
@@ -244,7 +221,7 @@ function AvatarCropModal(props) {
isPressableEnabled.value = false;
},
- onChange: (event) => {
+ onChange: (event: GestureUpdateEvent) => {
'worklet';
const newSliderValue = clamp(translateSlider.value + event.changeX, [0, sliderContainerSize]);
@@ -311,24 +288,35 @@ function AvatarCropModal(props) {
// Svg images are converted to a png blob to preserve transparency, so we need to update the
// image name and type accordingly.
- const isSvg = props.imageType.includes('image/svg');
- const imageName = isSvg ? 'fileName.png' : props.imageName;
- const imageType = isSvg ? 'image/png' : props.imageType;
+ const isSvg = imageType.includes('image/svg');
+ const name = isSvg ? 'fileName.png' : imageName;
+ const type = isSvg ? 'image/png' : imageType;
- cropOrRotateImage(props.imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name: imageName, type: imageType})
+ cropOrRotateImage(imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name, type})
.then((newImage) => {
- props.onClose();
- props.onSave(newImage);
+ onClose?.();
+ onSave?.(newImage);
})
.catch(() => {
isLoading.value = false;
});
- }, [originalImageHeight.value, originalImageWidth.value, scale.value, translateX.value, imageContainerSize, translateY.value, props, rotation.value, isLoading]);
-
- /**
- * @param {Number} locationX
- */
- const sliderOnPress = (locationX) => {
+ }, [
+ imageUri,
+ imageName,
+ imageType,
+ onClose,
+ onSave,
+ originalImageHeight.value,
+ originalImageWidth.value,
+ scale.value,
+ translateX.value,
+ imageContainerSize,
+ translateY.value,
+ rotation.value,
+ isLoading,
+ ]);
+
+ const sliderOnPress = (locationX: number) => {
// We are using the worklet directive here and running on the UI thread to ensure the Reanimated
// shared values are updated synchronously, as they update asynchronously on the JS thread.
@@ -349,8 +337,8 @@ function AvatarCropModal(props) {
return (
onClose?.()}
+ isVisible={isVisible}
type={CONST.MODAL.MODAL_TYPE.RIGHT_DOCKED}
onModalHide={resetState}
>
@@ -360,12 +348,12 @@ function AvatarCropModal(props) {
includeSafeAreaPaddingBottom={false}
testID={AvatarCropModal.displayName}
>
- {props.isSmallScreenWidth && }
+ {isSmallScreenWidth && }
- {props.translate('avatarCropModal.description')}
+ {translate('avatarCropModal.description')}
@@ -432,7 +420,7 @@ function AvatarCropModal(props) {
style={[styles.m5]}
onPress={cropAndSaveImage}
pressOnEnter
- text={props.translate('common.save')}
+ text={translate('common.save')}
/>
@@ -440,6 +428,5 @@ function AvatarCropModal(props) {
}
AvatarCropModal.displayName = 'AvatarCropModal';
-AvatarCropModal.propTypes = propTypes;
-AvatarCropModal.defaultProps = defaultProps;
-export default compose(withWindowDimensions, withLocalize)(AvatarCropModal);
+
+export default AvatarCropModal;
diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.tsx
similarity index 69%
rename from src/components/AvatarCropModal/ImageCropView.js
rename to src/components/AvatarCropModal/ImageCropView.tsx
index 0790ffaca8e1..c79a209376b4 100644
--- a/src/components/AvatarCropModal/ImageCropView.js
+++ b/src/components/AvatarCropModal/ImageCropView.tsx
@@ -1,58 +1,52 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
+import type {PanGesture} from 'react-native-gesture-handler';
import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated';
+import type {SharedValue} from 'react-native-reanimated';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import ControlSelection from '@libs/ControlSelection';
+import type IconAsset from '@src/types/utils/IconAsset';
-const propTypes = {
+type ImageCropViewProps = {
/** Link to image for cropping */
- imageUri: PropTypes.string,
+ imageUri?: string;
/** Size of the image container that will be rendered */
- containerSize: PropTypes.number,
+ containerSize?: number;
/** The height of the selected image */
- originalImageHeight: PropTypes.shape({value: PropTypes.number}).isRequired,
+ originalImageHeight: SharedValue;
/** The width of the selected image */
- originalImageWidth: PropTypes.shape({value: PropTypes.number}).isRequired,
+ originalImageWidth: SharedValue;
/** The rotation value of the selected image */
- rotation: PropTypes.shape({value: PropTypes.number}).isRequired,
+ rotation: SharedValue;
/** The relative image shift along X-axis */
- translateX: PropTypes.shape({value: PropTypes.number}).isRequired,
+ translateX: SharedValue;
/** The relative image shift along Y-axis */
- translateY: PropTypes.shape({value: PropTypes.number}).isRequired,
+ translateY: SharedValue;
/** The scale factor of the image */
- scale: PropTypes.shape({value: PropTypes.number}).isRequired,
+ scale: SharedValue;
/** Configuration object for pan gesture for handling image panning */
- // eslint-disable-next-line react/forbid-prop-types
- panGesture: PropTypes.object,
+ panGesture?: PanGesture;
/** Image crop vector mask */
- maskImage: PropTypes.func,
+ maskImage?: IconAsset;
};
-const defaultProps = {
- imageUri: '',
- containerSize: 0,
- panGesture: Gesture.Pan(),
- maskImage: Expensicons.ImageCropCircleMask,
-};
-
-function ImageCropView(props) {
+function ImageCropView({imageUri = '', containerSize = 0, panGesture = Gesture.Pan(), maskImage = Expensicons.ImageCropCircleMask, ...props}: ImageCropViewProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
- const containerStyle = StyleUtils.getWidthAndHeightStyle(props.containerSize, props.containerSize);
+ const containerStyle = StyleUtils.getWidthAndHeightStyle(containerSize, containerSize);
const originalImageHeight = props.originalImageHeight;
const originalImageWidth = props.originalImageWidth;
@@ -75,23 +69,23 @@ function ImageCropView(props) {
// We're preventing text selection with ControlSelection.blockElement to prevent safari
// default behaviour of cursor - I-beam cursor on drag. See https://github.com/Expensify/App/issues/13688
return (
-
+
ControlSelection.blockElement(el as HTMLElement | null)}
style={[containerStyle, styles.imageCropContainer]}
>
@@ -100,8 +94,6 @@ function ImageCropView(props) {
}
ImageCropView.displayName = 'ImageCropView';
-ImageCropView.propTypes = propTypes;
-ImageCropView.defaultProps = defaultProps;
// React.memo is needed here to prevent styles recompilation
// which sometimes may cause glitches during rerender of the modal
diff --git a/src/components/AvatarCropModal/Slider.js b/src/components/AvatarCropModal/Slider.tsx
similarity index 65%
rename from src/components/AvatarCropModal/Slider.js
rename to src/components/AvatarCropModal/Slider.tsx
index 83c8577d2e55..9a9da65befa0 100644
--- a/src/components/AvatarCropModal/Slider.js
+++ b/src/components/AvatarCropModal/Slider.tsx
@@ -1,43 +1,31 @@
-import PropTypes from 'prop-types';
import React, {useState} from 'react';
import {View} from 'react-native';
import {Gesture, GestureDetector} from 'react-native-gesture-handler';
+import type {GestureUpdateEvent, PanGestureChangeEventPayload, PanGestureHandlerEventPayload} from 'react-native-gesture-handler';
import Animated, {runOnJS, useAnimatedStyle} from 'react-native-reanimated';
+import type {SharedValue} from 'react-native-reanimated';
import Tooltip from '@components/Tooltip';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import ControlSelection from '@libs/ControlSelection';
-const propTypes = {
- /** Callbacks for react-native-gesture-handler to be executed when the user is panning slider */
- gestureCallbacks: PropTypes.shape({onBegin: PropTypes.func, onChange: PropTypes.func, onFinalize: PropTypes.func}),
+type SliderProps = {
+ /** React-native-reanimated lib handler which executes when the user is panning slider */
+ gestureCallbacks: {
+ onBegin: () => void;
+ onChange: (event: GestureUpdateEvent) => void;
+ onFinalize: () => void;
+ };
/** X position of the slider knob */
- sliderValue: PropTypes.shape({value: PropTypes.number}),
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- gestureCallbacks: {
- onBegin: () => {
- 'worklet';
- },
- onChange: () => {
- 'worklet';
- },
- onFinalize: () => {
- 'worklet';
- },
- },
- sliderValue: {},
+ sliderValue: SharedValue;
};
// This component can't be written using class since reanimated API uses hooks.
-function Slider(props) {
+function Slider({sliderValue, gestureCallbacks}: SliderProps) {
const styles = useThemeStyles();
- const sliderValue = props.sliderValue;
const [tooltipIsVisible, setTooltipIsVisible] = useState(true);
+ const {translate} = useLocalize();
// A reanimated memoized style, which tracks
// a translateX shared value and updates the slider position.
@@ -49,28 +37,28 @@ function Slider(props) {
.minDistance(5)
.onBegin(() => {
runOnJS(setTooltipIsVisible)(false);
- props.gestureCallbacks.onBegin();
+ gestureCallbacks.onBegin();
})
.onChange((event) => {
- props.gestureCallbacks.onChange(event);
+ gestureCallbacks.onChange(event);
})
.onFinalize(() => {
runOnJS(setTooltipIsVisible)(true);
- props.gestureCallbacks.onFinalize();
+ gestureCallbacks.onFinalize();
});
// We're preventing text selection with ControlSelection.blockElement to prevent safari
// default behaviour of cursor - I-beam cursor on drag. See https://github.com/Expensify/App/issues/13688
return (
ControlSelection.blockElement(el as HTMLElement | null)}
style={styles.sliderBar}
>
{tooltipIsVisible && (
{/* pointerEventsNone is a workaround to make sure the pan gesture works correctly on mobile safari */}
@@ -84,6 +72,4 @@ function Slider(props) {
}
Slider.displayName = 'Slider';
-Slider.propTypes = propTypes;
-Slider.defaultProps = defaultProps;
-export default withLocalize(Slider);
+export default Slider;
diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx
index 5fb134648134..f4b6e8b23ecf 100644
--- a/src/components/Button/index.tsx
+++ b/src/components/Button/index.tsx
@@ -20,7 +20,7 @@ import validateSubmitShortcut from './validateSubmitShortcut';
type ButtonWithText = {
/** The text for the button label */
- text: string;
+ text?: string;
/** Boolean whether to display the right icon */
shouldShowRightIcon?: boolean;
diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx
index d60a41e0f263..ade1513c8613 100644
--- a/src/components/Composer/index.android.tsx
+++ b/src/components/Composer/index.android.tsx
@@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import type {TextInput} from 'react-native';
import {StyleSheet} from 'react-native';
import RNTextInput from '@components/RNTextInput';
+import useResetComposerFocus from '@hooks/useResetComposerFocus';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ComposerUtils from '@libs/ComposerUtils';
@@ -28,6 +29,7 @@ function Composer(
ref: ForwardedRef,
) {
const textInput = useRef(null);
+ const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput);
const styles = useThemeStyles();
const theme = useTheme();
@@ -89,6 +91,12 @@ function Composer(
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
readOnly={isDisabled}
+ onBlur={(e) => {
+ if (!isFocused) {
+ shouldResetFocus.current = true; // detect the input is blurred when the page is hidden
+ }
+ props?.onBlur?.(e);
+ }}
/>
);
}
diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx
index b1357fef9a46..07736e5ddcba 100644
--- a/src/components/Composer/index.ios.tsx
+++ b/src/components/Composer/index.ios.tsx
@@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react';
import type {TextInput} from 'react-native';
import {StyleSheet} from 'react-native';
import RNTextInput from '@components/RNTextInput';
+import useResetComposerFocus from '@hooks/useResetComposerFocus';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ComposerUtils from '@libs/ComposerUtils';
@@ -28,7 +29,7 @@ function Composer(
ref: ForwardedRef,
) {
const textInput = useRef(null);
-
+ const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput);
const styles = useThemeStyles();
const theme = useTheme();
@@ -84,6 +85,12 @@ function Composer(
/* eslint-disable-next-line react/jsx-props-no-spreading */
{...props}
readOnly={isDisabled}
+ onBlur={(e) => {
+ if (!isFocused) {
+ shouldResetFocus.current = true; // detect the input is blurred when the page is hidden
+ }
+ props?.onBlur?.(e);
+ }}
/>
);
}
diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js
index e627119270dd..832715e3214c 100644
--- a/src/components/EmojiPicker/EmojiPickerButton.js
+++ b/src/components/EmojiPicker/EmojiPickerButton.js
@@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
import getButtonState from '@libs/getButtonState';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
+import CONST from '@src/CONST';
const propTypes = {
/** Flag to disable the emoji picker button */
@@ -22,6 +23,9 @@ const propTypes = {
/** Unique id for emoji picker */
emojiPickerID: PropTypes.string,
+ /** Emoji popup anchor offset shift vertical */
+ shiftVertical: PropTypes.number,
+
...withLocalizePropTypes,
};
@@ -29,6 +33,7 @@ const defaultProps = {
isDisabled: false,
id: '',
emojiPickerID: '',
+ shiftVertical: 0,
};
function EmojiPickerButton(props) {
@@ -49,7 +54,18 @@ function EmojiPickerButton(props) {
return;
}
if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) {
- EmojiPickerAction.showEmojiPicker(props.onModalHide, props.onEmojiSelected, emojiPopoverAnchor, undefined, () => {}, props.emojiPickerID);
+ EmojiPickerAction.showEmojiPicker(
+ props.onModalHide,
+ props.onEmojiSelected,
+ emojiPopoverAnchor,
+ {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ shiftVertical: props.shiftVertical,
+ },
+ () => {},
+ props.emojiPickerID,
+ );
} else {
EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker();
}
diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.tsx b/src/components/ErrorBoundary/BaseErrorBoundary.tsx
index 2f775aa4bef1..6a0f1a0ae55e 100644
--- a/src/components/ErrorBoundary/BaseErrorBoundary.tsx
+++ b/src/components/ErrorBoundary/BaseErrorBoundary.tsx
@@ -1,9 +1,7 @@
-import React, {useState} from 'react';
+import React from 'react';
import {ErrorBoundary} from 'react-error-boundary';
import BootSplash from '@libs/BootSplash';
import GenericErrorPage from '@pages/ErrorPage/GenericErrorPage';
-import UpdateRequiredView from '@pages/ErrorPage/UpdateRequiredView';
-import CONST from '@src/CONST';
import type {BaseErrorBoundaryProps, LogError} from './types';
/**
@@ -13,19 +11,15 @@ import type {BaseErrorBoundaryProps, LogError} from './types';
*/
function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseErrorBoundaryProps) {
- const [errorContent, setErrorContent] = useState('');
- const catchError = (errorObject: Error, errorInfo: React.ErrorInfo) => {
- logError(errorMessage, errorObject, JSON.stringify(errorInfo));
+ const catchError = (error: Error, errorInfo: React.ErrorInfo) => {
+ logError(errorMessage, error, JSON.stringify(errorInfo));
// We hide the splash screen since the error might happened during app init
BootSplash.hide();
- setErrorContent(errorObject.message);
};
- const updateRequired = errorContent === CONST.ERROR.UPDATE_REQUIRED;
-
return (
: }
+ fallback={}
onError={catchError}
>
{children}
diff --git a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js b/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js
deleted file mode 100644
index 2f7ac48b558b..000000000000
--- a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import PropTypes from 'prop-types';
-import participantPropTypes from '@components/participantPropTypes';
-import {ThreeDotsMenuItemPropTypes} from '@components/ThreeDotsMenu';
-import iouReportPropTypes from '@pages/iouReportPropTypes';
-
-const propTypes = {
- /** Title of the Header */
- title: PropTypes.string,
-
- /** Subtitle of the header */
- subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
-
- /** Method to trigger when pressing download button of the header */
- onDownloadButtonPress: PropTypes.func,
-
- /** Method to trigger when pressing close button of the header */
- onCloseButtonPress: PropTypes.func,
-
- /** Method to trigger when pressing back button of the header */
- onBackButtonPress: PropTypes.func,
-
- /** Method to trigger when pressing more options button of the header */
- onThreeDotsButtonPress: PropTypes.func,
-
- /** Whether we should show a border on the bottom of the Header */
- shouldShowBorderBottom: PropTypes.bool,
-
- /** Whether we should show a download button */
- shouldShowDownloadButton: PropTypes.bool,
-
- /** Whether we should show a get assistance (question mark) button */
- shouldShowGetAssistanceButton: PropTypes.bool,
-
- /** Whether we should disable the get assistance button */
- shouldDisableGetAssistanceButton: PropTypes.bool,
-
- /** Whether we should show a pin button */
- shouldShowPinButton: PropTypes.bool,
-
- /** Whether we should show a more options (threedots) button */
- shouldShowThreeDotsButton: PropTypes.bool,
-
- /** Whether we should disable threedots button */
- shouldDisableThreeDotsButton: PropTypes.bool,
-
- /** List of menu items for more(three dots) menu */
- threeDotsMenuItems: ThreeDotsMenuItemPropTypes,
-
- /** The anchor position of the menu */
- threeDotsAnchorPosition: PropTypes.shape({
- top: PropTypes.number,
- right: PropTypes.number,
- bottom: PropTypes.number,
- left: PropTypes.number,
- }),
-
- /** Whether we should show a close button */
- shouldShowCloseButton: PropTypes.bool,
-
- /** Whether we should show a back button */
- shouldShowBackButton: PropTypes.bool,
-
- /** The guides call taskID to associate with the get assistance button, if we show it */
- guidesCallTaskID: PropTypes.string,
-
- /** Data to display a step counter in the header */
- stepCounter: PropTypes.shape({
- step: PropTypes.number,
- total: PropTypes.number,
- text: PropTypes.string,
- }),
-
- /** Whether we should show an avatar */
- shouldShowAvatarWithDisplay: PropTypes.bool,
-
- /** Parent report, if provided it will override props.report for AvatarWithDisplay */
- parentReport: iouReportPropTypes,
-
- /** Report, if we're showing the details for one and using AvatarWithDisplay */
- report: iouReportPropTypes,
-
- /** The report's policy, if we're showing the details for a report and need info about it for AvatarWithDisplay */
- policy: PropTypes.shape({
- /** Name of the policy */
- name: PropTypes.string,
- }),
-
- /** Policies, if we're showing the details for a report and need participant details for AvatarWithDisplay */
- personalDetails: PropTypes.objectOf(participantPropTypes),
-
- /** Children to wrap in Header */
- children: PropTypes.node,
-
- /** Single execution function to prevent concurrent navigation actions */
- singleExecution: PropTypes.func,
-
- /** Whether we should navigate to report page when the route have a topMostReport */
- shouldNavigateToTopMostReport: PropTypes.bool,
-
- /** Whether we should overlay the 3 dots menu */
- shouldOverlayDots: PropTypes.bool,
-};
-
-export default propTypes;
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index 832351b2b70e..725d14e041a7 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -9,7 +9,7 @@ import type IconAsset from '@src/types/utils/IconAsset';
type ThreeDotsMenuItem = {
/** An icon element displayed on the left side */
- icon?: IconAsset;
+ icon: IconAsset;
/** Text label */
text: string;
diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx
index 1932cf6c6b7f..4123e9d20d58 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.tsx
+++ b/src/components/LHNOptionsList/OptionRowLHN.tsx
@@ -57,18 +57,17 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
return null;
}
+ const isInFocusMode = viewMode === CONST.OPTION_MODE.COMPACT;
const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText;
- const textUnreadStyle = optionItem?.isUnread ? [textStyle, styles.sidebarLinkTextBold] : [textStyle];
+ const textUnreadStyle = optionItem?.isUnread && optionItem.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ? [textStyle, styles.sidebarLinkTextBold] : [textStyle];
const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, style];
- const alternateTextStyle =
- viewMode === CONST.OPTION_MODE.COMPACT
- ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style]
- : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, style];
+ const alternateTextStyle = isInFocusMode
+ ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style]
+ : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, style];
- const contentContainerStyles =
- viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1];
+ const contentContainerStyles = isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1];
const sidebarInnerRowStyle = StyleSheet.flatten(
- viewMode === CONST.OPTION_MODE.COMPACT
+ isInFocusMode
? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter]
: [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter],
);
@@ -113,7 +112,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
const report = ReportUtils.getReport(optionItem.reportID ?? '');
const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null);
- const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips?.length ?? 0) > 2;
+ const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && !optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips?.length ?? 0) > 2;
const fullTitle = isGroupChat ? getGroupChatName(!isEmptyObject(report) ? report : null) : optionItem.text;
const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar;
@@ -175,13 +174,13 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
backgroundColor={hovered && !isFocused ? hoveredBackgroundColor : subscriptAvatarBorderColor}
mainAvatar={optionItem.icons?.[0]}
secondaryAvatar={optionItem.icons?.[1]}
- size={viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT}
+ size={isInFocusMode ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT}
/>
) : (
= {
@@ -52,11 +51,6 @@ const DotLottieAnimations: Record = {
w: 853,
h: 480,
},
- Update: {
- file: require('@assets/animations/Update.lottie'),
- w: variables.updateAnimationW,
- h: variables.updateAnimationH,
- },
Coin: {
file: require('@assets/animations/Coin.lottie'),
w: 375,
diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js
index 59465e34eec0..8614736d200f 100644
--- a/src/components/MagicCodeInput.js
+++ b/src/components/MagicCodeInput.js
@@ -61,6 +61,9 @@ const propTypes = {
/** Last pressed digit on BigDigitPad */
lastPressedDigit: PropTypes.string,
+
+ /** TestID for test */
+ testID: PropTypes.string,
};
const defaultProps = {
@@ -77,6 +80,7 @@ const defaultProps = {
maxLength: CONST.MAGIC_CODE_LENGTH,
isDisableKeyboard: false,
lastPressedDigit: '',
+ testID: '',
};
/**
@@ -394,6 +398,7 @@ function MagicCodeInput(props) {
role={CONST.ACCESSIBILITY_ROLE.TEXT}
style={[styles.inputTransparent]}
textInputContainerStyles={[styles.borderNone]}
+ testID={props.testID}
/>
diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx
index 17b1a119671a..ff7d0fdfb8e5 100644
--- a/src/components/PopoverMenu.tsx
+++ b/src/components/PopoverMenu.tsx
@@ -177,3 +177,4 @@ function PopoverMenu({
PopoverMenu.displayName = 'PopoverMenu';
export default React.memo(PopoverMenu);
+export type {PopoverMenuItem};
diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts
index dc04b6fcf329..2dd2e17e0454 100644
--- a/src/components/Pressable/GenericPressable/types.ts
+++ b/src/components/Pressable/GenericPressable/types.ts
@@ -40,7 +40,7 @@ type PressableProps = RNPressableProps &
/**
* onPress callback
*/
- onPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise;
+ onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise;
/**
* Specifies keyboard shortcut to trigger onPressHandler
diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx
index ab1fa95efeb5..86f6c9d8aff8 100644
--- a/src/components/Pressable/PressableWithDelayToggle.tsx
+++ b/src/components/Pressable/PressableWithDelayToggle.tsx
@@ -78,7 +78,7 @@ function PressableWithDelayToggle(
return;
}
temporarilyDisableInteractions();
- onPress();
+ onPress?.();
};
// Due to limitations in RN regarding the vertical text alignment of non-Text elements,
diff --git a/src/components/Pressable/PressableWithoutFocus.tsx b/src/components/Pressable/PressableWithoutFocus.tsx
index f887b0ea9b7d..240ef4a9873a 100644
--- a/src/components/Pressable/PressableWithoutFocus.tsx
+++ b/src/components/Pressable/PressableWithoutFocus.tsx
@@ -15,7 +15,7 @@ function PressableWithoutFocus({children, onPress, onLongPress, ...rest}: Pressa
const pressAndBlur = () => {
ref?.current?.blur();
- onPress();
+ onPress?.();
};
return (
diff --git a/src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js b/src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js
deleted file mode 100644
index 9f09eabbc7f7..000000000000
--- a/src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import PropTypes from 'prop-types';
-import sourcePropTypes from '@components/Image/sourcePropTypes';
-
-const menuItemProps = PropTypes.arrayOf(
- PropTypes.shape({
- icon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]),
- text: PropTypes.string,
- onPress: PropTypes.func,
- }),
-);
-
-export default menuItemProps;
diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.tsx
similarity index 67%
rename from src/components/ThreeDotsMenu/index.js
rename to src/components/ThreeDotsMenu/index.tsx
index 150487b2aa57..920b8f9f4130 100644
--- a/src/components/ThreeDotsMenu/index.js
+++ b/src/components/ThreeDotsMenu/index.tsx
@@ -1,10 +1,10 @@
-import PropTypes from 'prop-types';
import React, {useRef, useState} from 'react';
+import type {StyleProp, ViewStyle} from 'react-native';
import {View} from 'react-native';
-import _ from 'underscore';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
-import sourcePropTypes from '@components/Image/sourcePropTypes';
+import type {AnchorAlignment} from '@components/Popover/types';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
import PopoverMenu from '@components/PopoverMenu';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Tooltip from '@components/Tooltip/PopoverAnchorTooltip';
@@ -13,68 +13,61 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Browser from '@libs/Browser';
import CONST from '@src/CONST';
-import ThreeDotsMenuItemPropTypes from './ThreeDotsMenuItemPropTypes';
+import type {TranslationPaths} from '@src/languages/types';
+import type {AnchorPosition} from '@src/styles';
+import type IconAsset from '@src/types/utils/IconAsset';
-const propTypes = {
+type ThreeDotsMenuProps = {
/** Tooltip for the popup icon */
- iconTooltip: PropTypes.string,
+ iconTooltip?: TranslationPaths;
/** icon for the popup trigger */
- icon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]),
+ icon?: IconAsset;
/** Any additional styles to pass to the icon container. */
- // eslint-disable-next-line react/forbid-prop-types
- iconStyles: PropTypes.arrayOf(PropTypes.object),
+ iconStyles?: StyleProp;
/** The fill color to pass into the icon. */
- iconFill: PropTypes.string,
+ iconFill?: string;
/** Function to call on icon press */
- onIconPress: PropTypes.func,
+ onIconPress?: () => void;
/** menuItems that'll show up on toggle of the popup menu */
- menuItems: ThreeDotsMenuItemPropTypes.isRequired,
+ menuItems: PopoverMenuItem[];
/** The anchor position of the menu */
- anchorPosition: PropTypes.shape({
- top: PropTypes.number,
- right: PropTypes.number,
- bottom: PropTypes.number,
- left: PropTypes.number,
- }).isRequired,
+ anchorPosition: AnchorPosition;
/** The anchor alignment of the menu */
- anchorAlignment: PropTypes.shape({
- horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
- vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
- }),
+ anchorAlignment?: AnchorAlignment;
/** Whether the popover menu should overlay the current view */
- shouldOverlay: PropTypes.bool,
+ shouldOverlay?: boolean;
/** Whether the menu is disabled */
- disabled: PropTypes.bool,
+ disabled?: boolean;
/** Should we announce the Modal visibility changes? */
- shouldSetModalVisibility: PropTypes.bool,
+ shouldSetModalVisibility?: boolean;
};
-const defaultProps = {
- iconTooltip: 'common.more',
- disabled: false,
- iconFill: undefined,
- iconStyles: [],
- icon: Expensicons.ThreeDots,
- onIconPress: () => {},
- anchorAlignment: {
+function ThreeDotsMenu({
+ iconTooltip = 'common.more',
+ icon = Expensicons.ThreeDots,
+ iconFill,
+ iconStyles,
+ onIconPress = () => {},
+ menuItems,
+ anchorPosition,
+ anchorAlignment = {
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP
},
- shouldOverlay: false,
- shouldSetModalVisibility: true,
-};
-
-function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment, shouldOverlay, shouldSetModalVisibility, disabled}) {
+ shouldOverlay = false,
+ shouldSetModalVisibility = true,
+ disabled = false,
+}: ThreeDotsMenuProps) {
const theme = useTheme();
const styles = useThemeStyles();
const [isPopupMenuVisible, setPopupMenuVisible] = useState(false);
@@ -113,13 +106,13 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me
e.preventDefault();
}}
ref={buttonRef}
- style={[styles.touchableButtonImage, ...iconStyles]}
+ style={[styles.touchableButtonImage, iconStyles]}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate(iconTooltip)}
>
@@ -139,10 +132,6 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me
);
}
-ThreeDotsMenu.propTypes = propTypes;
-ThreeDotsMenu.defaultProps = defaultProps;
ThreeDotsMenu.displayName = 'ThreeDotsMenu';
export default ThreeDotsMenu;
-
-export {ThreeDotsMenuItemPropTypes};
diff --git a/src/hooks/useResetComposerFocus.ts b/src/hooks/useResetComposerFocus.ts
new file mode 100644
index 000000000000..e9f88ed93346
--- /dev/null
+++ b/src/hooks/useResetComposerFocus.ts
@@ -0,0 +1,19 @@
+import {useIsFocused} from '@react-navigation/native';
+import type {MutableRefObject} from 'react';
+import {useEffect, useRef} from 'react';
+import type {TextInput} from 'react-native';
+
+export default function useResetComposerFocus(inputRef: MutableRefObject) {
+ const isFocused = useIsFocused();
+ const shouldResetFocus = useRef(false);
+
+ useEffect(() => {
+ if (!isFocused || !shouldResetFocus.current) {
+ return;
+ }
+ inputRef.current?.focus(); // focus input again
+ shouldResetFocus.current = false;
+ }, [isFocused, inputRef]);
+
+ return {isFocused, shouldResetFocus};
+}
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 712113cb89a9..46d9e90e84d9 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -20,6 +20,7 @@ import type {
DeleteConfirmationParams,
DidSplitAmountMessageParams,
EditActionParams,
+ ElectronicFundsParams,
EnterMagicCodeParams,
FormattedMaxLengthParams,
GoBackMessageParams,
@@ -67,6 +68,7 @@ import type {
StepCounterParams,
TagSelectionParams,
TaskCreatedActionParams,
+ TermsParams,
ThreadRequestReportNameParams,
ThreadSentMoneyReportNameParams,
ToValidateLoginParams,
@@ -299,7 +301,6 @@ export default {
showing: 'Showing',
of: 'of',
default: 'Default',
- update: 'Update',
},
location: {
useCurrent: 'Use current location',
@@ -773,11 +774,6 @@ export default {
isShownOnProfile: 'Your timezone is shown on your profile.',
getLocationAutomatically: 'Automatically determine your location.',
},
- updateRequiredView: {
- updateRequired: 'Update required',
- pleaseInstall: 'Please update to the latest version of New Expensify',
- toGetLatestChanges: 'For mobile or desktop, download and install the latest version. For web, refresh your browser.',
- },
initialSettingsPage: {
about: 'About',
aboutPage: {
@@ -1364,10 +1360,8 @@ export default {
agreeToThe: 'I agree to the',
walletAgreement: 'Wallet agreement',
enablePayments: 'Enable payments',
- feeAmountZero: '$0',
monthlyFee: 'Monthly fee',
inactivity: 'Inactivity',
- electronicFundsInstantFee: '1.5%',
noOverdraftOrCredit: 'No overdraft/credit feature.',
electronicFundsWithdrawal: 'Electronic funds withdrawal',
standard: 'Standard',
@@ -1389,7 +1383,7 @@ export default {
conditionsDetails: 'Find details and conditions for all fees and services by visiting',
conditionsPhone: 'or calling +1 833-400-0904.',
instant: '(instant)',
- electronicFundsInstantFeeMin: '(min $0.25)',
+ electronicFundsInstantFeeMin: ({amount}: TermsParams) => `(min ${amount})`,
},
longTermsForm: {
listOfAllFees: 'A list of all Expensify Wallet fees',
@@ -1408,14 +1402,14 @@ export default {
'There is no fee to transfer funds from your Expensify Wallet ' +
'to your bank account using the standard option. This transfer usually completes within 1-3 business' +
' days.',
- electronicFundsInstantDetails:
+ electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) =>
'There is a fee to transfer funds from your Expensify Wallet to ' +
'your linked debit card using the instant transfer option. This transfer usually completes within ' +
- 'several minutes. The fee is 1.5% of the transfer amount (with a minimum fee of $0.25).',
- fdicInsuranceBancorp:
+ `several minutes. The fee is ${percentage}% of the transfer amount (with a minimum fee of ${amount}).`,
+ fdicInsuranceBancorp: ({amount}: TermsParams) =>
'Your funds are eligible for FDIC insurance. Your funds will be held at or ' +
`transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` +
- `to $250,000 by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`,
+ `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`,
fdicInsuranceBancorp2: 'for details.',
contactExpensifyPayments: `Contact ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} by calling +1 833-400-0904, by email at`,
contactExpensifyPayments2: 'or sign in at',
@@ -1425,7 +1419,7 @@ export default {
automated: 'Automated',
liveAgent: 'Live Agent',
instant: 'Instant',
- electronicFundsInstantFeeMin: 'Min $0.25',
+ electronicFundsInstantFeeMin: ({amount}: TermsParams) => `Min ${amount}`,
},
},
activateStep: {
diff --git a/src/languages/es.ts b/src/languages/es.ts
index d46f275a8109..db010d3266c2 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -18,6 +18,7 @@ import type {
DeleteConfirmationParams,
DidSplitAmountMessageParams,
EditActionParams,
+ ElectronicFundsParams,
EnglishTranslation,
EnterMagicCodeParams,
FormattedMaxLengthParams,
@@ -66,6 +67,7 @@ import type {
StepCounterParams,
TagSelectionParams,
TaskCreatedActionParams,
+ TermsParams,
ThreadRequestReportNameParams,
ThreadSentMoneyReportNameParams,
ToValidateLoginParams,
@@ -288,7 +290,6 @@ export default {
showing: 'Mostrando',
of: 'de',
default: 'Predeterminado',
- update: 'Actualizar',
},
location: {
useCurrent: 'Usar ubicación actual',
@@ -442,10 +443,10 @@ export default {
copyEmailToClipboard: 'Copiar email al portapapeles',
markAsUnread: 'Marcar como no leído',
markAsRead: 'Marcar como leído',
- editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`,
- deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`,
+ editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`,
+ deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`,
deleteConfirmation: ({action}: DeleteConfirmationParams) =>
- `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`,
+ `¿Estás seguro de que quieres eliminar esta ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`,
onlyVisible: 'Visible sólo para',
replyInThread: 'Responder en el hilo',
joinThread: 'Unirse al hilo',
@@ -460,16 +461,16 @@ export default {
reportActionsView: {
beginningOfArchivedRoomPartOne: 'Te perdiste la fiesta en ',
beginningOfArchivedRoomPartTwo: ', no hay nada que ver aquí.',
- beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}: BeginningOfChatHistoryDomainRoomPartOneParams) => `Colabora aquí con todos los participantes de ${domainRoom}! 🎉\nUtiliza `,
+ beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}: BeginningOfChatHistoryDomainRoomPartOneParams) => `¡Colabora aquí con todos los participantes de ${domainRoom}! 🎉\nUtiliza `,
beginningOfChatHistoryDomainRoomPartTwo: ' para chatear con compañeros, compartir consejos o hacer una pregunta.',
beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAdminRoomPartOneParams) =>
- `Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `,
+ `¡Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `,
beginningOfChatHistoryAdminRoomPartTwo: ' para chatear sobre temas como la configuración del espacio de trabajo y mas.',
beginningOfChatHistoryAdminOnlyPostingRoom: 'Solo los administradores pueden enviar mensajes en esta sala.',
beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartOneParams) =>
- `Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `,
+ `¡Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `,
beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`,
- beginningOfChatHistoryUserRoomPartOne: 'Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ',
+ beginningOfChatHistoryUserRoomPartOne: '¡Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ',
beginningOfChatHistoryUserRoomPartTwo: '.',
beginningOfChatHistory: 'Aquí comienzan tus conversaciones con ',
beginningOfChatHistoryPolicyExpenseChatPartOne: '¡La colaboración entre ',
@@ -582,8 +583,8 @@ export default {
transactionPendingText: 'La transacción tarda unos días en contabilizarse desde la fecha en que se utilizó la tarjeta.',
requestCount: ({count, scanningReceipts = 0}: RequestCountParams) =>
`${count} ${Str.pluralize('solicitude', 'solicitudes', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}`,
- deleteRequest: 'Eliminar pedido',
- deleteConfirmation: '¿Estás seguro de que quieres eliminar este pedido?',
+ deleteRequest: 'Eliminar solicitud',
+ deleteConfirmation: '¿Estás seguro de que quieres eliminar esta solicitud?',
settledExpensify: 'Pagado',
settledElsewhere: 'Pagado de otra forma',
settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`),
@@ -628,22 +629,22 @@ export default {
tagSelection: ({tagName}: TagSelectionParams) => `Seleccione una ${tagName} para organizar mejor tu dinero.`,
categorySelection: 'Seleccione una categoría para organizar mejor tu dinero.',
error: {
- invalidCategoryLength: 'El largo de la categoría escogida excede el máximo permitido (255). Por favor escoge otra categoría o acorta la categoría primero.',
- invalidAmount: 'Por favor ingresa un monto válido antes de continuar.',
+ invalidCategoryLength: 'El largo de la categoría escogida excede el máximo permitido (255). Por favor, escoge otra categoría o acorta la categoría primero.',
+ invalidAmount: 'Por favor, ingresa un importe válido antes de continuar.',
invalidTaxAmount: ({amount}: RequestAmountParams) => `El importe máximo del impuesto es ${amount}`,
- invalidSplit: 'La suma de las partes no equivale al monto total',
+ invalidSplit: 'La suma de las partes no equivale al importe total',
other: 'Error inesperado, por favor inténtalo más tarde',
- genericCreateFailureMessage: 'Error inesperado solicitando dinero, Por favor, inténtalo más tarde',
+ genericCreateFailureMessage: 'Error inesperado solicitando dinero. Por favor, inténtalo más tarde',
receiptFailureMessage: 'El recibo no se subió. ',
saveFileMessage: 'Guarda el archivo ',
loseFileMessage: 'o descarta este error y piérdelo',
genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde',
genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde',
genericSmartscanFailureMessage: 'La transacción tiene campos vacíos',
- duplicateWaypointsErrorMessage: 'Por favor elimina los puntos de ruta duplicados',
- atLeastTwoDifferentWaypoints: 'Por favor introduce al menos dos direcciones diferentes',
- splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor actualiza tu selección.',
- invalidMerchant: 'Por favor ingrese un comerciante correcto.',
+ duplicateWaypointsErrorMessage: 'Por favor, elimina los puntos de ruta duplicados',
+ atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes',
+ splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selección.',
+ invalidMerchant: 'Por favor, introduce un comerciante correcto.',
},
waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`,
enableWallet: 'Habilitar Billetera',
@@ -767,11 +768,6 @@ export default {
isShownOnProfile: 'Tu zona horaria se muestra en tu perfil.',
getLocationAutomatically: 'Detecta tu ubicación automáticamente.',
},
- updateRequiredView: {
- updateRequired: 'Actualización requerida',
- pleaseInstall: 'Por favor, actualice la última versión de Nuevo Expensify',
- toGetLatestChanges: 'Para móvil o escritorio, descarga e instala la última versión. Para la web, actualiza tu navegador.',
- },
initialSettingsPage: {
about: 'Acerca de',
aboutPage: {
@@ -871,7 +867,7 @@ export default {
},
},
passwordConfirmationScreen: {
- passwordUpdated: 'Contraseña actualizada!',
+ passwordUpdated: '¡Contraseña actualizada!',
allSet: 'Todo está listo. Guarda tu contraseña en un lugar seguro.',
},
privateNotes: {
@@ -928,7 +924,7 @@ export default {
enableWallet: 'Habilitar Billetera',
bankAccounts: 'Cuentas bancarias',
addBankAccountToSendAndReceive: 'Añade una cuenta bancaria para enviar y recibir pagos directamente en la aplicación.',
- addBankAccount: 'Agregar cuenta bancaria',
+ addBankAccount: 'Añadir cuenta bancaria',
assignedCards: 'Tarjetas asignadas',
assignedCardsDescription: 'Son tarjetas asignadas por un administrador del Espacio de Trabajo para gestionar los gastos de la empresa.',
expensifyCard: 'Tarjeta Expensify',
@@ -1217,7 +1213,7 @@ export default {
},
statusPage: {
status: 'Estado',
- statusExplanation: 'Agrega un emoji para que tus colegas y amigos puedan saber fácilmente qué está pasando. ¡También puedes agregar un mensaje opcionalmente!',
+ statusExplanation: 'Añade un emoji para que tus colegas y amigos puedan saber fácilmente qué está pasando. ¡También puedes añadir un mensaje opcionalmente!',
today: 'Hoy',
clearStatus: 'Borrar estado',
save: 'Guardar',
@@ -1381,35 +1377,33 @@ export default {
headerTitle: 'Condiciones y tarifas',
haveReadAndAgree: 'He leído y acepto recibir ',
electronicDisclosures: 'divulgaciones electrónicas',
- agreeToThe: 'Estoy de acuerdo con la ',
- walletAgreement: 'Acuerdo de billetera',
+ agreeToThe: 'Estoy de acuerdo con el ',
+ walletAgreement: 'Acuerdo de la billetera',
enablePayments: 'Habilitar pagos',
- feeAmountZero: '$0',
monthlyFee: 'Cuota mensual',
inactivity: 'Inactividad',
- electronicFundsInstantFee: '1.5%',
- noOverdraftOrCredit: 'Sin función de sobregiro / crédito',
+ noOverdraftOrCredit: 'Sin función de sobregiro/crédito',
electronicFundsWithdrawal: 'Retiro electrónico de fondos',
standard: 'Estándar',
shortTermsForm: {
expensifyPaymentsAccount: ({walletProgram}: WalletProgramParams) => `La billetera Expensify es emitida por ${walletProgram}.`,
perPurchase: 'Por compra',
- atmWithdrawal: 'Retiro de cajero automático',
+ atmWithdrawal: 'Retiro en cajeros automáticos',
cashReload: 'Recarga de efectivo',
inNetwork: 'en la red',
outOfNetwork: 'fuera de la red',
- atmBalanceInquiry: 'Consulta de saldo de cajero automático',
+ atmBalanceInquiry: 'Consulta de saldo en cajeros automáticos',
inOrOutOfNetwork: '(dentro o fuera de la red)',
customerService: 'Servicio al cliente',
automatedOrLive: '(agente automatizado o en vivo)',
afterTwelveMonths: '(después de 12 meses sin transacciones)',
weChargeOneFee: 'Cobramos un tipo de tarifa.',
- fdicInsurance: 'Sus fondos son elegibles para el seguro de la FDIC.',
- generalInfo: 'Para obtener información general sobre cuentas prepagas, visite',
+ fdicInsurance: 'Tus fondos pueden acogerse al seguro de la FDIC.',
+ generalInfo: 'Para obtener información general sobre cuentas de prepago, visite',
conditionsDetails: 'Encuentra detalles y condiciones para todas las tarifas y servicios visitando',
conditionsPhone: 'o llamando al +1 833-400-0904.',
instant: '(instantáneo)',
- electronicFundsInstantFeeMin: '(mínimo $0.25)',
+ electronicFundsInstantFeeMin: ({amount}: TermsParams) => `(mínimo ${amount})`,
},
longTermsForm: {
listOfAllFees: 'Una lista de todas las tarifas de la billetera Expensify',
@@ -1423,30 +1417,30 @@ export default {
customerServiceDetails: 'No hay tarifas de servicio al cliente.',
inactivityDetails: 'No hay tarifa de inactividad.',
sendingFundsTitle: 'Enviar fondos a otro titular de cuenta',
- sendingFundsDetails: 'No se aplica ningún cargo por enviar fondos a otro titular de cuenta utilizando su saldo cuenta bancaria o tarjeta de débito',
+ sendingFundsDetails: 'No se aplica ningún cargo por enviar fondos a otro titular de cuenta utilizando tu saldo cuenta bancaria o tarjeta de débito',
electronicFundsStandardDetails:
- 'No hay cargo por transferir fondos desde su billetera Expensify ' +
- 'a su cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' +
- '1-3 negocios días.',
- electronicFundsInstantDetails:
- 'Hay una tarifa para transferir fondos desde su billetera Expensify a ' +
- 'su tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' +
- 'generalmente se completa dentro de varios minutos. La tarifa es el 1.5% del monto de la ' +
- 'transferencia (con una tarifa mínima de $ 0.25). ',
- fdicInsuranceBancorp:
- 'Sus fondos son elegibles para el seguro de la FDIC. Sus fondos se mantendrán en o ' +
- `transferido a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, sus fondos ` +
- `están asegurados a $ 250,000 por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`,
- fdicInsuranceBancorp2: 'para detalles.',
- contactExpensifyPayments: `Comuníquese con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, por correoelectrónico a`,
+ 'No hay cargo por transferir fondos desde tu billetera Expensify ' +
+ 'a tu cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' +
+ '1-3 días laborables.',
+ electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) =>
+ 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' +
+ 'la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' +
+ `generalmente se completa dentro de varios minutos. La tarifa es el ${percentage}% del importe de la ` +
+ `transferencia (con una tarifa mínima de ${amount}). `,
+ fdicInsuranceBancorp: ({amount}: TermsParams) =>
+ 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' +
+ `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` +
+ `están asegurados hasta ${amount} por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`,
+ fdicInsuranceBancorp2: 'para más detalles.',
+ contactExpensifyPayments: `Comunícate con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, o por correo electrónico a`,
contactExpensifyPayments2: 'o inicie sesión en',
- generalInformation: 'Para obtener información general sobre cuentas prepagas, visite',
- generalInformation2: 'Si tiene una queja sobre una cuenta prepaga, llame al Consumer Financial Oficina de Protección al 1-855-411-2372 o visite',
+ generalInformation: 'Para obtener información general sobre cuentas de prepago, visite',
+ generalInformation2: 'Si tienes alguna queja sobre una cuenta de prepago, llama al Consumer Financial Oficina de Protección al 1-855-411-2372 o visita',
printerFriendlyView: 'Ver versión para imprimir',
automated: 'Automatizado',
liveAgent: 'Agente en vivo',
instant: 'Instantáneo',
- electronicFundsInstantFeeMin: 'Mínimo $0.25',
+ electronicFundsInstantFeeMin: ({amount}: TermsParams) => `Mínimo ${amount}`,
},
},
activateStep: {
@@ -1454,7 +1448,7 @@ export default {
activatedTitle: '¡Billetera activada!',
activatedMessage: 'Felicidades, tu Billetera está configurada y lista para hacer pagos.',
checkBackLaterTitle: 'Un momento...',
- checkBackLaterMessage: 'Todavía estamos revisando tu información. Por favor, vuelva más tarde.',
+ checkBackLaterMessage: 'Todavía estamos revisando tu información. Por favor, vuelve más tarde.',
continueToPayment: 'Continuar al pago',
continueToTransfer: 'Continuar a la transferencia',
},
@@ -1819,7 +1813,7 @@ export default {
resultsAreLimited: 'Los resultados de búsqueda están limitados.',
},
genericErrorPage: {
- title: '¡Uh-oh, algo salió mal!',
+ title: '¡Oh-oh, algo salió mal!',
body: {
helpTextMobile: 'Intenta cerrar y volver a abrir la aplicación o cambiar a la',
helpTextWeb: 'web.',
@@ -1902,12 +1896,12 @@ export default {
},
notAvailable: {
title: 'Actualización no disponible',
- message: 'No existe ninguna actualización disponible! Inténtalo de nuevo más tarde.',
+ message: '¡No existe ninguna actualización disponible! Inténtalo de nuevo más tarde.',
okay: 'Vale',
},
error: {
title: 'Comprobación fallida',
- message: 'No hemos podido comprobar si existe una actualización. Inténtalo de nuevo más tarde!',
+ message: 'No hemos podido comprobar si existe una actualización. ¡Inténtalo de nuevo más tarde!',
},
},
report: {
@@ -2425,7 +2419,7 @@ export default {
},
parentReportAction: {
deletedMessage: '[Mensaje eliminado]',
- deletedRequest: '[Pedido eliminado]',
+ deletedRequest: '[Solicitud eliminada]',
reversedTransaction: '[Transacción anulada]',
deletedTask: '[Tarea eliminada]',
hiddenMessage: '[Mensaje oculto]',
@@ -2449,13 +2443,13 @@ export default {
flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para su revisión.',
chooseAReason: 'Elige abajo un motivo para reportarlo:',
spam: 'Spam',
- spamDescription: 'Promoción fuera de tema no solicitada',
+ spamDescription: 'Publicidad no solicitada',
inconsiderate: 'Desconsiderado',
inconsiderateDescription: 'Frase insultante o irrespetuosa, con intenciones cuestionables',
intimidation: 'Intimidación',
intimidationDescription: 'Persigue agresivamente una agenda sobre objeciones válidas',
bullying: 'Bullying',
- bullyingDescription: 'Apunta a un individuo para obtener obediencia',
+ bullyingDescription: 'Se dirige a un individuo para obtener obediencia',
harassment: 'Acoso',
harassmentDescription: 'Comportamiento racista, misógino u otro comportamiento discriminatorio',
assault: 'Agresion',
@@ -2463,8 +2457,8 @@ export default {
flaggedContent: 'Este mensaje ha sido marcado por violar las reglas de nuestra comunidad y el contenido se ha ocultado.',
hideMessage: 'Ocultar mensaje',
revealMessage: 'Revelar mensaje',
- levelOneResult: 'Envia una advertencia anónima y el mensaje es reportado para revisión.',
- levelTwoResult: 'Mensaje ocultado del canal, más advertencia anónima y mensaje reportado para revisión.',
+ levelOneResult: 'Envía una advertencia anónima y el mensaje es reportado para revisión.',
+ levelTwoResult: 'Mensaje ocultado en el canal, más advertencia anónima y mensaje reportado para revisión.',
levelThreeResult: 'Mensaje eliminado del canal, más advertencia anónima y mensaje reportado para revisión.',
},
teachersUnitePage: {
@@ -2497,7 +2491,7 @@ export default {
companySpend: 'Gastos de empresa',
},
distance: {
- addStop: 'Agregar parada',
+ addStop: 'Añadir parada',
deleteWaypoint: 'Eliminar punto de ruta',
deleteWaypointConfirmation: '¿Estás seguro de que quieres eliminar este punto de ruta?',
address: 'Dirección',
@@ -2579,21 +2573,21 @@ export default {
allTagLevelsRequired: 'Todas las etiquetas son obligatorias',
autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`,
billableExpense: 'La opción facturable ya no es válida',
- cashExpenseWithNoReceipt: ({amount}: ViolationsCashExpenseWithNoReceiptParams) => `Recibo obligatorio para montos mayores a ${amount}`,
+ cashExpenseWithNoReceipt: ({amount}: ViolationsCashExpenseWithNoReceiptParams) => `Recibo obligatorio para cantidades mayores de ${amount}`,
categoryOutOfPolicy: 'La categoría ya no es válida',
conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `${surcharge}% de recargo aplicado`,
- customUnitOutOfPolicy: 'Unidad ya no es válida',
- duplicatedTransaction: 'Potencial duplicado',
+ customUnitOutOfPolicy: 'La unidad ya no es válida',
+ duplicatedTransaction: 'Posible duplicado',
fieldRequired: 'Los campos del informe son obligatorios',
futureDate: 'Fecha futura no permitida',
invoiceMarkup: ({invoiceMarkup}: ViolationsInvoiceMarkupParams) => `Incrementado un ${invoiceMarkup}%`,
maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Fecha de más de ${maxAge} días`,
missingCategory: 'Falta categoría',
- missingComment: 'Descripción obligatoria para categoría seleccionada',
+ missingComment: 'Descripción obligatoria para la categoría seleccionada',
missingTag: ({tagName}: ViolationsMissingTagParams) => `Falta ${tagName}`,
modifiedAmount: 'Importe superior al del recibo escaneado',
modifiedDate: 'Fecha difiere del recibo escaneado',
- nonExpensiworksExpense: 'Gasto no es de Expensiworks',
+ nonExpensiworksExpense: 'Gasto no proviene de Expensiworks',
overAutoApprovalLimit: ({formattedLimitAmount}: ViolationsOverAutoApprovalLimitParams) => `Importe supera el límite de aprobación automática de ${formattedLimitAmount}`,
overCategoryLimit: ({categoryLimit}: ViolationsOverCategoryLimitParams) => `Importe supera el límite para la categoría de ${categoryLimit}/persona`,
overLimit: ({amount}: ViolationsOverLimitParams) => `Importe supera el límite de ${amount}/persona`,
@@ -2604,22 +2598,22 @@ export default {
rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => {
if (brokenBankConnection) {
return isAdmin
- ? `No se puede adjuntar recibo debido a una conexión con su banco que ${email} necesita arreglar`
- : 'No se puede adjuntar recibo debido a una conexión con su banco que necesitas arreglar';
+ ? `No se puede adjuntar recibo debido a un problema con la conexión a su banco que ${email} necesita arreglar`
+ : 'No se puede adjuntar recibo debido a un problema con la conexión a su banco que necesitas arreglar';
}
if (!isTransactionOlderThan7Days) {
return isAdmin
- ? `Pídele a ${member} que marque la transacción como efectivo o espera 7 días e intenta de nuevo`
- : 'Esperando adjuntar automáticamente a transacción de tarjeta de crédito';
+ ? `Pide a ${member} que marque la transacción como efectivo o espera 7 días e inténtalo de nuevo`
+ : 'Esperando a adjuntar automáticamente la transacción de tarjeta de crédito';
}
return '';
},
smartscanFailed: 'No se pudo escanear el recibo. Introduce los datos manualmente',
someTagLevelsRequired: 'Falta etiqueta',
- tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `Le etiqueta ${tagName} ya no es válida`,
+ tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `La etiqueta ${tagName} ya no es válida`,
taxAmountChanged: 'El importe del impuesto fue modificado',
taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName} ya no es válido`,
taxRateChanged: 'La tasa de impuesto fue modificada',
- taxRequired: 'Falta tasa de impuesto',
+ taxRequired: 'Falta la tasa de impuesto',
},
} satisfies EnglishTranslation;
diff --git a/src/languages/types.ts b/src/languages/types.ts
index 3185b7a8f6f1..11adf01ac252 100644
--- a/src/languages/types.ts
+++ b/src/languages/types.ts
@@ -287,6 +287,10 @@ type TranslationFlatObject = {
[TKey in TranslationPaths]: TranslateType;
};
+type TermsParams = {amount: string};
+
+type ElectronicFundsParams = {percentage: string; amount: string};
+
export type {
ApprovedAmountParams,
AddressLineParams,
@@ -305,6 +309,7 @@ export type {
DeleteConfirmationParams,
DidSplitAmountMessageParams,
EditActionParams,
+ ElectronicFundsParams,
EnglishTranslation,
EnterMagicCodeParams,
FormattedMaxLengthParams,
@@ -353,6 +358,7 @@ export type {
StepCounterParams,
TagSelectionParams,
TaskCreatedActionParams,
+ TermsParams,
ThreadRequestReportNameParams,
ThreadSentMoneyReportNameParams,
ToValidateLoginParams,
diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.ts
similarity index 78%
rename from src/libs/ComposerFocusManager.js
rename to src/libs/ComposerFocusManager.ts
index 569e165da962..b66bbe92599e 100644
--- a/src/libs/ComposerFocusManager.js
+++ b/src/libs/ComposerFocusManager.ts
@@ -1,18 +1,20 @@
let isReadyToFocusPromise = Promise.resolve();
-let resolveIsReadyToFocus;
+let resolveIsReadyToFocus: (value: void | PromiseLike) => void;
function resetReadyToFocus() {
isReadyToFocusPromise = new Promise((resolve) => {
resolveIsReadyToFocus = resolve;
});
}
+
function setReadyToFocus() {
if (!resolveIsReadyToFocus) {
return;
}
resolveIsReadyToFocus();
}
-function isReadyToFocus() {
+
+function isReadyToFocus(): Promise {
return isReadyToFocusPromise;
}
diff --git a/src/libs/ControlSelection/index.ts b/src/libs/ControlSelection/index.ts
index ab11e66bc369..44787dc77dbe 100644
--- a/src/libs/ControlSelection/index.ts
+++ b/src/libs/ControlSelection/index.ts
@@ -1,4 +1,3 @@
-import type CustomRefObject from '@src/types/utils/CustomRefObject';
import type ControlSelectionModule from './types';
/**
@@ -20,25 +19,25 @@ function unblock() {
/**
* Block selection on particular element
*/
-function blockElement(ref?: CustomRefObject | null) {
- if (!ref) {
+function blockElement(element?: HTMLElement | null) {
+ if (!element) {
return;
}
// eslint-disable-next-line no-param-reassign
- ref.onselectstart = () => false;
+ element.onselectstart = () => false;
}
/**
* Unblock selection on particular element
*/
-function unblockElement(ref?: CustomRefObject | null) {
- if (!ref) {
+function unblockElement(element?: HTMLElement | null) {
+ if (!element) {
return;
}
// eslint-disable-next-line no-param-reassign
- ref.onselectstart = () => true;
+ element.onselectstart = () => true;
}
const ControlSelection: ControlSelectionModule = {
diff --git a/src/libs/ControlSelection/types.ts b/src/libs/ControlSelection/types.ts
index fc0b488577ec..c4ca4b713b9b 100644
--- a/src/libs/ControlSelection/types.ts
+++ b/src/libs/ControlSelection/types.ts
@@ -1,10 +1,8 @@
-import type CustomRefObject from '@src/types/utils/CustomRefObject';
-
type ControlSelectionModule = {
block: () => void;
unblock: () => void;
- blockElement: (ref?: CustomRefObject | null) => void;
- unblockElement: (ref?: CustomRefObject | null) => void;
+ blockElement: (element?: HTMLElement | null) => void;
+ unblockElement: (element?: HTMLElement | null) => void;
};
export default ControlSelectionModule;
diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts
index 1a10eb03a00e..526769723531 100644
--- a/src/libs/DateUtils.ts
+++ b/src/libs/DateUtils.ts
@@ -5,9 +5,12 @@ import {
eachDayOfInterval,
eachMonthOfInterval,
endOfDay,
+ endOfMonth,
endOfWeek,
format,
formatDistanceToNow,
+ getDate,
+ getDay,
getDayOfYear,
isAfter,
isBefore,
@@ -730,6 +733,25 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone {
};
}
+/**
+ * Returns the last business day of given date month
+ *
+ * param {Date} inputDate
+ * returns {number}
+ */
+function getLastBusinessDayOfMonth(inputDate: Date): number {
+ let currentDate = endOfMonth(inputDate);
+ const dayOfWeek = getDay(currentDate);
+
+ if (dayOfWeek === 0) {
+ currentDate = subDays(currentDate, 2);
+ } else if (dayOfWeek === 6) {
+ currentDate = subDays(currentDate, 1);
+ }
+
+ return getDate(currentDate);
+}
+
const DateUtils = {
formatToDayOfWeek,
formatToLongDateWithWeekday,
@@ -774,6 +796,7 @@ const DateUtils = {
getWeekEndsOn,
isTimeAtLeastOneMinuteInFuture,
formatToSupportedTimezone,
+ getLastBusinessDayOfMonth,
};
export default DateUtils;
diff --git a/src/libs/Environment/betaChecker/index.android.ts b/src/libs/Environment/betaChecker/index.android.ts
index 4b912e0daaa5..aeb1527457f7 100644
--- a/src/libs/Environment/betaChecker/index.android.ts
+++ b/src/libs/Environment/betaChecker/index.android.ts
@@ -1,6 +1,6 @@
import Onyx from 'react-native-onyx';
import semver from 'semver';
-import * as AppUpdate from '@libs/actions/AppUpdate';
+import * as AppUpdate from '@userActions/AppUpdate';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import pkg from '../../../../package.json';
diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts
index 16afc377bba3..22e342ac847b 100644
--- a/src/libs/HttpUtils.ts
+++ b/src/libs/HttpUtils.ts
@@ -6,7 +6,6 @@ import ONYXKEYS from '@src/ONYXKEYS';
import type {RequestType} from '@src/types/onyx/Request';
import type Response from '@src/types/onyx/Response';
import * as NetworkActions from './actions/Network';
-import * as UpdateRequired from './actions/UpdateRequired';
import * as ApiUtils from './ApiUtils';
import HttpsError from './Errors/HttpsError';
@@ -129,10 +128,6 @@ function processHTTPRequest(url: string, method: RequestType = 'get', body: Form
alert('Too many auth writes', message);
}
}
- if (response.jsonCode === CONST.JSON_CODE.UPDATE_REQUIRED) {
- // Trigger a modal and disable the app as the user needs to upgrade to the latest minimum version to continue
- UpdateRequired.alertUser();
- }
return response as Promise;
});
}
diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts
index 0c3f3ec60203..e65bd3d0021f 100644
--- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts
+++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts
@@ -2,9 +2,9 @@
import Str from 'expensify-common/lib/str';
import type {ImageSourcePropType} from 'react-native';
import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.png';
-import * as AppUpdate from '@libs/actions/AppUpdate';
import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage';
import * as ReportUtils from '@libs/ReportUtils';
+import * as AppUpdate from '@userActions/AppUpdate';
import type {Report, ReportAction} from '@src/types/onyx';
import focusApp from './focusApp';
import type {LocalNotificationClickHandler, LocalNotificationData} from './types';
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 33290b5046f0..1d888b087e53 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -397,6 +397,7 @@ type OptionData = {
parentReportAction?: OnyxEntry;
displayNamesWithTooltips?: DisplayNameWithTooltips | null;
descriptiveText?: string;
+ notificationPreference?: NotificationPreference | null;
isDisabled?: boolean | null;
name?: string | null;
} & Report;
@@ -3789,7 +3790,7 @@ function shouldReportBeInOptionList({
// All unread chats (even archived ones) in GSD mode will be shown. This is because GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones
if (isInGSDMode) {
- return isUnread(report);
+ return isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE;
}
// Archived reports should always be shown when in default (most recent) mode. This is because you should still be able to access and search for the chats to find them.
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 4a2c4a2da22a..ddd0365e865f 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -147,6 +147,7 @@ function getOrderedReportIDs(
const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD;
const isInDefaultMode = !isInGSDMode;
const allReportsDictValues = Object.values(allReports);
+
// Filter out all the reports that shouldn't be displayed
let reportsToDisplay = allReportsDictValues.filter((report) => {
const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`;
diff --git a/src/libs/SuggestionUtils.js b/src/libs/SuggestionUtils.ts
similarity index 70%
rename from src/libs/SuggestionUtils.js
rename to src/libs/SuggestionUtils.ts
index 338f3b455431..96379ce49ef3 100644
--- a/src/libs/SuggestionUtils.js
+++ b/src/libs/SuggestionUtils.ts
@@ -2,21 +2,14 @@ import CONST from '@src/CONST';
/**
* Trims first character of the string if it is a space
- * @param {String} str
- * @returns {String}
*/
-function trimLeadingSpace(str) {
- return str.slice(0, 1) === ' ' ? str.slice(1) : str;
+function trimLeadingSpace(str: string): string {
+ return str.startsWith(' ') ? str.slice(1) : str;
}
-
/**
* Checks if space is available to render large suggestion menu
- * @param {Number} listHeight
- * @param {Number} composerHeight
- * @param {Number} totalSuggestions
- * @returns {Boolean}
*/
-function hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, totalSuggestions) {
+function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight: number, totalSuggestions: number): boolean {
const maxSuggestions = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER;
const chatFooterHeight = CONST.CHAT_FOOTER_SECONDARY_ROW_HEIGHT + 2 * CONST.CHAT_FOOTER_SECONDARY_ROW_PADDING;
const availableHeight = listHeight - composerHeight - chatFooterHeight;
diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts
index 2a7019686308..b4f3cd34a8c4 100644
--- a/src/libs/UnreadIndicatorUpdater/index.ts
+++ b/src/libs/UnreadIndicatorUpdater/index.ts
@@ -26,8 +26,12 @@ export default function getUnreadReportsForUnreadIndicator(reports: OnyxCollecti
* Chats with hidden preference remain invisible in the LHN and are not considered "unread."
* They are excluded from the LHN rendering, but not filtered from the "option list."
* This ensures they appear in Search, but not in the LHN or unread count.
+ *
+ * Furthermore, muted reports may or may not appear in the LHN depending on priority mode,
+ * but they should not be considered in the unread indicator count.
*/
- report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN,
+ report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN &&
+ report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE,
);
}
diff --git a/src/libs/actions/AppUpdate/index.ts b/src/libs/actions/AppUpdate.ts
similarity index 71%
rename from src/libs/actions/AppUpdate/index.ts
rename to src/libs/actions/AppUpdate.ts
index 69c80a089831..29ee2a4547ab 100644
--- a/src/libs/actions/AppUpdate/index.ts
+++ b/src/libs/actions/AppUpdate.ts
@@ -1,6 +1,5 @@
import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
-import updateApp from './updateApp';
function triggerUpdateAvailable() {
Onyx.set(ONYXKEYS.UPDATE_AVAILABLE, true);
@@ -10,4 +9,4 @@ function setIsAppInBeta(isBeta: boolean) {
Onyx.set(ONYXKEYS.IS_BETA, isBeta);
}
-export {triggerUpdateAvailable, setIsAppInBeta, updateApp};
+export {triggerUpdateAvailable, setIsAppInBeta};
diff --git a/src/libs/actions/AppUpdate/updateApp/index.android.ts b/src/libs/actions/AppUpdate/updateApp/index.android.ts
deleted file mode 100644
index f6a6387a8aef..000000000000
--- a/src/libs/actions/AppUpdate/updateApp/index.android.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import * as Link from '@userActions/Link';
-import CONST from '@src/CONST';
-
-export default function updateApp() {
- Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.ANDROID);
-}
diff --git a/src/libs/actions/AppUpdate/updateApp/index.desktop.ts b/src/libs/actions/AppUpdate/updateApp/index.desktop.ts
deleted file mode 100644
index fb3a7d649baa..000000000000
--- a/src/libs/actions/AppUpdate/updateApp/index.desktop.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import {Linking} from 'react-native';
-import CONST from '@src/CONST';
-
-export default function updateApp() {
- Linking.openURL(CONST.APP_DOWNLOAD_LINKS.DESKTOP);
-}
diff --git a/src/libs/actions/AppUpdate/updateApp/index.ios.ts b/src/libs/actions/AppUpdate/updateApp/index.ios.ts
deleted file mode 100644
index 8b66521bb9c8..000000000000
--- a/src/libs/actions/AppUpdate/updateApp/index.ios.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import * as Link from '@userActions/Link';
-import CONST from '@src/CONST';
-
-export default function updateApp() {
- Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.IOS);
-}
diff --git a/src/libs/actions/AppUpdate/updateApp/index.ts b/src/libs/actions/AppUpdate/updateApp/index.ts
deleted file mode 100644
index 8c2b191029a2..000000000000
--- a/src/libs/actions/AppUpdate/updateApp/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-/**
- * On web or mWeb we can simply refresh the page and the user should have the new version of the app downloaded.
- */
-export default function updateApp() {
- window.location.reload();
-}
diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts
index cbbc00dd42fc..b47891e64350 100644
--- a/src/libs/actions/Policy.ts
+++ b/src/libs/actions/Policy.ts
@@ -200,6 +200,7 @@ function deleteWorkspace(policyID: string, reports: Report[], policyName: string
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
value: {
+ avatar: '',
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE,
errors: null,
},
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 228b88d194ba..36ac445a78d4 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -934,7 +934,7 @@ function expandURLPreview(reportID: string, reportActionID: string) {
}
/** Marks the new report actions as read */
-function readNewestAction(reportID: string) {
+function readNewestAction(reportID: string, shouldEmitEvent = true) {
const lastReadTime = DateUtils.getDBTime();
const optimisticData: OnyxUpdate[] = [
@@ -958,6 +958,11 @@ function readNewestAction(reportID: string) {
};
API.write('ReadNewestAction', parameters, {optimisticData});
+
+ if (!shouldEmitEvent) {
+ return;
+ }
+
DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime);
}
diff --git a/src/libs/actions/UpdateRequired.ts b/src/libs/actions/UpdateRequired.ts
deleted file mode 100644
index 26f0a119ac8d..000000000000
--- a/src/libs/actions/UpdateRequired.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import Onyx from 'react-native-onyx';
-import getEnvironment from '@libs/Environment/getEnvironment';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-
-function alertUser() {
- // For now, we will pretty much never have to do this on a platform other than production.
- // We should only update the minimum app version in the API after all platforms of a new version have been deployed to PRODUCTION.
- // As staging is always ahead of production there is no reason to "force update" those apps.
- getEnvironment().then((environment) => {
- if (environment !== CONST.ENVIRONMENT.PRODUCTION) {
- return;
- }
-
- Onyx.set(ONYXKEYS.UPDATE_REQUIRED, true);
- });
-}
-
-export {
- // eslint-disable-next-line import/prefer-default-export
- alertUser,
-};
diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts
index 66966b7b504c..3b6617aa3ed0 100644
--- a/src/libs/calculateAnchorPosition.ts
+++ b/src/libs/calculateAnchorPosition.ts
@@ -22,7 +22,7 @@ export default function calculateAnchorPosition(anchorComponent: View, anchorOri
if (anchorOrigin?.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP && anchorOrigin?.horizontal === CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT) {
return resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0)});
}
- return resolve({horizontal: x + width, vertical: y});
+ return resolve({horizontal: x + width, vertical: y + (anchorOrigin?.shiftVertical ?? 0)});
});
});
}
diff --git a/src/libs/migrations/PersonalDetailsByAccountID.js b/src/libs/migrations/PersonalDetailsByAccountID.js
index 24aece8f5a97..c08ec6fb2c43 100644
--- a/src/libs/migrations/PersonalDetailsByAccountID.js
+++ b/src/libs/migrations/PersonalDetailsByAccountID.js
@@ -251,6 +251,12 @@ export default function () {
delete newReport.lastActorEmail;
}
+ if (lodashHas(newReport, ['participants'])) {
+ reportWasModified = true;
+ Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing participants from report ${newReport.reportID}`);
+ delete newReport.participants;
+ }
+
if (lodashHas(newReport, ['ownerEmail'])) {
reportWasModified = true;
Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing ownerEmail from report ${newReport.reportID}`);
diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js
index b29cb0c777f7..fad19c5ecf6f 100644
--- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js
+++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js
@@ -6,97 +6,100 @@ import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
+import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as Localize from '@libs/Localize';
+import * as CurrencyUtils from '@libs/CurrencyUtils';
import CONST from '@src/CONST';
-const termsData = [
- {
- title: Localize.translateLocal('termsStep.longTermsForm.openingAccountTitle'),
- rightText: Localize.translateLocal('termsStep.feeAmountZero'),
- details: Localize.translateLocal('termsStep.longTermsForm.openingAccountDetails'),
- },
- {
- title: Localize.translateLocal('termsStep.monthlyFee'),
- rightText: Localize.translateLocal('termsStep.feeAmountZero'),
- details: Localize.translateLocal('termsStep.longTermsForm.monthlyFeeDetails'),
- },
- {
- title: Localize.translateLocal('termsStep.longTermsForm.customerServiceTitle'),
- subTitle: Localize.translateLocal('termsStep.longTermsForm.automated'),
- rightText: Localize.translateLocal('termsStep.feeAmountZero'),
- details: Localize.translateLocal('termsStep.longTermsForm.customerServiceDetails'),
- },
- {
- title: Localize.translateLocal('termsStep.longTermsForm.customerServiceTitle'),
- subTitle: Localize.translateLocal('termsStep.longTermsForm.liveAgent'),
- rightText: Localize.translateLocal('termsStep.feeAmountZero'),
- details: Localize.translateLocal('termsStep.longTermsForm.customerServiceDetails'),
- },
- {
- title: Localize.translateLocal('termsStep.inactivity'),
- rightText: Localize.translateLocal('termsStep.feeAmountZero'),
- details: Localize.translateLocal('termsStep.longTermsForm.inactivityDetails'),
- },
- {
- title: Localize.translateLocal('termsStep.longTermsForm.sendingFundsTitle'),
- rightText: Localize.translateLocal('termsStep.feeAmountZero'),
- details: Localize.translateLocal('termsStep.longTermsForm.sendingFundsDetails'),
- },
- {
- title: Localize.translateLocal('termsStep.electronicFundsWithdrawal'),
- subTitle: Localize.translateLocal('termsStep.standard'),
- rightText: Localize.translateLocal('termsStep.feeAmountZero'),
- details: Localize.translateLocal('termsStep.longTermsForm.electronicFundsStandardDetails'),
- },
- {
- title: Localize.translateLocal('termsStep.electronicFundsWithdrawal'),
- subTitle: Localize.translateLocal('termsStep.longTermsForm.instant'),
- rightText: Localize.translateLocal('termsStep.electronicFundsInstantFee'),
- subRightText: Localize.translateLocal('termsStep.longTermsForm.electronicFundsInstantFeeMin'),
- details: Localize.translateLocal('termsStep.longTermsForm.electronicFundsInstantDetails'),
- },
-];
+function LongTermsForm() {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {translate, numberFormat} = useLocalize();
-const getLongTermsSections = (styles) =>
- _.map(termsData, (section, index) => (
- // eslint-disable-next-line react/no-array-index-key
-
-
-
- {section.title}
- {Boolean(section.subTitle) && {section.subTitle}}
-
-
- {section.rightText}
- {Boolean(section.subRightText) && {section.subRightText}}
+ const termsData = [
+ {
+ title: translate('termsStep.longTermsForm.openingAccountTitle'),
+ rightText: CurrencyUtils.convertToDisplayString(0, 'USD'),
+ details: translate('termsStep.longTermsForm.openingAccountDetails'),
+ },
+ {
+ title: translate('termsStep.monthlyFee'),
+ rightText: CurrencyUtils.convertToDisplayString(0, 'USD'),
+ details: translate('termsStep.longTermsForm.monthlyFeeDetails'),
+ },
+ {
+ title: translate('termsStep.longTermsForm.customerServiceTitle'),
+ subTitle: translate('termsStep.longTermsForm.automated'),
+ rightText: CurrencyUtils.convertToDisplayString(0, 'USD'),
+ details: translate('termsStep.longTermsForm.customerServiceDetails'),
+ },
+ {
+ title: translate('termsStep.longTermsForm.customerServiceTitle'),
+ subTitle: translate('termsStep.longTermsForm.liveAgent'),
+ rightText: CurrencyUtils.convertToDisplayString(0, 'USD'),
+ details: translate('termsStep.longTermsForm.customerServiceDetails'),
+ },
+ {
+ title: translate('termsStep.inactivity'),
+ rightText: CurrencyUtils.convertToDisplayString(0, 'USD'),
+ details: translate('termsStep.longTermsForm.inactivityDetails'),
+ },
+ {
+ title: translate('termsStep.longTermsForm.sendingFundsTitle'),
+ rightText: CurrencyUtils.convertToDisplayString(0, 'USD'),
+ details: translate('termsStep.longTermsForm.sendingFundsDetails'),
+ },
+ {
+ title: translate('termsStep.electronicFundsWithdrawal'),
+ subTitle: translate('termsStep.standard'),
+ rightText: CurrencyUtils.convertToDisplayString(0, 'USD'),
+ details: translate('termsStep.longTermsForm.electronicFundsStandardDetails'),
+ },
+ {
+ title: translate('termsStep.electronicFundsWithdrawal'),
+ subTitle: translate('termsStep.longTermsForm.instant'),
+ rightText: `${numberFormat(1.5)}%`,
+ subRightText: translate('termsStep.longTermsForm.electronicFundsInstantFeeMin', {amount: CurrencyUtils.convertToDisplayString(25, 'USD')}),
+ details: translate('termsStep.longTermsForm.electronicFundsInstantDetails', {percentage: numberFormat(1.5), amount: CurrencyUtils.convertToDisplayString(25, 'USD')}),
+ },
+ ];
+
+ const getLongTermsSections = () =>
+ _.map(termsData, (section, index) => (
+ // eslint-disable-next-line react/no-array-index-key
+
+
+
+ {section.title}
+ {Boolean(section.subTitle) && {section.subTitle}}
+
+
+ {section.rightText}
+ {Boolean(section.subRightText) && {section.subRightText}}
+
+ {section.details}
- {section.details}
-
- ));
+ ));
-function LongTermsForm() {
- const theme = useTheme();
- const styles = useThemeStyles();
return (
<>
- {getLongTermsSections(styles)}
+ {getLongTermsSections()}
- {Localize.translateLocal('termsStep.longTermsForm.fdicInsuranceBancorp')} {CONST.TERMS.FDIC_PREPAID}{' '}
- {Localize.translateLocal('termsStep.longTermsForm.fdicInsuranceBancorp2')}
+ {translate('termsStep.longTermsForm.fdicInsuranceBancorp', {amount: CurrencyUtils.convertToDisplayString(25000000, 'USD')})} {CONST.TERMS.FDIC_PREPAID}{' '}
+ {translate('termsStep.longTermsForm.fdicInsuranceBancorp2')}
- {Localize.translateLocal('termsStep.noOverdraftOrCredit')}
+ {translate('termsStep.noOverdraftOrCredit')}
- {Localize.translateLocal('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE}{' '}
- {Localize.translateLocal('termsStep.longTermsForm.contactExpensifyPayments2')} {CONST.NEW_EXPENSIFY_URL}.
+ {translate('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE} {translate('termsStep.longTermsForm.contactExpensifyPayments2')}{' '}
+ {CONST.NEW_EXPENSIFY_URL}.
- {Localize.translateLocal('termsStep.longTermsForm.generalInformation')} {CONST.TERMS.CFPB_PREPAID}
+ {translate('termsStep.longTermsForm.generalInformation')} {CONST.TERMS.CFPB_PREPAID}
{'. '}
- {Localize.translateLocal('termsStep.longTermsForm.generalInformation2')} {CONST.TERMS.CFPB_COMPLAINT}.
+ {translate('termsStep.longTermsForm.generalInformation2')} {CONST.TERMS.CFPB_COMPLAINT}.
@@ -109,7 +112,7 @@ function LongTermsForm() {
style={styles.ml1}
href={CONST.FEES_URL}
>
- {Localize.translateLocal('termsStep.longTermsForm.printerFriendlyView')}
+ {translate('termsStep.longTermsForm.printerFriendlyView')}
>
diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js
index 77f77f3cb34b..40824f47b036 100644
--- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js
+++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js
@@ -2,8 +2,9 @@ import React from 'react';
import {View} from 'react-native';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
+import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as Localize from '@libs/Localize';
+import * as CurrencyUtils from '@libs/CurrencyUtils';
import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes';
import CONST from '@src/CONST';
@@ -18,10 +19,11 @@ const defaultProps = {
function ShortTermsForm(props) {
const styles = useThemeStyles();
+ const {translate, numberFormat} = useLocalize();
return (
<>
- {Localize.translateLocal('termsStep.shortTermsForm.expensifyPaymentsAccount', {
+ {translate('termsStep.shortTermsForm.expensifyPaymentsAccount', {
walletProgram:
props.userWallet.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK,
})}
@@ -31,19 +33,19 @@ function ShortTermsForm(props) {
- {Localize.translateLocal('termsStep.monthlyFee')}
+ {translate('termsStep.monthlyFee')}
- {Localize.translateLocal('termsStep.feeAmountZero')}
+ {CurrencyUtils.convertToDisplayString(0, 'USD')}
- {Localize.translateLocal('termsStep.shortTermsForm.perPurchase')}
+ {translate('termsStep.shortTermsForm.perPurchase')}
- {Localize.translateLocal('termsStep.feeAmountZero')}
+ {CurrencyUtils.convertToDisplayString(0, 'USD')}
@@ -52,28 +54,28 @@ function ShortTermsForm(props) {
- {Localize.translateLocal('termsStep.shortTermsForm.atmWithdrawal')}
+ {translate('termsStep.shortTermsForm.atmWithdrawal')}
- {Localize.translateLocal('common.na')}
+ {translate('common.na')}
- {Localize.translateLocal('termsStep.shortTermsForm.inNetwork')}
+ {translate('termsStep.shortTermsForm.inNetwork')}
- {Localize.translateLocal('common.na')}
+ {translate('common.na')}
- {Localize.translateLocal('termsStep.shortTermsForm.outOfNetwork')}
+ {translate('termsStep.shortTermsForm.outOfNetwork')}
- {Localize.translateLocal('termsStep.shortTermsForm.cashReload')}
+ {translate('termsStep.shortTermsForm.cashReload')}
- {Localize.translateLocal('common.na')}
+ {translate('common.na')}
@@ -83,11 +85,11 @@ function ShortTermsForm(props) {
- {Localize.translateLocal('termsStep.shortTermsForm.atmBalanceInquiry')} {Localize.translateLocal('termsStep.shortTermsForm.inOrOutOfNetwork')}
+ {translate('termsStep.shortTermsForm.atmBalanceInquiry')} {translate('termsStep.shortTermsForm.inOrOutOfNetwork')}
- {Localize.translateLocal('common.na')}
+ {translate('common.na')}
@@ -95,11 +97,11 @@ function ShortTermsForm(props) {
- {Localize.translateLocal('termsStep.shortTermsForm.customerService')} {Localize.translateLocal('termsStep.shortTermsForm.automatedOrLive')}
+ {translate('termsStep.shortTermsForm.customerService')} {translate('termsStep.shortTermsForm.automatedOrLive')}
- {Localize.translateLocal('termsStep.feeAmountZero')}
+ {CurrencyUtils.convertToDisplayString(0, 'USD')}
@@ -107,40 +109,40 @@ function ShortTermsForm(props) {
- {Localize.translateLocal('termsStep.inactivity')} {Localize.translateLocal('termsStep.shortTermsForm.afterTwelveMonths')}
+ {translate('termsStep.inactivity')} {translate('termsStep.shortTermsForm.afterTwelveMonths')}
- {Localize.translateLocal('termsStep.feeAmountZero')}
+ {CurrencyUtils.convertToDisplayString(0, 'USD')}
- {Localize.translateLocal('termsStep.shortTermsForm.weChargeOneFee')}
+ {translate('termsStep.shortTermsForm.weChargeOneFee')}
- {Localize.translateLocal('termsStep.electronicFundsWithdrawal')} {Localize.translateLocal('termsStep.shortTermsForm.instant')}
+ {translate('termsStep.electronicFundsWithdrawal')} {translate('termsStep.shortTermsForm.instant')}
- {Localize.translateLocal('termsStep.electronicFundsInstantFee')}
- {Localize.translateLocal('termsStep.shortTermsForm.electronicFundsInstantFeeMin')}
+ {numberFormat(1.5)}%
+ {translate('termsStep.shortTermsForm.electronicFundsInstantFeeMin', {amount: CurrencyUtils.convertToDisplayString(25, 'USD')})}
- {Localize.translateLocal('termsStep.noOverdraftOrCredit')}
- {Localize.translateLocal('termsStep.shortTermsForm.fdicInsurance')}
+ {translate('termsStep.noOverdraftOrCredit')}
+ {translate('termsStep.shortTermsForm.fdicInsurance')}
- {Localize.translateLocal('termsStep.shortTermsForm.generalInfo')} {CONST.TERMS.CFPB_PREPAID}.
+ {translate('termsStep.shortTermsForm.generalInfo')} {CONST.TERMS.CFPB_PREPAID}.
- {Localize.translateLocal('termsStep.shortTermsForm.conditionsDetails')} {CONST.TERMS.USE_EXPENSIFY_FEES}{' '}
- {Localize.translateLocal('termsStep.shortTermsForm.conditionsPhone')}
+ {translate('termsStep.shortTermsForm.conditionsDetails')} {CONST.TERMS.USE_EXPENSIFY_FEES}{' '}
+ {translate('termsStep.shortTermsForm.conditionsPhone')}
diff --git a/src/pages/ErrorPage/UpdateRequiredView.tsx b/src/pages/ErrorPage/UpdateRequiredView.tsx
deleted file mode 100644
index 2a73215d2293..000000000000
--- a/src/pages/ErrorPage/UpdateRequiredView.tsx
+++ /dev/null
@@ -1,60 +0,0 @@
-import React from 'react';
-import {View} from 'react-native';
-import Button from '@components/Button';
-import Header from '@components/Header';
-import HeaderGap from '@components/HeaderGap';
-import Lottie from '@components/Lottie';
-import LottieAnimations from '@components/LottieAnimations';
-import Text from '@components/Text';
-import useLocalize from '@hooks/useLocalize';
-import useSafeAreaInsets from '@hooks/useSafeAreaInsets';
-import useStyleUtils from '@hooks/useStyleUtils';
-import useThemeStyles from '@hooks/useThemeStyles';
-import useWindowDimensions from '@hooks/useWindowDimensions';
-import * as AppUpdate from '@libs/actions/AppUpdate';
-
-function UpdateRequiredView() {
- const insets = useSafeAreaInsets();
- const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
- const {translate} = useLocalize();
- const {isSmallScreenWidth} = useWindowDimensions();
- return (
-
-
-
-
-
-
-
-
-
-
- {translate('updateRequiredView.pleaseInstall')}
-
-
- {translate('updateRequiredView.toGetLatestChanges')}
-
-
-
-
-
- );
-}
-
-UpdateRequiredView.displayName = 'UpdateRequiredView';
-export default UpdateRequiredView;
diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js
index b3de1eb53548..3f55ad6c0ff7 100644
--- a/src/pages/RoomInvitePage.js
+++ b/src/pages/RoomInvitePage.js
@@ -1,3 +1,4 @@
+import {useNavigation} from '@react-navigation/native';
import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
@@ -71,6 +72,8 @@ function RoomInvitePage(props) {
const [selectedOptions, setSelectedOptions] = useState([]);
const [personalDetails, setPersonalDetails] = useState([]);
const [userToInvite, setUserToInvite] = useState(null);
+ const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
+ const navigation = useNavigation();
// Any existing participants and Expensify emails should not be eligible for invitation
const excludedUsers = useMemo(
@@ -95,10 +98,26 @@ function RoomInvitePage(props) {
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change
}, [props.personalDetails, props.betas, searchTerm, excludedUsers]);
- const getSections = () => {
- const sections = [];
+ useEffect(() => {
+ const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => {
+ setDidScreenTransitionEnd(true);
+ });
+
+ return () => {
+ unsubscribeTransitionEnd();
+ };
+ // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const sections = useMemo(() => {
+ const sectionsArr = [];
let indexOffset = 0;
+ if (!didScreenTransitionEnd) {
+ return [];
+ }
+
// Filter all options that is a part of the search term or in the personal details
let filterSelectedOptions = selectedOptions;
if (searchTerm !== '') {
@@ -112,7 +131,7 @@ function RoomInvitePage(props) {
});
}
- sections.push({
+ sectionsArr.push({
title: undefined,
data: filterSelectedOptions,
shouldShow: true,
@@ -126,7 +145,7 @@ function RoomInvitePage(props) {
const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail, false));
const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login);
- sections.push({
+ sectionsArr.push({
title: translate('common.contacts'),
data: personalDetailsFormatted,
shouldShow: !_.isEmpty(personalDetailsFormatted),
@@ -135,7 +154,7 @@ function RoomInvitePage(props) {
indexOffset += personalDetailsFormatted.length;
if (hasUnselectedUserToInvite) {
- sections.push({
+ sectionsArr.push({
title: undefined,
data: [OptionsListUtils.formatMemberForList(userToInvite, false)],
shouldShow: true,
@@ -143,8 +162,8 @@ function RoomInvitePage(props) {
});
}
- return sections;
- };
+ return sectionsArr;
+ }, [personalDetails, searchTerm, selectedOptions, translate, userToInvite, didScreenTransitionEnd]);
const toggleOption = useCallback(
(option) => {
@@ -213,49 +232,43 @@ function RoomInvitePage(props) {
shouldEnableMaxHeight
testID={RoomInvitePage.displayName}
>
- {({didScreenTransitionEnd}) => {
- const sections = didScreenTransitionEnd ? getSections() : [];
-
- return (
- Navigation.goBack(backRoute)}
- >
- {
- Navigation.goBack(backRoute);
- }}
- />
-
-
-
-
-
- );
- }}
+ Navigation.goBack(backRoute)}
+ >
+ {
+ Navigation.goBack(backRoute);
+ }}
+ />
+
+
+
+
+
);
}
diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js
index c072666920ae..c52b8ec6760a 100644
--- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js
+++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js
@@ -354,6 +354,13 @@ function ReportActionCompose({
runOnJS(submitForm)();
}, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]);
+ const emojiShiftVertical = useMemo(() => {
+ const chatItemComposeSecondaryRowHeight = styles.chatItemComposeSecondaryRow.height + styles.chatItemComposeSecondaryRow.marginTop + styles.chatItemComposeSecondaryRow.marginBottom;
+ const reportActionComposeHeight = styles.chatItemComposeBox.minHeight + chatItemComposeSecondaryRowHeight;
+ const emojiOffsetWithComposeBox = (styles.chatItemComposeBox.minHeight - styles.chatItemEmojiButton.height) / 2;
+ return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM;
+ }, [styles]);
+
return (
@@ -453,6 +460,7 @@ function ReportActionCompose({
onModalHide={focus}
onEmojiSelected={(...args) => composerRef.current.replaceSelectionWithText(...args)}
emojiPickerID={report.reportID}
+ shiftVertical={emojiShiftVertical}
/>
)}
Report.navigateToConciergeChatAndDeleteReport(props.report.reportID)}
- needsOffscreenAlphaCompositing
- >
-
-
-
- ReportUtils.navigateToDetailsPage(props.report)}
- style={[styles.mh5, styles.mb3, styles.alignSelfStart]}
- accessibilityLabel={props.translate('common.details')}
- role={CONST.ROLE.BUTTON}
- disabled={shouldDisableDetailPage}
- >
-
-
-
-
-
-
-
-
- );
-}
-
-ReportActionItemCreated.defaultProps = defaultProps;
-ReportActionItemCreated.propTypes = propTypes;
-ReportActionItemCreated.displayName = 'ReportActionItemCreated';
-
-export default compose(
- withWindowDimensions,
- withLocalize,
- withOnyx({
- report: {
- key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
- selector: reportWithoutHasDraftSelector,
- },
- personalDetails: {
- key: ONYXKEYS.PERSONAL_DETAILS_LIST,
- },
- policy: {
- key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
- },
- }),
-)(
- memo(
- ReportActionItemCreated,
- (prevProps, nextProps) =>
- lodashGet(prevProps.props, 'policy.name') === lodashGet(nextProps, 'policy.name') &&
- lodashGet(prevProps.props, 'policy.avatar') === lodashGet(nextProps, 'policy.avatar') &&
- lodashGet(prevProps.props, 'report.lastReadTime') === lodashGet(nextProps, 'report.lastReadTime') &&
- lodashGet(prevProps.props, 'report.statusNum') === lodashGet(nextProps, 'report.statusNum') &&
- lodashGet(prevProps.props, 'report.stateNum') === lodashGet(nextProps, 'report.stateNum'),
- ),
-);
diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx
new file mode 100644
index 000000000000..82c6bebd9ba1
--- /dev/null
+++ b/src/pages/home/report/ReportActionItemCreated.tsx
@@ -0,0 +1,120 @@
+import React, {memo} from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {withOnyx} from 'react-native-onyx';
+import MultipleAvatars from '@components/MultipleAvatars';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import ReportWelcomeText from '@components/ReportWelcomeText';
+import useLocalize from '@hooks/useLocalize';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector';
+import * as ReportUtils from '@libs/ReportUtils';
+import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx';
+import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground';
+
+type ReportActionItemCreatedOnyxProps = {
+ /** The report currently being looked at */
+ report: OnyxEntry;
+
+ /** The policy object for the current route */
+ policy: OnyxEntry;
+
+ /** Personal details of all the users */
+ personalDetails: OnyxEntry;
+};
+
+type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & {
+ /** The id of the report */
+ reportID: string;
+
+ /** The id of the policy */
+ // eslint-disable-next-line react/no-unused-prop-types
+ policyID: string;
+};
+function ReportActionItemCreated(props: ReportActionItemCreatedProps) {
+ const styles = useThemeStyles();
+ const StyleUtils = useStyleUtils();
+
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth, isLargeScreenWidth} = useWindowDimensions();
+
+ if (!ReportUtils.isChatReport(props.report)) {
+ return null;
+ }
+
+ const icons = ReportUtils.getIcons(props.report, props.personalDetails);
+ const shouldDisableDetailPage = ReportUtils.shouldDisableDetailPage(props.report);
+
+ return (
+ navigateToConciergeChatAndDeleteReport(props.report?.reportID ?? props.reportID)}
+ needsOffscreenAlphaCompositing
+ >
+
+
+
+ ReportUtils.navigateToDetailsPage(props.report)}
+ style={[styles.mh5, styles.mb3, styles.alignSelfStart]}
+ accessibilityLabel={translate('common.details')}
+ role={CONST.ROLE.BUTTON}
+ disabled={shouldDisableDetailPage}
+ >
+
+
+
+
+
+
+
+
+ );
+}
+
+ReportActionItemCreated.displayName = 'ReportActionItemCreated';
+
+export default withOnyx({
+ report: {
+ key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`,
+ selector: reportWithoutHasDraftSelector,
+ },
+
+ policy: {
+ key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`,
+ },
+
+ personalDetails: {
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ },
+})(
+ memo(
+ ReportActionItemCreated,
+ (prevProps, nextProps) =>
+ prevProps.policy?.name === nextProps.policy?.name &&
+ prevProps.policy?.avatar === nextProps.policy?.avatar &&
+ prevProps.report?.stateNum === nextProps.report?.stateNum &&
+ prevProps.report?.statusNum === nextProps.report?.statusNum &&
+ prevProps.report?.lastReadTime === nextProps.report?.lastReadTime,
+ ),
+);
diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js
index 8d79e7af8dd4..ce8dcb10ef5f 100644
--- a/src/pages/home/report/ReportActionsList.js
+++ b/src/pages/home/report/ReportActionsList.js
@@ -143,6 +143,7 @@ function ReportActionsList({
const route = useRoute();
const opacity = useSharedValue(0);
const userActiveSince = useRef(null);
+ const userInactiveSince = useRef(null);
const markerInit = () => {
if (!cacheUnreadMarkers.has(report.reportID)) {
@@ -387,7 +388,7 @@ function ReportActionsList({
[currentUnreadMarker, sortedVisibleReportActions, report.reportID, messageManuallyMarkedUnread],
);
- useEffect(() => {
+ const calculateUnreadMarker = useCallback(() => {
// Iterate through the report actions and set appropriate unread marker.
// This is to avoid a warning of:
// Cannot update a component (ReportActionsList) while rendering a different component (CellRenderer).
@@ -405,7 +406,48 @@ function ReportActionsList({
if (!markerFound) {
setCurrentUnreadMarker(null);
}
- }, [sortedVisibleReportActions, report.lastReadTime, report.reportID, messageManuallyMarkedUnread, shouldDisplayNewMarker, currentUnreadMarker]);
+ }, [sortedVisibleReportActions, shouldDisplayNewMarker, currentUnreadMarker, report.reportID]);
+
+ useEffect(() => {
+ calculateUnreadMarker();
+ }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]);
+
+ const onVisibilityChange = useCallback(() => {
+ if (!Visibility.isVisible()) {
+ userInactiveSince.current = DateUtils.getDBTime();
+ return;
+ }
+ // In case the user read new messages (after being inactive) with other device we should
+ // show marker based on report.lastReadTime
+ const newMessageTimeReference = userInactiveSince.current > report.lastReadTime ? userActiveSince.current : report.lastReadTime;
+ if (
+ scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD ||
+ !(
+ sortedVisibleReportActions &&
+ _.some(
+ sortedVisibleReportActions,
+ (reportAction) =>
+ newMessageTimeReference < reportAction.created &&
+ (ReportActionsUtils.isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(),
+ )
+ )
+ ) {
+ return;
+ }
+
+ Report.readNewestAction(report.reportID, false);
+ userActiveSince.current = DateUtils.getDBTime();
+ lastReadTimeRef.current = newMessageTimeReference;
+ setCurrentUnreadMarker(null);
+ cacheUnreadMarkers.delete(report.reportID);
+ calculateUnreadMarker();
+ }, [calculateUnreadMarker, report, sortedVisibleReportActions]);
+
+ useEffect(() => {
+ const unsubscribeVisibilityListener = Visibility.onVisibilityChange(onVisibilityChange);
+
+ return unsubscribeVisibilityListener;
+ }, [onVisibilityChange]);
const renderItem = useCallback(
({item: reportAction, index}) => (
diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js
index 2758437a3962..4843a29dde60 100755
--- a/src/pages/home/report/ReportActionsView.js
+++ b/src/pages/home/report/ReportActionsView.js
@@ -87,8 +87,7 @@ function ReportActionsView(props) {
const didSubscribeToReportTypingEvents = useRef(false);
const isFirstRender = useRef(true);
const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0);
- const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions));
-
+ const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions), [props.reportActions]);
const prevNetworkRef = useRef(props.network);
const prevAuthTokenType = usePrevious(props.session.authTokenType);
diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js
index 379b010f16e7..d64734a78085 100644
--- a/src/pages/home/sidebar/SidebarLinksData.js
+++ b/src/pages/home/sidebar/SidebarLinksData.js
@@ -14,6 +14,7 @@ import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import compose from '@libs/compose';
+import * as ReportUtils from '@libs/ReportUtils';
import SidebarUtils from '@libs/SidebarUtils';
import reportPropTypes from '@pages/reportPropTypes';
import CONST from '@src/CONST';
@@ -172,6 +173,7 @@ const chatReportSelector = (report) =>
hasDraft: report.hasDraft,
isPinned: report.isPinned,
isHidden: report.isHidden,
+ notificationPreference: report.notificationPreference,
errorFields: {
addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom,
},
@@ -201,6 +203,7 @@ const chatReportSelector = (report) =>
parentReportActionID: report.parentReportActionID,
parentReportID: report.parentReportID,
isDeletedParentAction: report.isDeletedParentAction,
+ isUnreadWithMention: ReportUtils.isUnreadWithMention(report),
};
/**
diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js
index 536944f4a2d8..8775562d4476 100644
--- a/src/pages/iou/steps/MoneyRequestAmountForm.js
+++ b/src/pages/iou/steps/MoneyRequestAmountForm.js
@@ -132,9 +132,9 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward
return;
}
initializeAmount(amount);
- // we want to re-initialize the state only when the selected tab changes
+ // we want to re-initialize the state only when the selected tab or amount changes
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [selectedTab]);
+ }, [selectedTab, amount]);
/**
* Sets the selection and the amount accordingly to the value passed to the input
diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js
index 8fcea461eacd..a7e2b5ee07fb 100644
--- a/src/pages/signin/LoginForm/BaseLoginForm.js
+++ b/src/pages/signin/LoginForm/BaseLoginForm.js
@@ -276,6 +276,7 @@ function LoginForm(props) {
textContentType="username"
id="username"
name="username"
+ testID="username"
onBlur={() => {
if (firstBlurred.current || !Visibility.isVisible() || !Visibility.hasFocus()) {
return;
diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js
index 9d5b51d667ff..7686a2c542b0 100644
--- a/src/pages/signin/SignInPage.js
+++ b/src/pages/signin/SignInPage.js
@@ -251,7 +251,10 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer
return (
// Bottom SafeAreaView is removed so that login screen svg displays correctly on mobile.
// The SVG should flow under the Home Indicator on iOS.
-
+
{hasError && }
diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js
index 80813c847239..6c8476fed5cb 100644
--- a/src/pages/workspace/WorkspaceInitialPage.js
+++ b/src/pages/workspace/WorkspaceInitialPage.js
@@ -67,6 +67,15 @@ function dismissError(policyID) {
Policy.removeWorkspace(policyID);
}
+/**
+ * Whether the policy report should be archived when we delete the policy.
+ * @param {Object} report
+ * @returns {Boolean}
+ */
+function shouldArchiveReport(report) {
+ return ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report);
+}
+
function WorkspaceInitialPage(props) {
const styles = useThemeStyles();
const policy = props.policyDraft && props.policyDraft.id ? props.policyDraft : props.policy;
@@ -111,7 +120,7 @@ function WorkspaceInitialPage(props) {
* Call the delete policy and hide the modal
*/
const confirmDeleteAndHideModal = useCallback(() => {
- Policy.deleteWorkspace(policyID, policyReports, policy.name);
+ Policy.deleteWorkspace(policyID, _.filter(policyReports, shouldArchiveReport), policy.name);
setIsDeleteModalOpen(false);
// Pop the deleted workspace page before opening workspace settings.
Navigation.goBack(ROUTES.SETTINGS_WORKSPACES);
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
index 9c831ebda428..72f3747c127c 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -1,3 +1,4 @@
+import {useNavigation} from '@react-navigation/native';
import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
@@ -76,6 +77,8 @@ function WorkspaceInvitePage(props) {
const [selectedOptions, setSelectedOptions] = useState([]);
const [personalDetails, setPersonalDetails] = useState([]);
const [usersToInvite, setUsersToInvite] = useState([]);
+ const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false);
+ const navigation = useNavigation();
const openWorkspaceInvitePage = () => {
const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails);
Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs));
@@ -94,6 +97,18 @@ function WorkspaceInvitePage(props) {
// eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component
}, []);
+ useEffect(() => {
+ const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => {
+ setDidScreenTransitionEnd(true);
+ });
+
+ return () => {
+ unsubscribeTransitionEnd();
+ };
+ // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
useNetwork({onReconnect: openWorkspaceInvitePage});
const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]);
@@ -145,10 +160,14 @@ function WorkspaceInvitePage(props) {
// eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change
}, [props.personalDetails, props.policyMembers, props.betas, searchTerm, excludedUsers]);
- const getSections = () => {
- const sections = [];
+ const sections = useMemo(() => {
+ const sectionsArr = [];
let indexOffset = 0;
+ if (!didScreenTransitionEnd) {
+ return [];
+ }
+
// Filter all options that is a part of the search term or in the personal details
let filterSelectedOptions = selectedOptions;
if (searchTerm !== '') {
@@ -163,7 +182,7 @@ function WorkspaceInvitePage(props) {
});
}
- sections.push({
+ sectionsArr.push({
title: undefined,
data: filterSelectedOptions,
shouldShow: true,
@@ -176,7 +195,7 @@ function WorkspaceInvitePage(props) {
const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login));
const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, OptionsListUtils.formatMemberForList);
- sections.push({
+ sectionsArr.push({
title: translate('common.contacts'),
data: personalDetailsFormatted,
shouldShow: !_.isEmpty(personalDetailsFormatted),
@@ -188,7 +207,7 @@ function WorkspaceInvitePage(props) {
const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login);
if (hasUnselectedUserToInvite) {
- sections.push({
+ sectionsArr.push({
title: undefined,
data: [OptionsListUtils.formatMemberForList(userToInvite)],
shouldShow: true,
@@ -197,8 +216,8 @@ function WorkspaceInvitePage(props) {
}
});
- return sections;
- };
+ return sectionsArr;
+ }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]);
const toggleOption = (option) => {
Policy.clearErrors(props.route.params.policyID);
@@ -269,56 +288,50 @@ function WorkspaceInvitePage(props) {
shouldEnableMaxHeight
testID={WorkspaceInvitePage.displayName}
>
- {({didScreenTransitionEnd}) => {
- const sections = didScreenTransitionEnd ? getSections() : [];
-
- return (
- Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
- >
- {
- Policy.clearErrors(props.route.params.policyID);
- Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID));
- }}
- />
- {
- SearchInputManager.searchInput = value;
- setSearchTerm(value);
- }}
- headerMessage={headerMessage}
- onSelectRow={toggleOption}
- onConfirm={inviteUser}
- showScrollIndicator
- showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)}
- shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
- />
-
-
-
-
- );
- }}
+ Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)}
+ >
+ {
+ Policy.clearErrors(props.route.params.policyID);
+ Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID));
+ }}
+ />
+ {
+ SearchInputManager.searchInput = value;
+ setSearchTerm(value);
+ }}
+ headerMessage={headerMessage}
+ onSelectRow={toggleOption}
+ onConfirm={inviteUser}
+ showScrollIndicator
+ showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)}
+ shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
+ />
+
+
+
+
);
}
diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx
old mode 100755
new mode 100644
index d6bb3fb05385..3f084b4f770b
--- a/src/pages/workspace/WorkspacesListRow.tsx
+++ b/src/pages/workspace/WorkspacesListRow.tsx
@@ -4,7 +4,7 @@ import type {ValueOf} from 'type-fest';
import Avatar from '@components/Avatar';
import Icon from '@components/Icon';
import * as Illustrations from '@components/Icon/Illustrations';
-import type {MenuItemProps} from '@components/MenuItem';
+import type {PopoverMenuItem} from '@components/PopoverMenu';
import Text from '@components/Text';
import ThreeDotsMenu from '@components/ThreeDotsMenu';
import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails';
@@ -34,7 +34,7 @@ type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & {
fallbackWorkspaceIcon?: AvatarSource;
/** Items for the three dots menu */
- menuItems: MenuItemProps[];
+ menuItems: PopoverMenuItem[];
/** Renders the component using big screen layout or small screen layout. When layoutWidth === WorkspaceListRowLayout.NONE,
* component will return null to prevent layout from jumping on initial render and when parent width changes. */
@@ -111,7 +111,7 @@ function WorkspacesListRow({
{isNarrow && (
)}
@@ -165,7 +165,7 @@ function WorkspacesListRow({
{isWide && (
)}
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 34f69faa89cc..726df7658c5c 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -1467,7 +1467,7 @@ const styles = (theme: ThemeColors) =>
createMenuPositionReportActionCompose: (windowHeight: number) =>
({
horizontal: 18 + variables.sideBarWidth,
- vertical: windowHeight - 83,
+ vertical: windowHeight - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM,
} satisfies AnchorPosition),
createMenuPositionRightSidepane: {
@@ -4169,19 +4169,6 @@ const styles = (theme: ThemeColors) =>
},
colorSchemeStyle: (colorScheme: ColorScheme) => ({colorScheme}),
-
- updateAnimation: {
- width: variables.updateAnimationW,
- height: variables.updateAnimationH,
- },
-
- updateRequiredViewHeader: {
- height: variables.updateViewHeaderHeight,
- },
-
- updateRequiredViewTextContainer: {
- width: variables.updateTextViewContainerWidth,
- },
} satisfies Styles);
type ThemeStyles = ReturnType;
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index 6a03354dd3d1..09c593dff8dc 100644
--- a/src/styles/utils/index.ts
+++ b/src/styles/utils/index.ts
@@ -1444,14 +1444,6 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({
return containerStyles;
},
- getUpdateRequiredViewStyles: (isSmallScreenWidth: boolean): ViewStyle[] => [
- {
- alignItems: 'center',
- justifyContent: 'center',
- ...(isSmallScreenWidth ? {} : styles.pb40),
- },
- ],
-
getFullscreenCenteredContentStyles: () => [StyleSheet.absoluteFill, styles.justifyContentCenter, styles.alignItemsCenter],
});
diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts
index bb0e797e5812..6def4858229f 100644
--- a/src/styles/utils/spacing.ts
+++ b/src/styles/utils/spacing.ts
@@ -533,10 +533,6 @@ export default {
paddingBottom: 80,
},
- pb40: {
- paddingBottom: 160,
- },
-
pb10Percentage: {
paddingBottom: '10%',
},
diff --git a/src/styles/variables.ts b/src/styles/variables.ts
index d966b7829bc9..b11d48898af5 100644
--- a/src/styles/variables.ts
+++ b/src/styles/variables.ts
@@ -198,8 +198,4 @@ export default {
cardPreviewWidth: 235,
cardNameWidth: 156,
holdMenuIconSize: 64,
- updateAnimationW: 390,
- updateAnimationH: 240,
- updateTextViewContainerWidth: 310,
- updateViewHeaderHeight: 70,
} as const;
diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts
index 0ea3e05e8d6a..4e7c5396b649 100644
--- a/src/types/onyx/Account.ts
+++ b/src/types/onyx/Account.ts
@@ -50,7 +50,7 @@ type Account = {
/** The active policy ID. Initiating a SmartScan will create an expense on this policy by default. */
activePolicyID?: string;
- errors?: OnyxCommon.Errors;
+ errors?: OnyxCommon.Errors | null;
success?: string;
codesAreCopied?: boolean;
twoFactorAuthStep?: TwoFactorAuthStep;
diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts
index 97e6597c6444..2d8bbb7924bd 100644
--- a/src/types/onyx/Policy.ts
+++ b/src/types/onyx/Policy.ts
@@ -118,6 +118,9 @@ type Policy = {
/** Informative messages about which policy members were added with primary logins when invited with their secondary login */
primaryLoginsInvited?: Record;
+
+ /** The approval mode set up on this policy */
+ approvalMode?: ValueOf;
};
export default Policy;
diff --git a/src/types/utils/CustomRefObject.ts b/src/types/utils/CustomRefObject.ts
deleted file mode 100644
index 13bb0f27a42e..000000000000
--- a/src/types/utils/CustomRefObject.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import type {RefObject} from 'react';
-
-type CustomRefObject = RefObject & {onselectstart: () => boolean};
-
-export default CustomRefObject;
diff --git a/tests/perf-test/README.md b/tests/perf-test/README.md
index a9b1643d191d..35af9dc35b7d 100644
--- a/tests/perf-test/README.md
+++ b/tests/perf-test/README.md
@@ -60,6 +60,7 @@ We use Reassure for monitoring performance regression. It helps us check if our
- Investigate the code changes that might be causing this and address them to maintain a stable render count. More info [here](https://github.com/Expensify/App/blob/fe9e9e3e31bae27c2398678aa632e808af2690b5/tests/perf-test/README.md?plain=1#L32).
- It is important to run Reassure tests locally and see if our changes caused a regression.
+ - One of the potential factors that may influence variation in the number of renders is adding unnecesary providers to the component we want to test using `````` . Ensure that all providers are necessary for running the test.
## What can be tested (scenarios)
diff --git a/tests/perf-test/SearchPage.perf-test.js b/tests/perf-test/SearchPage.perf-test.js
index 046d651469f1..e948ade014dc 100644
--- a/tests/perf-test/SearchPage.perf-test.js
+++ b/tests/perf-test/SearchPage.perf-test.js
@@ -5,11 +5,7 @@ import {measurePerformance} from 'reassure';
import _ from 'underscore';
import SearchPage from '@pages/SearchPage';
import ComposeProviders from '../../src/components/ComposeProviders';
-import {LocaleContextProvider} from '../../src/components/LocaleContextProvider';
import OnyxProvider from '../../src/components/OnyxProvider';
-import {CurrentReportIDContextProvider} from '../../src/components/withCurrentReportID';
-import {KeyboardStateProvider} from '../../src/components/withKeyboardState';
-import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions';
import CONST from '../../src/CONST';
import ONYXKEYS from '../../src/ONYXKEYS';
import createCollection from '../utils/collections/createCollection';
@@ -81,7 +77,7 @@ afterEach(() => {
function SearchPageWrapper(args) {
return (
-
+
{
.then(() => measurePerformance(, {scenario, runs}));
});
-test.skip('[Search Page] should search in options list', async () => {
+test('[Search Page] should search in options list', async () => {
const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock();
const scenario = async () => {
@@ -156,10 +152,6 @@ test.skip('[Search Page] should search in options list', async () => {
fireEvent.changeText(input, mockedPersonalDetails['88'].login);
await act(triggerTransitionEnd);
await screen.findByText(mockedPersonalDetails['88'].login);
-
- fireEvent.changeText(input, mockedPersonalDetails['45'].login);
- await act(triggerTransitionEnd);
- await screen.findByText(mockedPersonalDetails['45'].login);
};
const navigation = {addListener};
@@ -177,16 +169,16 @@ test.skip('[Search Page] should search in options list', async () => {
.then(() => measurePerformance(, {scenario, runs}));
});
-test.skip('[Search Page] should click on list item', async () => {
+test('[Search Page] should click on list item', async () => {
const {triggerTransitionEnd, addListener} = TestHelper.createAddListenerMock();
const scenario = async () => {
await screen.findByTestId('SearchPage');
const input = screen.getByTestId('options-selector-input');
- fireEvent.changeText(input, mockedPersonalDetails['6'].login);
+ fireEvent.changeText(input, mockedPersonalDetails['4'].login);
await act(triggerTransitionEnd);
- const optionButton = await screen.findByText(mockedPersonalDetails['6'].login);
+ const optionButton = await screen.findByText(mockedPersonalDetails['4'].login);
fireEvent.press(optionButton);
};
diff --git a/tests/perf-test/SignInPage.perf-test.tsx b/tests/perf-test/SignInPage.perf-test.tsx
new file mode 100644
index 000000000000..80964c3c49cd
--- /dev/null
+++ b/tests/perf-test/SignInPage.perf-test.tsx
@@ -0,0 +1,148 @@
+import type * as NativeNavigation from '@react-navigation/native';
+import {fireEvent, screen} from '@testing-library/react-native';
+import React from 'react';
+import Onyx from 'react-native-onyx';
+import {measurePerformance} from 'reassure';
+import ComposeProviders from '@components/ComposeProviders';
+import {LocaleContextProvider} from '@components/LocaleContextProvider';
+import OnyxProvider from '@components/OnyxProvider';
+import {WindowDimensionsProvider} from '@components/withWindowDimensions';
+import * as Localize from '@libs/Localize';
+import type * as Navigation from '@libs/Navigation/Navigation';
+import ONYXKEYS from '@src/ONYXKEYS';
+import SignInPage from '@src/pages/signin/SignInPage';
+import getValidCodeCredentials from '../utils/collections/getValidCodeCredentials';
+import userAccount, {getValidAccount} from '../utils/collections/userAccount';
+import PusherHelper from '../utils/PusherHelper';
+import * as TestHelper from '../utils/TestHelper';
+import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
+import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates';
+
+jest.mock('../../src/libs/Navigation/Navigation', () => {
+ const actualNav = jest.requireActual('../../src/libs/Navigation/Navigation');
+ return {
+ ...actualNav,
+ navigationRef: {
+ addListener: () => jest.fn(),
+ removeListener: () => jest.fn(),
+ },
+ } as typeof Navigation;
+});
+
+const mockedNavigate = jest.fn();
+jest.mock('@react-navigation/native', () => {
+ const actualNav = jest.requireActual('@react-navigation/native');
+ return {
+ ...actualNav,
+ useFocusEffect: jest.fn(),
+ useIsFocused: () => ({
+ navigate: mockedNavigate,
+ }),
+ useRoute: () => jest.fn(),
+ useNavigation: () => ({
+ navigate: jest.fn(),
+ addListener: () => jest.fn(),
+ }),
+ createNavigationContainerRef: jest.fn(),
+ } as typeof NativeNavigation;
+});
+
+type Props = Partial & {navigation: Partial};
+
+function SignInPageWrapper(args: Props) {
+ return (
+
+
+
+ );
+}
+
+const login = 'test@mail.com';
+
+describe('SignInPage', () => {
+ beforeAll(() => {
+ Onyx.init({
+ keys: ONYXKEYS,
+ safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS],
+ });
+ });
+
+ // Initialize the network key for OfflineWithFeedback
+ beforeEach(() => {
+ global.fetch = TestHelper.getGlobalFetchMock() as typeof fetch;
+ wrapOnyxWithWaitForBatchedUpdates(Onyx);
+ Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false});
+ });
+
+ // Clear out Onyx after each test so that each test starts with a clean state
+ afterEach(() => {
+ Onyx.clear();
+ PusherHelper.teardown();
+ });
+
+ test('[SignInPage] should add username and click continue button', () => {
+ const addListener = jest.fn();
+ const scenario = async () => {
+ // Checking the SignInPage is mounted
+ await screen.findByTestId('SignInPage');
+
+ const usernameInput = screen.getByTestId('username');
+
+ fireEvent.changeText(usernameInput, login);
+
+ const hintContinueButtonText = Localize.translateLocal('common.continue');
+
+ const continueButton = await screen.findByText(hintContinueButtonText);
+
+ fireEvent.press(continueButton);
+ };
+
+ const navigation = {addListener};
+
+ return waitForBatchedUpdates()
+ .then(() =>
+ Onyx.multiSet({
+ [ONYXKEYS.ACCOUNT]: userAccount,
+ [ONYXKEYS.IS_SIDEBAR_LOADED]: false,
+ }),
+ )
+ .then(() => measurePerformance(, {scenario}));
+ });
+
+ test('[SignInPage] should add magic code and click Sign In button', () => {
+ const addListener = jest.fn();
+ const scenario = async () => {
+ // Checking the SignInPage is mounted
+ await screen.findByTestId('SignInPage');
+
+ const welcomeBackText = Localize.translateLocal('welcomeText.welcomeBack');
+ const enterMagicCodeText = Localize.translateLocal('welcomeText.welcomeEnterMagicCode', {login});
+
+ await screen.findByText(`${welcomeBackText} ${enterMagicCodeText}`);
+ const magicCodeInput = screen.getByTestId('validateCode');
+
+ fireEvent.changeText(magicCodeInput, '123456');
+
+ const signInButtonText = Localize.translateLocal('common.signIn');
+ const signInButton = await screen.findByText(signInButtonText);
+
+ fireEvent.press(signInButton);
+ };
+
+ const navigation = {addListener};
+
+ return waitForBatchedUpdates()
+ .then(() =>
+ Onyx.multiSet({
+ [ONYXKEYS.ACCOUNT]: getValidAccount(login),
+ [ONYXKEYS.CREDENTIALS]: getValidCodeCredentials(login),
+ [ONYXKEYS.IS_SIDEBAR_LOADED]: false,
+ }),
+ )
+ .then(() => measurePerformance(, {scenario}));
+ });
+});
diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js
index 7480da456d7f..a752eea1a990 100644
--- a/tests/unit/DateUtilsTest.js
+++ b/tests/unit/DateUtilsTest.js
@@ -213,4 +213,35 @@ describe('DateUtils', () => {
});
});
});
+
+ describe('getLastBusinessDayOfMonth', () => {
+ const scenarios = [
+ {
+ // Last business day of May in 2025
+ inputDate: new Date(2025, 4),
+ expectedResult: 30,
+ },
+ {
+ // Last business day of February in 2024
+ inputDate: new Date(2024, 2),
+ expectedResult: 29,
+ },
+ {
+ // Last business day of January in 2024
+ inputDate: new Date(2024, 0),
+ expectedResult: 31,
+ },
+ {
+ // Last business day of September in 2023
+ inputDate: new Date(2023, 8),
+ expectedResult: 29,
+ },
+ ];
+
+ test.each(scenarios)('returns a last business day based on the input date', ({inputDate, expectedResult}) => {
+ const lastBusinessDay = DateUtils.getLastBusinessDayOfMonth(inputDate);
+
+ expect(lastBusinessDay).toEqual(expectedResult);
+ });
+ });
});
diff --git a/tests/unit/MigrationTest.js b/tests/unit/MigrationTest.js
index 28d21cd4b11c..ebffc71e4e0e 100644
--- a/tests/unit/MigrationTest.js
+++ b/tests/unit/MigrationTest.js
@@ -428,6 +428,31 @@ describe('Migrations', () => {
});
}));
+ it('Should remove any instances of participants found in a report', () =>
+ Onyx.multiSet({
+ [`${ONYXKEYS.COLLECTION.REPORT}1`]: {
+ reportID: 1,
+ participants: ['fake@test.com'],
+ participantAccountIDs: [5],
+ },
+ })
+ .then(PersonalDetailsByAccountID)
+ .then(() => {
+ expect(LogSpy).toHaveBeenCalledWith('[Migrate Onyx] PersonalDetailsByAccountID migration: removing participants from report 1');
+ const connectionID = Onyx.connect({
+ key: ONYXKEYS.COLLECTION.REPORT,
+ waitForCollectionCallback: true,
+ callback: (allReports) => {
+ Onyx.disconnect(connectionID);
+ const expectedReport = {
+ reportID: 1,
+ participantAccountIDs: [5],
+ };
+ expect(allReports[`${ONYXKEYS.COLLECTION.REPORT}1`]).toMatchObject(expectedReport);
+ },
+ });
+ }));
+
it('Should remove any instances of ownerEmail found in a report', () =>
Onyx.multiSet({
[`${ONYXKEYS.COLLECTION.REPORT}1`]: {
diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js
index 67831bd32bca..6c72558e5df3 100644
--- a/tests/utils/LHNTestUtils.js
+++ b/tests/utils/LHNTestUtils.js
@@ -262,6 +262,7 @@ function getFakePolicy(id = 1, name = 'Workspace-Test-001') {
submitsTo: 123456,
defaultBillable: false,
disabledFields: {defaultBillable: true, reimbursable: false},
+ approvalMode: 'BASIC',
};
}
diff --git a/tests/utils/collections/getValidCodeCredentials.ts b/tests/utils/collections/getValidCodeCredentials.ts
new file mode 100644
index 000000000000..5ee856b61160
--- /dev/null
+++ b/tests/utils/collections/getValidCodeCredentials.ts
@@ -0,0 +1,11 @@
+import {randEmail, randNumber} from '@ngneat/falso';
+import type {Credentials} from '@src/types/onyx';
+
+function getValidCodeCredentials(login = randEmail()): Credentials {
+ return {
+ login,
+ validateCode: `${randNumber()}`,
+ };
+}
+
+export default getValidCodeCredentials;
diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts
index acdd22253173..8547c171c7a7 100644
--- a/tests/utils/collections/policies.ts
+++ b/tests/utils/collections/policies.ts
@@ -26,5 +26,6 @@ export default function createRandomPolicy(index: number): Policy {
errors: {},
customUnits: {},
errorFields: {},
+ approvalMode: rand(Object.values(CONST.POLICY.APPROVAL_MODE)),
};
}
diff --git a/tests/utils/collections/userAccount.ts b/tests/utils/collections/userAccount.ts
new file mode 100644
index 000000000000..9e7c33a228d5
--- /dev/null
+++ b/tests/utils/collections/userAccount.ts
@@ -0,0 +1,14 @@
+import CONST from '@src/CONST';
+import type {Account} from '@src/types/onyx';
+
+function getValidAccount(credentialLogin = ''): Account {
+ return {
+ validated: true,
+ primaryLogin: credentialLogin,
+ isLoading: false,
+ requiresTwoFactorAuth: false,
+ };
+}
+
+export default CONST.DEFAULT_ACCOUNT_DATA;
+export {getValidAccount};