diff --git a/android/app/build.gradle b/android/app/build.gradle index 425a4c2ddd4b..e135d44eb834 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 1001043001 - versionName "1.4.30-1" + versionCode 1001043202 + versionName "1.4.32-2" } 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/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md index d4181735298e..fd137aab62fb 100644 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md +++ b/docs/articles/expensify-classic/billing-and-subscriptions/Receipt-Breakdown.md @@ -16,8 +16,8 @@ Your receipt is broken up into multiple sections that include: The top section will show the total amount you paid as the billing owner of Expensify workspaces and give you a breakdown of price per member. Every member of your workspace(s) gets to store data, review data, and access free features like Expensify Chat. Thus, we show the total price and then use all of the members across all of the workspaces you own to calculate the price per member. Further down in the receipt, and in this article, we break down the members who generated billable activity. ## How-to reduce your bill and get paid to use Expensify -Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. - +Chances are you can actually get paid to use Expensify with the Expensify Card. In this section of the receipt, we outline how much money you're leaving on the table by not using the Expensify Card. You can click `Get started` to connect with your account manager (if you have one) or Concierge, both of whom can help get you started with the card. + _Note: Currently, we offer Expensify Cards to companies with USD bank accounts._ ## How-to understand your billing breakdown @@ -25,7 +25,7 @@ Your receipt will have a detailed breakdown of activity and discounts across all - [Number of] Inactive workspace members @ $0.00 - All inactive members from any of your workspaces. - [Number of] Chat-only members @ $0.00 - - Any workspace members who chatted but didn't generate any other billable activity. Learn more about [chatting for free.](https://help.expensify.com/articles/new-expensify/getting-started/chat/Everything-About-Chat) + - Any workspace members who chatted but didn't generate any other billable activity. Learn more about [chatting for free.](https://help.expensify.com/articles/new-expensify/chat/Introducing-Expensify-Chat) - [Number of] Annual Control members @ $18.00 - Any members included in your annual subscription on the Control plan. - [Number of] Pay-per-use Control members @ $36.00 @@ -37,9 +37,9 @@ Your receipt will have a detailed breakdown of activity and discounts across all - [Number of] Free members @ $0.00 - All members across any of your Free workspaces. - X% Expensify Card discount with $Y spend - - This shows the % discount you're getting based on total spend across your Expensify Cards. This is only available in the US. + - This shows the % discount you're getting based on total approved spend across your Expensify Cards. This is only available in the US. - X% Expensify Card cash back credit for $Y spend - - The amount of cash back you've earned based on total spend across your Expensify Cards. This is only available in the US. + - The amount of cash back you've earned based on total approved spend across your Expensify Cards. This is only available in the US. - 50% ExpensifyApproved! partner discount - If you're part of an accounting firm, you get an additional discount for being our partner. [Learn more about our ExpensifyApproved! accountants program.](https://use.expensify.com/accountants-program) - Total diff --git a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md index 464f2129d800..1cf29531f696 100644 --- a/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md +++ b/docs/articles/expensify-classic/expensify-card/Set-Up-the-Card-for-Your-Company.md @@ -58,7 +58,7 @@ Applying for or using the Expensify Card will never have any positive or negativ ## How much does the Expensify Card cost? -The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription. +The Expensify Card is a free corporate card, and no fees are associated with it. In addition, if you use the Expensify Card, you can save money on your Expensify subscription (based on how much of your approved spend occurs on the Expensify Card, compared with other approved spend, in each month). ## If I have staff outside the US, can they use the Expensify Card? diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 86b6c0374898..c636ced8e7f9 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.30 + 1.4.32 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.30.1 + 1.4.32.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 9c3f1c7d74f6..ef1ef0d998d5 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.30 + 1.4.32 CFBundleSignature ???? CFBundleVersion - 1.4.30.1 + 1.4.32.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 3461a6f34880..16439b1d24d9 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -3,9 +3,9 @@ CFBundleShortVersionString - 1.4.30 + 1.4.32 CFBundleVersion - 1.4.30.1 + 1.4.32.2 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index bf806ea2ab76..543a1366f8d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.30-1", + "version": "1.4.32-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.30-1", + "version": "1.4.32-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 002f1468a811..8ceac3912660 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.30-1", + "version": "1.4.32-2", "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..ff3934c31943 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -724,6 +724,8 @@ const CONST = { REPORT_INITIAL_RENDER: 'report_initial_render', SWITCH_REPORT: 'switch_report', SIDEBAR_LOADED: 'sidebar_loaded', + OPEN_SEARCH: 'open_search', + LOAD_SEARCH_OPTIONS: 'load_search_options', COLD: 'cold', WARM: 'warm', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, @@ -788,7 +790,6 @@ const CONST = { EXP_ERROR: 666, MANY_WRITES_ERROR: 665, UNABLE_TO_RETRY: 'unableToRetry', - UPDATE_REQUIRED: 426, }, HTTP_STATUS: { // When Cloudflare throttles @@ -819,9 +820,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 +977,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 +1304,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/ROUTES.ts b/src/ROUTES.ts index 42123aa9b4a4..9c4375b84ab6 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -237,10 +237,6 @@ const ROUTES = { route: 'r/:reportID/assignee', getRoute: (reportID: string) => `r/${reportID}/assignee` as const, }, - PRIVATE_NOTES_VIEW: { - route: 'r/:reportID/notes/:accountID', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}` as const, - }, PRIVATE_NOTES_LIST: { route: 'r/:reportID/notes', getRoute: (reportID: string) => `r/${reportID}/notes` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 960991eb277b..2bf40caede57 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -183,7 +183,6 @@ const SCREENS = { }, PRIVATE_NOTES: { - VIEW: 'PrivateNotes_View', LIST: 'PrivateNotes_List', EDIT: 'PrivateNotes_Edit', }, 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/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index c1e4562a0c2d..8cac059436b5 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -9,6 +9,8 @@ import SectionList from '@components/SectionList'; import Text from '@components/Text'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import Timing from '@libs/actions/Timing'; +import Performance from '@libs/Performance'; import type {OptionData} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; @@ -108,6 +110,16 @@ function BaseOptionsList( flattenedData.current = buildFlatSectionArray(); }); + useEffect(() => { + if (isLoading) { + return; + } + + // Mark the end of the search page load time. This data is collected only for Search page. + Timing.end(CONST.TIMING.OPEN_SEARCH); + Performance.markEnd(CONST.TIMING.OPEN_SEARCH); + }, [isLoading]); + const onViewableItemsChanged = () => { if (didLayout.current || !onLayout) { return; diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 412aeedcf965..bbcce6fff9a6 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -91,7 +91,7 @@ class BaseOptionsSelector extends Component { allOptions, focusedIndex, shouldDisableRowSelection: false, - shouldShowReferralModal: false, + shouldShowReferralModal: this.props.shouldShowReferralCTA, errorMessage: '', paginationPage: 1, disableEnterShortCut: false, @@ -660,9 +660,12 @@ class BaseOptionsSelector extends Component { )} - {this.props.shouldShowReferralCTA && ( + {this.props.shouldShowReferralCTA && this.state.shouldShowReferralModal && ( - + )} diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 678bb6f06403..1bee95532104 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -49,10 +49,6 @@ function BasePicker( // reference to @react-native-picker/picker const picker = useRef(null); - // Windows will reuse the text color of the select for each one of the options - // so we might need to color accordingly so it doesn't blend with the background. - const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: theme.text} : {}; - useEffect(() => { if (!!value || !items || items.length !== 1 || !onInputChange) { return; @@ -152,6 +148,10 @@ function BasePicker( return theme.text; }, [theme]); + // Windows will reuse the text color of the select for each one of the options + // so we might need to color accordingly so it doesn't blend with the background. + const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: itemColor} : {}; + const hasError = !!errorText; if (isDisabled) { 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/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 473d5cdbed08..4a6b8b03f2b4 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -6,7 +6,7 @@ import CONST from '@src/CONST'; import Navigation from '@src/libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import Icon from './Icon'; -import {Info} from './Icon/Expensicons'; +import {Close} from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; import Text from './Text'; @@ -16,9 +16,12 @@ type ReferralProgramCTAProps = { | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND; + + /** Method to trigger when pressing close button of the banner */ + onCloseButtonPress?: () => void; }; -function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) { +function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}}: ReferralProgramCTAProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); @@ -28,7 +31,7 @@ function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) { onPress={() => { Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(referralContentType)); }} - style={[styles.p5, styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10}]} + style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5]} accessibilityLabel="referral" role={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -41,12 +44,22 @@ function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) { {translate(`referralProgram.${referralContentType}.buttonText2`)} - + { + e.preventDefault(); + }} + style={[styles.touchableButtonImage]} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + ); } diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 3d1710de1432..ed7c05b828a9 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -1,6 +1,8 @@ import React, {useMemo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -18,10 +20,11 @@ import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {PolicyReportField, Report} from '@src/types/onyx'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; -type MoneyReportViewProps = { +type MoneyReportViewComponentProps = { /** The report currently being looked at */ report: Report; @@ -32,7 +35,14 @@ type MoneyReportViewProps = { shouldShowHorizontalRule: boolean; }; -function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) { +type MoneyReportViewOnyxProps = { + /** Policies that the user is part of */ + policies: OnyxCollection; +}; + +type MoneyReportViewProps = MoneyReportViewComponentProps & MoneyReportViewOnyxProps; + +function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule, policies}: MoneyReportViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -59,7 +69,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: () => policyReportFields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight), [policyReportFields], ); - + const isAdmin = ReportUtils.isPolicyAdmin(report.policyID ?? '', policies); return ( @@ -67,6 +77,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: {canUseReportFields && sortedPolicyReportFields.map((reportField) => { const title = ReportUtils.getReportFieldTitle(report, reportField); + const isDisabled = !isAdmin || isSettled || ReportUtils.isReportFieldOfTypeTitle(reportField); return ( Navigation.navigate(ROUTES.EDIT_REPORT_FIELD_REQUEST.getRoute(report.reportID, report.policyID ?? '', reportField.fieldID))} shouldShowRightIcon - disabled={ReportUtils.isReportFieldOfTypeTitle(reportField)} + disabled={isDisabled} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} shouldGreyOutWhenDisabled={false} numberOfLinesTitle={0} @@ -167,4 +178,8 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportView.displayName = 'MoneyReportView'; -export default MoneyReportView; +export default withOnyx({ + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(MoneyReportView); diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.tsx similarity index 91% rename from src/components/SelectionList/BaseListItem.js rename to src/components/SelectionList/BaseListItem.tsx index 6a067ea0fe3d..71845931ba52 100644 --- a/src/components/SelectionList/BaseListItem.js +++ b/src/components/SelectionList/BaseListItem.tsx @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; @@ -12,10 +11,10 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import RadioListItem from './RadioListItem'; -import {baseListItemPropTypes} from './selectionListPropTypes'; +import type {BaseListItemProps, RadioItem, User} from './types'; import UserListItem from './UserListItem'; -function BaseListItem({ +function BaseListItem({ item, isFocused = false, isDisabled = false, @@ -26,12 +25,12 @@ function BaseListItem({ onDismissError = () => {}, rightHandSideComponent, keyForList, -}) { +}: BaseListItemProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const isUserItem = lodashGet(item, 'icons.length', 0) > 0; + const isUserItem = 'icons' in item && item?.icons?.length && item.icons.length > 0; const ListItem = isUserItem ? UserListItem : RadioListItem; const rightHandSideComponentRender = () => { @@ -49,8 +48,8 @@ function BaseListItem({ return ( onDismissError(item)} - pendingAction={item.pendingAction} - errors={item.errors} + pendingAction={isUserItem ? item.pendingAction : undefined} + errors={isUserItem ? item.errors : undefined} errorRowStyles={styles.ph5} > )} + onSelectRow(item)} showTooltip={showTooltip} /> + {!canSelectMultiple && item.isSelected && !rightHandSideComponent && ( - {Boolean(item.invitedSecondaryLogin) && ( + {isUserItem && item.invitedSecondaryLogin && ( {translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})} @@ -140,6 +141,5 @@ function BaseListItem({ } BaseListItem.displayName = 'BaseListItem'; -BaseListItem.propTypes = baseListItemPropTypes; export default BaseListItem; diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.tsx similarity index 76% rename from src/components/SelectionList/BaseSelectionList.js rename to src/components/SelectionList/BaseSelectionList.tsx index 960618808fd9..d97c47c84ee7 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -1,8 +1,8 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; +import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; @@ -13,69 +13,61 @@ import SafeAreaConsumer from '@components/SafeAreaConsumer'; import SectionList from '@components/SectionList'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withKeyboardState, {keyboardStatePropTypes} from '@components/withKeyboardState'; import useActiveElementRole from '@hooks/useActiveElementRole'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import BaseListItem from './BaseListItem'; -import {propTypes as selectionListPropTypes} from './selectionListPropTypes'; - -const propTypes = { - ...keyboardStatePropTypes, - ...selectionListPropTypes, -}; - -function BaseSelectionList({ - sections, - canSelectMultiple = false, - onSelectRow, - onSelectAll, - onDismissError, - textInputLabel = '', - textInputPlaceholder = '', - textInputValue = '', - textInputHint = '', - textInputMaxLength, - inputMode = CONST.INPUT_MODE.TEXT, - onChangeText, - initiallyFocusedOptionKey = '', - onScroll, - onScrollBeginDrag, - headerMessage = '', - confirmButtonText = '', - onConfirm, - headerContent, - footerContent, - showScrollIndicator = false, - showLoadingPlaceholder = false, - showConfirmButton = false, - shouldPreventDefaultFocusOnSelectRow = false, - isKeyboardShown = false, - containerStyle = [], - disableInitialFocusOptionStyle = false, - inputRef = null, - disableKeyboardShortcuts = false, - children, - shouldStopPropagation = false, - shouldShowTooltips = true, - shouldUseDynamicMaxToRenderPerBatch = false, - rightHandSideComponent, -}) { - const theme = useTheme(); +import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, RadioItem, Section, SectionListDataType, User} from './types'; + +function BaseSelectionList( + { + sections, + canSelectMultiple = false, + onSelectRow, + onSelectAll, + onDismissError, + textInputLabel = '', + textInputPlaceholder = '', + textInputValue = '', + textInputHint, + textInputMaxLength, + inputMode = CONST.INPUT_MODE.TEXT, + onChangeText, + initiallyFocusedOptionKey = '', + onScroll, + onScrollBeginDrag, + headerMessage = '', + confirmButtonText = '', + onConfirm = () => {}, + headerContent, + footerContent, + showScrollIndicator = false, + showLoadingPlaceholder = false, + showConfirmButton = false, + shouldPreventDefaultFocusOnSelectRow = false, + containerStyle, + isKeyboardShown = false, + disableKeyboardShortcuts = false, + children, + shouldStopPropagation = false, + shouldShowTooltips = true, + shouldUseDynamicMaxToRenderPerBatch = false, + rightHandSideComponent, + }: BaseSelectionListProps, + inputRef: ForwardedRef, +) { const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const listRef = useRef(null); - const textInputRef = useRef(null); - const focusTimeoutRef = useRef(null); - const shouldShowTextInput = Boolean(textInputLabel); - const shouldShowSelectAll = Boolean(onSelectAll); + const listRef = useRef>>(null); + const textInputRef = useRef(null); + const focusTimeoutRef = useRef(null); + const shouldShowTextInput = !!textInputLabel; + const shouldShowSelectAll = !!onSelectAll; const activeElementRole = useActiveElementRole(); const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); @@ -87,26 +79,24 @@ function BaseSelectionList({ * - `disabledOptionsIndexes`: Contains the indexes of all the disabled items in the list, to be used by the ArrowKeyFocusManager * - `itemLayouts`: Contains the layout information for each item, header and footer in the list, * so we can calculate the position of any given item when scrolling programmatically - * - * @return {{itemLayouts: [{offset: number, length: number}], disabledOptionsIndexes: *[], allOptions: *[]}} */ - const flattenedSections = useMemo(() => { - const allOptions = []; + const flattenedSections = useMemo>(() => { + const allOptions: TItem[] = []; - const disabledOptionsIndexes = []; + const disabledOptionsIndexes: number[] = []; let disabledIndex = 0; let offset = 0; const itemLayouts = [{length: 0, offset}]; - const selectedOptions = []; + const selectedOptions: TItem[] = []; - _.each(sections, (section, sectionIndex) => { + sections.forEach((section, sectionIndex) => { const sectionHeaderHeight = variables.optionsListSectionHeaderHeight; itemLayouts.push({length: sectionHeaderHeight, offset}); offset += sectionHeaderHeight; - _.each(section.data, (item, optionIndex) => { + section.data?.forEach((item, optionIndex) => { // Add item to the general flattened array allOptions.push({ ...item, @@ -115,7 +105,7 @@ function BaseSelectionList({ }); // If disabled, add to the disabled indexes array - if (section.isDisabled || item.isDisabled) { + if (!!section.isDisabled || item.isDisabled) { disabledOptionsIndexes.push(disabledIndex); } disabledIndex += 1; @@ -155,34 +145,34 @@ function BaseSelectionList({ }, [canSelectMultiple, sections]); // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member - const [focusedIndex, setFocusedIndex] = useState(() => _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey)); + const [focusedIndex, setFocusedIndex] = useState(() => flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey)); // Disable `Enter` shortcut if the active element is a button or checkbox - const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole); + const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles); /** * Scrolls to the desired item index in the section list * - * @param {Number} index - the index of the item to scroll to - * @param {Boolean} animated - whether to animate the scroll + * @param index - the index of the item to scroll to + * @param animated - whether to animate the scroll */ const scrollToIndex = useCallback( - (index, animated = true) => { + (index: number, animated = true) => { const item = flattenedSections.allOptions[index]; if (!listRef.current || !item) { return; } - const itemIndex = item.index; - const sectionIndex = item.sectionIndex; + const itemIndex = item.index ?? -1; + const sectionIndex = item.sectionIndex ?? -1; // Note: react-native's SectionList automatically strips out any empty sections. // So we need to reduce the sectionIndex to remove any empty sections in front of the one we're trying to scroll to. // Otherwise, it will cause an index-out-of-bounds error and crash the app. let adjustedSectionIndex = sectionIndex; for (let i = 0; i < sectionIndex; i++) { - if (_.isEmpty(lodashGet(sections, `[${i}].data`))) { + if (sections[i].data) { adjustedSectionIndex--; } } @@ -197,10 +187,10 @@ function BaseSelectionList({ /** * Logic to run when a row is selected, either with click/press or keyboard hotkeys. * - * @param {Object} item - the list item - * @param {Boolean} shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard) + * @param item - the list item + * @param shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard) */ - const selectRow = (item, shouldUnfocusRow = false) => { + const selectRow = (item: TItem, shouldUnfocusRow = false) => { // In single-selection lists we don't care about updating the focused index, because the list is closed after selecting an item if (canSelectMultiple) { if (sections.length > 1) { @@ -233,15 +223,15 @@ function BaseSelectionList({ }; const selectAllRow = () => { - onSelectAll(); + onSelectAll?.(); + if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && textInputRef.current) { textInputRef.current.focus(); } }; - const selectFocusedOption = (e) => { - const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value']); - const focusedOption = focusedItemKey ? _.find(flattenedSections.allOptions, (option) => option.keyForList === focusedItemKey) : flattenedSections.allOptions[focusedIndex]; + const selectFocusedOption = () => { + const focusedOption = flattenedSections.allOptions[focusedIndex]; if (!focusedOption || focusedOption.isDisabled) { return; @@ -254,8 +244,8 @@ function BaseSelectionList({ * This function is used to compute the layout of any given item in our list. * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. * - * @param {Array} data - This is the same as the data we pass into the component - * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: + * @param data - This is the same as the data we pass into the component + * @param flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: * * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. * 2. Each section includes a header, even if we don't provide/render one. @@ -263,10 +253,8 @@ function BaseSelectionList({ * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: * * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] - * - * @returns {Object} */ - const getItemLayout = (data, flatDataArrayIndex) => { + const getItemLayout = (data: Array> | null, flatDataArrayIndex: number) => { const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex]; if (!targetItem) { @@ -284,8 +272,8 @@ function BaseSelectionList({ }; }; - const renderSectionHeader = ({section}) => { - if (!section.title || _.isEmpty(section.data)) { + const renderSectionHeader = ({section}: {section: SectionListDataType}) => { + if (!section.title || isEmptyObject(section.data)) { return null; } @@ -300,9 +288,10 @@ function BaseSelectionList({ ); }; - const renderItem = ({item, index, section}) => { - const normalizedIndex = index + lodashGet(section, 'indexOffset', 0); - const isDisabled = section.isDisabled || item.isDisabled; + const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { + const indexOffset = section.indexOffset ? section.indexOffset : 0; + const normalizedIndex = index + indexOffset; + const isDisabled = !!section.isDisabled || item.isDisabled; const isItemFocused = !isDisabled && focusedIndex === normalizedIndex; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const showTooltip = shouldShowTooltips && normalizedIndex < 10; @@ -312,11 +301,9 @@ function BaseSelectionList({ item={item} isFocused={isItemFocused} isDisabled={isDisabled} - isHide={!maxToRenderPerBatch} showTooltip={showTooltip} canSelectMultiple={canSelectMultiple} onSelectRow={() => selectRow(item, true)} - disableIsFocusStyle={disableInitialFocusOptionStyle} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} @@ -326,11 +313,10 @@ function BaseSelectionList({ }; const scrollToFocusedIndexOnFirstRender = useCallback( - ({nativeEvent}) => { + (nativeEvent: LayoutChangeEvent) => { if (shouldUseDynamicMaxToRenderPerBatch) { - const listHeight = lodashGet(nativeEvent, 'layout.height', 0); - const itemHeight = lodashGet(nativeEvent, 'layout.y', 0); - + const listHeight = nativeEvent.nativeEvent.layout.height; + const itemHeight = nativeEvent.nativeEvent.layout.y; setMaxToRenderPerBatch((Math.ceil(listHeight / itemHeight) || 0) + CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); } @@ -344,7 +330,7 @@ function BaseSelectionList({ ); const updateAndScrollToFocusedIndex = useCallback( - (newFocusedIndex) => { + (newFocusedIndex: number) => { setFocusedIndex(newFocusedIndex); scrollToIndex(newFocusedIndex, true); }, @@ -355,7 +341,12 @@ function BaseSelectionList({ useFocusEffect( useCallback(() => { if (shouldShowTextInput) { - focusTimeoutRef.current = setTimeout(() => textInputRef.current.focus(), CONST.ANIMATED_TRANSITION); + focusTimeoutRef.current = setTimeout(() => { + if (!textInputRef.current) { + return; + } + textInputRef.current.focus(); + }, CONST.ANIMATED_TRANSITION); } return () => { if (!focusTimeoutRef.current) { @@ -382,7 +373,7 @@ function BaseSelectionList({ /** Selects row when pressing Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { captureOnInputs: true, - shouldBubble: () => !flattenedSections.allOptions[focusedIndex], + shouldBubble: !flattenedSections.allOptions[focusedIndex], shouldStopPropagation, isActive: !disableKeyboardShortcuts && !disableEnterShortcut && isFocused, }); @@ -390,8 +381,8 @@ function BaseSelectionList({ /** Calls confirm action when pressing CTRL (CMD) + Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, { captureOnInputs: true, - shouldBubble: () => !flattenedSections.allOptions[focusedIndex], - isActive: !disableKeyboardShortcuts && Boolean(onConfirm) && isFocused, + shouldBubble: !flattenedSections.allOptions[focusedIndex], + isActive: !disableKeyboardShortcuts && !!onConfirm && isFocused, }); return ( @@ -401,19 +392,22 @@ function BaseSelectionList({ maxIndex={flattenedSections.allOptions.length - 1} onFocusedIndexChanged={updateAndScrollToFocusedIndex} > - {/* */} {({safeAreaPaddingBottomStyle}) => ( - + {shouldShowTextInput && ( { - if (inputRef) { - // eslint-disable-next-line no-param-reassign - inputRef.current = el; + ref={(element) => { + textInputRef.current = element as RNTextInput; + + if (!inputRef) { + return; + } + + if (typeof inputRef === 'function') { + inputRef(element as RNTextInput); } - textInputRef.current = el; }} label={textInputLabel} accessibilityLabel={textInputLabel} @@ -427,16 +421,16 @@ function BaseSelectionList({ selectTextOnFocus spellCheck={false} onSubmitEditing={selectFocusedOption} - blurOnSubmit={Boolean(flattenedSections.allOptions.length)} + blurOnSubmit={!!flattenedSections.allOptions.length} /> )} - {Boolean(headerMessage) && ( + {!!headerMessage && ( {headerMessage} )} - {Boolean(headerContent) && headerContent} + {!!headerContent && headerContent} {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? ( ) : ( @@ -474,7 +468,7 @@ function BaseSelectionList({ onScrollBeginDrag={onScrollBeginDrag} keyExtractor={(item) => item.keyForList} extraData={focusedIndex} - indicatorStyle={theme.white} + indicatorStyle="white" keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={showScrollIndicator} initialNumToRender={12} @@ -500,7 +494,7 @@ function BaseSelectionList({ /> )} - {Boolean(footerContent) && {footerContent}} + {!!footerContent && {footerContent}} )} @@ -509,6 +503,5 @@ function BaseSelectionList({ } BaseSelectionList.displayName = 'BaseSelectionList'; -BaseSelectionList.propTypes = propTypes; -export default withKeyboardState(BaseSelectionList); +export default forwardRef(BaseSelectionList); diff --git a/src/components/SelectionList/RadioListItem.js b/src/components/SelectionList/RadioListItem.tsx similarity index 87% rename from src/components/SelectionList/RadioListItem.js rename to src/components/SelectionList/RadioListItem.tsx index 2de0c96932ea..769eaa80df4b 100644 --- a/src/components/SelectionList/RadioListItem.js +++ b/src/components/SelectionList/RadioListItem.tsx @@ -3,10 +3,11 @@ import {View} from 'react-native'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useThemeStyles from '@hooks/useThemeStyles'; -import {radioListItemPropTypes} from './selectionListPropTypes'; +import type {RadioListItemProps} from './types'; -function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}) { +function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}: RadioListItemProps) { const styles = useThemeStyles(); + return ( - {Boolean(item.alternateText) && ( + {!!item.alternateText && ( - {Boolean(item.icons) && ( + {!!item.icons && ( )} @@ -26,19 +23,19 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style text={item.text} > {item.text} - {Boolean(item.alternateText) && ( + {!!item.alternateText && ( {item.alternateText} @@ -46,12 +43,11 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style )} - {Boolean(item.rightElement) && item.rightElement} + {!!item.rightElement && item.rightElement} ); } UserListItem.displayName = 'UserListItem'; -UserListItem.propTypes = userListItemPropTypes; export default UserListItem; diff --git a/src/components/SelectionList/index.android.js b/src/components/SelectionList/index.android.js deleted file mode 100644 index 53d5b6bbce06..000000000000 --- a/src/components/SelectionList/index.android.js +++ /dev/null @@ -1,17 +0,0 @@ -import React, {forwardRef} from 'react'; -import {Keyboard} from 'react-native'; -import BaseSelectionList from './BaseSelectionList'; - -const SelectionList = forwardRef((props, ref) => ( - Keyboard.dismiss()} - /> -)); - -SelectionList.displayName = 'SelectionList'; - -export default SelectionList; diff --git a/src/components/SelectionList/index.android.tsx b/src/components/SelectionList/index.android.tsx new file mode 100644 index 000000000000..8487c6e2cc67 --- /dev/null +++ b/src/components/SelectionList/index.android.tsx @@ -0,0 +1,22 @@ +import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; +import {Keyboard} from 'react-native'; +import type {TextInput} from 'react-native'; +import BaseSelectionList from './BaseSelectionList'; +import type {BaseSelectionListProps, RadioItem, User} from './types'; + +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { + return ( + Keyboard.dismiss()} + /> + ); +} + +SelectionList.displayName = 'SelectionList'; + +export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/index.ios.js b/src/components/SelectionList/index.ios.js deleted file mode 100644 index 7f2a282aeb89..000000000000 --- a/src/components/SelectionList/index.ios.js +++ /dev/null @@ -1,16 +0,0 @@ -import React, {forwardRef} from 'react'; -import {Keyboard} from 'react-native'; -import BaseSelectionList from './BaseSelectionList'; - -const SelectionList = forwardRef((props, ref) => ( - Keyboard.dismiss()} - /> -)); - -SelectionList.displayName = 'SelectionList'; - -export default SelectionList; diff --git a/src/components/SelectionList/index.ios.tsx b/src/components/SelectionList/index.ios.tsx new file mode 100644 index 000000000000..9c32d38314e2 --- /dev/null +++ b/src/components/SelectionList/index.ios.tsx @@ -0,0 +1,21 @@ +import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; +import {Keyboard} from 'react-native'; +import type {TextInput} from 'react-native'; +import BaseSelectionList from './BaseSelectionList'; +import type {BaseSelectionListProps, RadioItem, User} from './types'; + +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { + return ( + + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + ref={ref} + onScrollBeginDrag={() => Keyboard.dismiss()} + /> + ); +} + +SelectionList.displayName = 'SelectionList'; + +export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/index.js b/src/components/SelectionList/index.tsx similarity index 82% rename from src/components/SelectionList/index.js rename to src/components/SelectionList/index.tsx index 24ea60d29be5..93754926cacb 100644 --- a/src/components/SelectionList/index.js +++ b/src/components/SelectionList/index.tsx @@ -1,9 +1,12 @@ import React, {forwardRef, useEffect, useState} from 'react'; +import type {ForwardedRef} from 'react'; import {Keyboard} from 'react-native'; +import type {TextInput} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import BaseSelectionList from './BaseSelectionList'; +import type {BaseSelectionListProps, RadioItem, User} from './types'; -const SelectionList = forwardRef((props, ref) => { +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { const [isScreenTouched, setIsScreenTouched] = useState(false); const touchStart = () => setIsScreenTouched(true); @@ -39,8 +42,8 @@ const SelectionList = forwardRef((props, ref) => { }} /> ); -}); +} SelectionList.displayName = 'SelectionList'; -export default SelectionList; +export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts new file mode 100644 index 000000000000..a82ddef6febb --- /dev/null +++ b/src/components/SelectionList/types.ts @@ -0,0 +1,265 @@ +import type {ReactElement, ReactNode} from 'react'; +import type {GestureResponderEvent, InputModeOptions, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type CommonListItemProps = { + /** Whether this item is focused (for arrow key controls) */ + isFocused?: boolean; + + /** Style to be applied to Text */ + textStyles?: StyleProp; + + /** Style to be applied on the alternate text */ + alternateTextStyles?: StyleProp; + + /** Whether this item is disabled */ + isDisabled?: boolean; + + /** Whether this item should show Tooltip */ + showTooltip: boolean; + + /** Whether to use the Checkbox (multiple selection) instead of the Checkmark (single selection) */ + canSelectMultiple?: boolean; + + /** Callback to fire when the item is pressed */ + onSelectRow: (item: TItem) => void; + + /** Callback to fire when an error is dismissed */ + onDismissError?: (item: TItem) => void; + + /** Component to display on the right side */ + rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; +}; + +type User = { + /** Text to display */ + text: string; + + /** Alternate text to display */ + alternateText?: string; + + /** Key used internally by React */ + keyForList: string; + + /** Whether this option is selected */ + isSelected?: boolean; + + /** Whether this option is disabled for selection */ + isDisabled?: boolean; + + /** User accountID */ + accountID?: number; + + /** User login */ + login?: string; + + /** Element to show on the right side of the item */ + rightElement?: ReactElement; + + /** Icons for the user (can be multiple if it's a Workspace) */ + icons?: Icon[]; + + /** Errors that this user may contain */ + errors?: Errors; + + /** The type of action that's pending */ + pendingAction?: PendingAction; + + invitedSecondaryLogin?: string; + + /** Represents the index of the section it came from */ + sectionIndex?: number; + + /** Represents the index of the option within the section it came from */ + index?: number; +}; + +type UserListItemProps = CommonListItemProps & { + /** The section list item */ + item: User; + + /** Additional styles to apply to text */ + style?: StyleProp; +}; + +type RadioItem = { + /** Text to display */ + text: string; + + /** Alternate text to display */ + alternateText?: string; + + /** Key used internally by React */ + keyForList: string; + + /** Whether this option is selected */ + isSelected?: boolean; + + /** Whether this option is disabled for selection */ + isDisabled?: boolean; + + /** Represents the index of the section it came from */ + sectionIndex?: number; + + /** Represents the index of the option within the section it came from */ + index?: number; +}; + +type RadioListItemProps = CommonListItemProps & { + /** The section list item */ + item: RadioItem; +}; + +type BaseListItemProps = CommonListItemProps & { + item: TItem; + shouldPreventDefaultFocusOnSelectRow?: boolean; + keyForList?: string; +}; + +type Section = { + /** Title of the section */ + title?: string; + + /** The initial index of this section given the total number of options in each section's data array */ + indexOffset?: number; + + /** Array of options */ + data?: TItem[]; + + /** Whether this section items disabled for selection */ + isDisabled?: boolean; +}; + +type BaseSelectionListProps = Partial & { + /** Sections for the section list */ + sections: Array>>; + + /** Whether this is a multi-select list */ + canSelectMultiple?: boolean; + + /** Callback to fire when a row is pressed */ + onSelectRow: (item: TItem) => void; + + /** Callback to fire when "Select All" checkbox is pressed. Only use along with `canSelectMultiple` */ + onSelectAll?: () => void; + + /** Callback to fire when an error is dismissed */ + onDismissError?: () => void; + + /** Label for the text input */ + textInputLabel?: string; + + /** Placeholder for the text input */ + textInputPlaceholder?: string; + + /** Hint for the text input */ + textInputHint?: string; + + /** Value for the text input */ + textInputValue?: string; + + /** Max length for the text input */ + textInputMaxLength?: number; + + /** Callback to fire when the text input changes */ + onChangeText?: (text: string) => void; + + /** Input mode for the text input */ + inputMode?: InputModeOptions; + + /** Item `keyForList` to focus initially */ + initiallyFocusedOptionKey?: string; + + /** Callback to fire when the list is scrolled */ + onScroll?: () => void; + + /** Callback to fire when the list is scrolled and the user begins dragging */ + onScrollBeginDrag?: () => void; + + /** Message to display at the top of the list */ + headerMessage?: string; + + /** Text to display on the confirm button */ + confirmButtonText?: string; + + /** Callback to fire when the confirm button is pressed */ + onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined) => void; + + /** Whether to show the vertical scroll indicator */ + showScrollIndicator?: boolean; + + /** Whether to show the loading placeholder */ + showLoadingPlaceholder?: boolean; + + /** Whether to show the default confirm button */ + showConfirmButton?: boolean; + + /** Whether tooltips should be shown */ + shouldShowTooltips?: boolean; + + /** Whether to stop automatic form submission on pressing enter key or not */ + shouldStopPropagation?: boolean; + + /** Whether to prevent default focusing of options and focus the textinput when selecting an option */ + shouldPreventDefaultFocusOnSelectRow?: boolean; + + /** Custom content to display in the header */ + headerContent?: ReactNode; + + /** Custom content to display in the footer */ + footerContent?: ReactNode; + + /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */ + shouldUseDynamicMaxToRenderPerBatch?: boolean; + + /** Whether keyboard shortcuts should be disabled */ + disableKeyboardShortcuts?: boolean; + + /** Whether to disable initial styling for focused option */ + disableInitialFocusOptionStyle?: boolean; + + /** Styles to apply to SelectionList container */ + containerStyle?: ViewStyle; + + /** Whether keyboard is visible on the screen */ + isKeyboardShown?: boolean; + + /** Whether focus event should be delayed */ + shouldDelayFocus?: boolean; + + /** Component to display on the right side of each child */ + rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; +}; + +type ItemLayout = { + length: number; + offset: number; +}; + +type FlattenedSectionsReturn = { + allOptions: TItem[]; + selectedOptions: TItem[]; + disabledOptionsIndexes: number[]; + itemLayouts: ItemLayout[]; + allSelected: boolean; +}; + +type ButtonOrCheckBoxRoles = 'button' | 'checkbox'; + +type SectionListDataType = SectionListData>; + +export type { + BaseSelectionListProps, + CommonListItemProps, + UserListItemProps, + Section, + RadioListItemProps, + BaseListItemProps, + User, + RadioItem, + FlattenedSectionsReturn, + ItemLayout, + ButtonOrCheckBoxRoles, + SectionListDataType, +}; diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.tsx similarity index 73% rename from src/components/StatePicker/StateSelectorModal.js rename to src/components/StatePicker/StateSelectorModal.tsx index 003211478529..798d3be7a698 100644 --- a/src/components/StatePicker/StateSelectorModal.js +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -1,7 +1,5 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; @@ -9,40 +7,36 @@ import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import searchCountryOptions from '@libs/searchCountryOptions'; +import type {CountryData} from '@libs/searchCountryOptions'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; -const propTypes = { +type State = keyof typeof COMMON_CONST.STATES; + +type StateSelectorModalProps = { /** Whether the modal is visible */ - isVisible: PropTypes.bool.isRequired, + isVisible: boolean; /** State value selected */ - currentState: PropTypes.string, + currentState?: State; /** Function to call when the user selects a State */ - onStateSelected: PropTypes.func, + onStateSelected?: (state: CountryData) => void; /** Function to call when the user closes the State modal */ - onClose: PropTypes.func, + onClose?: () => void; /** The search value from the selection list */ - searchValue: PropTypes.string.isRequired, + searchValue: string; /** Function to call when the user types in the search input */ - setSearchValue: PropTypes.func.isRequired, + setSearchValue: (value: string) => void; /** Label to display on field */ - label: PropTypes.string, -}; - -const defaultProps = { - currentState: '', - onClose: () => {}, - onStateSelected: () => {}, - label: undefined, + label?: string; }; -function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, searchValue, setSearchValue, label}) { +function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -53,11 +47,11 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, setSearchValue(''); }, [isVisible, setSearchValue]); - const countryStates = useMemo( + const countryStates: CountryData[] = useMemo( () => - _.map(_.keys(COMMON_CONST.STATES), (state) => { - const stateName = translate(`allStates.${state}.stateName`); - const stateISO = translate(`allStates.${state}.stateISO`); + Object.keys(COMMON_CONST.STATES).map((state) => { + const stateName = translate(`allStates.${state as State}.stateName`); + const stateISO = translate(`allStates.${state as State}.stateISO`); return { value: stateISO, keyForList: stateISO, @@ -88,12 +82,16 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, testID={StateSelectorModal.displayName} > void; /** Label to display on field */ - label: PropTypes.string, + label?: string; /** Callback to call when the picker modal is dismissed */ - onBlur: PropTypes.func, -}; - -const defaultProps = { - value: undefined, - forwardedRef: undefined, - errorText: '', - onInputChange: () => {}, - label: undefined, - onBlur: () => {}, + onBlur?: () => void; }; -function StatePicker({value, errorText, onInputChange, forwardedRef, label, onBlur}) { +function StatePicker({value, onInputChange, label, onBlur, errorText = ''}: StatePickerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); @@ -51,29 +39,31 @@ function StatePicker({value, errorText, onInputChange, forwardedRef, label, onBl const hidePickerModal = (shouldBlur = true) => { if (shouldBlur) { - onBlur(); + onBlur?.(); } setIsPickerVisible(false); }; - const updateStateInput = (state) => { + const updateStateInput = (state: CountryData) => { if (state.value !== value) { - onInputChange(state.value); + onInputChange?.(state.value); } // If the user selects any state, call the hidePickerModal function with shouldBlur = false // to prevent the onBlur function from being called. hidePickerModal(false); }; - const title = value && _.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; + const title = value && Object.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; const descStyle = title.length === 0 ? styles.textNormal : null; return ( ( - -)); - -StatePickerWithRef.displayName = 'StatePickerWithRef'; - -export default StatePickerWithRef; +export default React.forwardRef(StatePicker); diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 24a647f900a7..bcbca6e2958b 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -86,3 +86,4 @@ function SubscriptAvatar({mainAvatar, secondaryAvatar, size = CONST.AVATAR_SIZE. SubscriptAvatar.displayName = 'SubscriptAvatar'; export default memo(SubscriptAvatar); +export type {SubscriptAvatarProps}; 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..8a959b5da550 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', @@ -582,7 +583,7 @@ export default { canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', - receiptScanning: 'Receipt scan in progress…', + receiptScanning: 'Scan in progress…', receiptMissingDetails: 'Receipt missing details', receiptStatusTitle: 'Scanning…', receiptStatusText: "Only you can see this receipt when it's scanning. Check back later or enter the details now.", @@ -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..271e564c9b1f 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 ', @@ -574,7 +575,7 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', - receiptScanning: 'Escaneo de recibo en curso…', + receiptScanning: 'Escaneo en curso…', receiptMissingDetails: 'Recibo con campos vacíos', receiptStatusTitle: 'Escaneando…', receiptStatusText: 'Solo tú puedes ver este recibo cuando se está escaneando. Vuelve más tarde o introduce los detalles ahora.', @@ -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/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index cf5eed232212..c9325206e5b2 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -270,7 +270,6 @@ const EditRequestStackNavigator = createModalStackNavigator({ - [SCREENS.PRIVATE_NOTES.VIEW]: () => require('../../../pages/PrivateNotes/PrivateNotesViewPage').default as React.ComponentType, [SCREENS.PRIVATE_NOTES.LIST]: () => require('../../../pages/PrivateNotes/PrivateNotesListPage').default as React.ComponentType, [SCREENS.PRIVATE_NOTES.EDIT]: () => require('../../../pages/PrivateNotes/PrivateNotesEditPage').default as React.ComponentType, }); diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index 0951b41d78b5..5df2bcf0e57b 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -281,7 +281,6 @@ const linkingConfig: LinkingOptions = { }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { screens: { - [SCREENS.PRIVATE_NOTES.VIEW]: ROUTES.PRIVATE_NOTES_VIEW.route, [SCREENS.PRIVATE_NOTES.LIST]: ROUTES.PRIVATE_NOTES_LIST.route, [SCREENS.PRIVATE_NOTES.EDIT]: ROUTES.PRIVATE_NOTES_EDIT.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index dd5a7720f00d..2371c764f42a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -333,14 +333,7 @@ type ProcessMoneyRequestHoldNavigatorParamList = { }; type PrivateNotesNavigatorParamList = { - [SCREENS.PRIVATE_NOTES.VIEW]: { - reportID: string; - accountID: string; - }; - [SCREENS.PRIVATE_NOTES.LIST]: { - reportID: string; - accountID: string; - }; + [SCREENS.PRIVATE_NOTES.LIST]: undefined; [SCREENS.PRIVATE_NOTES.EDIT]: { reportID: string; accountID: string; 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/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 2973228af51f..d44df3c6c39c 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -7,6 +7,7 @@ import Onyx from 'react-native-onyx'; import _ from 'underscore'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; @@ -14,6 +15,7 @@ import * as Localize from './Localize'; import * as LoginUtils from './LoginUtils'; import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import Navigation from './Navigation/Navigation'; +import Performance from './Performance'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PhoneNumber from './PhoneNumber'; @@ -1651,7 +1653,9 @@ function getOptions( * @returns {Object} */ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { - return getOptions(reports, personalDetails, { + Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); + Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); + const options = getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1666,6 +1670,10 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { includeMoneyRequests: true, includeTasks: true, }); + Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); + Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); + + return options; } /** diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 19f8ccf61720..e9c3b1710cc0 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; @@ -3707,7 +3708,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/IOU.js b/src/libs/actions/IOU.js index d258b5419103..eb9541edcad2 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1169,7 +1169,7 @@ function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, va } /** - * Updates the created date of a money request + * Updates the tag of a money request * * @param {String} transactionID * @param {Number} transactionThreadReportID @@ -1183,6 +1183,21 @@ function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag) { API.write('UpdateMoneyRequestTag', params, onyxData); } +/** + * Updates the waypoints of a distance money request + * + * @param {String} transactionID + * @param {Number} transactionThreadReportID + * @param {Object} waypoints + */ +function updateMoneyRequestDistance(transactionID, transactionThreadReportID, waypoints) { + const transactionChanges = { + waypoints, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + API.write('UpdateMoneyRequestDistance', params, onyxData); +} + /** * Updates the category of a money request * @@ -3734,6 +3749,7 @@ export { updateMoneyRequestBillable, updateMoneyRequestMerchant, updateMoneyRequestTag, + updateMoneyRequestDistance, updateMoneyRequestCategory, updateMoneyRequestAmountAndCurrency, updateMoneyRequestDescription, 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 c44c28233026..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); } @@ -976,7 +981,7 @@ function markCommentAsUnread(reportID: string, reportActionCreated: string) { }, null); // If no action created date is provided, use the last action's from other user - const actionCreationTime = reportActionCreated || (latestReportActionFromOtherUsers?.created ?? DateUtils.getDBTime(0)); + const actionCreationTime = reportActionCreated || (latestReportActionFromOtherUsers?.created ?? allReports?.[reportID]?.lastVisibleActionCreated ?? DateUtils.getDBTime(0)); // We subtract 1 millisecond so that the lastReadTime is updated to just before a given reportAction's created date // For example, if we want to mark a report action with ID 100 and created date '2014-04-01 16:07:02.999' unread, we set the lastReadTime to '2014-04-01 16:07:02.998' 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/libs/searchCountryOptions.ts b/src/libs/searchCountryOptions.ts index 8fb1cc9c37f3..1fc5d343f556 100644 --- a/src/libs/searchCountryOptions.ts +++ b/src/libs/searchCountryOptions.ts @@ -37,3 +37,4 @@ function searchCountryOptions(searchValue: string, countriesData: CountryData[]) } export default searchCountryOptions; +export type {CountryData}; diff --git a/src/libs/updateMultilineInputRange/types.ts b/src/libs/updateMultilineInputRange/types.ts index d1b134b09a99..ce8f553c51f8 100644 --- a/src/libs/updateMultilineInputRange/types.ts +++ b/src/libs/updateMultilineInputRange/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput, shouldAutoFocus?: boolean) => void; +type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput | null, shouldAutoFocus?: boolean) => void; export default UpdateMultilineInputRange; diff --git a/src/pages/EditRequestDistancePage.js b/src/pages/EditRequestDistancePage.js index 0ea295c0780b..f3ea76a3390a 100644 --- a/src/pages/EditRequestDistancePage.js +++ b/src/pages/EditRequestDistancePage.js @@ -79,7 +79,7 @@ function EditRequestDistancePage({report, route, transaction, transactionBackup} return; } - IOU.editMoneyRequest(transaction, report.reportID, {waypoints}); + IOU.updateMoneyRequestDistance(transaction.transactionID, report.reportID, waypoints); // If the client is offline, then the modal can be closed as well (because there are no errors or other feedback to show them // until they come online again and sync with the server). 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')} - - - -