diff --git a/.eslintrc.js b/.eslintrc.js
index f852c970f85c..281f8269804e 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -1,12 +1,13 @@
const restrictedImportPaths = [
{
name: 'react-native',
- importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable'],
+ importNames: ['useWindowDimensions', 'StatusBar', 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', 'Text'],
message: [
'',
"For 'useWindowDimensions', please use 'src/hooks/useWindowDimensions' instead.",
"For 'TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight', 'Pressable', please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.",
"For 'StatusBar', please use 'src/libs/StatusBar' instead.",
+ "For 'Text', please use '@components/Text' instead.",
].join('\n'),
},
{
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 3d10b31e72f5..25ff4448ecb8 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 1001042507
- versionName "1.4.25-7"
+ versionCode 1001042700
+ versionName "1.4.27-0"
}
flavorDimensions "default"
diff --git a/assets/images/chatbubble-add.svg b/assets/images/chatbubble-add.svg
new file mode 100644
index 000000000000..047a43073b3c
--- /dev/null
+++ b/assets/images/chatbubble-add.svg
@@ -0,0 +1,13 @@
+
+
+
diff --git a/assets/images/chatbubble-unread.svg b/assets/images/chatbubble-unread.svg
new file mode 100644
index 000000000000..9da789510276
--- /dev/null
+++ b/assets/images/chatbubble-unread.svg
@@ -0,0 +1,12 @@
+
+
+
diff --git a/contributingGuides/CONTRIBUTING.md b/contributingGuides/CONTRIBUTING.md
index 186d7def3423..9eb16099f535 100644
--- a/contributingGuides/CONTRIBUTING.md
+++ b/contributingGuides/CONTRIBUTING.md
@@ -5,7 +5,7 @@ Welcome! Thanks for checking out the New Expensify app and taking the time to co
If you would like to become an Expensify contributor, the first step is to read this document in its **entirety**. The second step is to review the README guidelines [here](https://github.com/Expensify/App/blob/main/README.md) to understand our coding philosophy and for a general overview of the code repository (i.e. how to run the app locally, testing, storage, our app philosophy, etc). Please read both documents before asking questions, as it may be covered within the documentation.
#### Test Accounts
-You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar. Do use Expensify employee or customer accounts for testing.
+You can create as many accounts as needed in order to test your changes directly from [the app](https://new.expensify.com/). An initial account can be created when logging in for the first time, and additional accounts can be created by opening the "New Chat" or "Group Chat" pages via the Global Create menu, inputting a valid email or phone number, and tapping the user's avatar. Do not use Expensify employee or customer accounts for testing.
**Notes**:
diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md b/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md
deleted file mode 100644
index e6d8f2fedb73..000000000000
--- a/docs/articles/expensify-classic/billing-and-subscriptions/Free-Trial.md
+++ /dev/null
@@ -1,46 +0,0 @@
----
-title: Free Trial
-description: Learn more about your free trial with Expensify.
----
-
-# Overview
-New customers can take advantage of a seven-day Free Trial on a group Workspace. This trial period allows you to fully explore Expensify's features and capabilities before deciding on a subscription.
-During the trial, your organization will have complete access to all the features and functionality offered by the Collect or Control workspace plan. This post provides a step-by-step guide on how to begin, oversee, and successfully conclude your organization's Expensify Free Trial.
-
-# How to start a Free Trial
-1. Sign up for a new Expensify account at expensify.com.
-2. After you've signed up for a new Expensify account, you will see a task on your Home page asking if you are using Expensify for your business or as an individual.
- a. **Note**: If you select “Individual”, Expensify is free for individuals for up to 25 SmartScans per month. Selecting Individual will **not** start a Free Trial. More details on individual subscriptions can be found [here](https://help.expensify.com/articles/expensify-classic/billing-and-subscriptions/Individual-Subscription).
-3. Select the Business option.
-4. Select which Expensify features you'd like to set up for your organization.
-5. Congratulations, your seven-day Free Trial has started!
-
-Once you've made these selections, we'll automatically enroll you in a Free Trial and create a Group Workspace, which will trigger new tasks on your Home page to walk you through how to configure Expensify for your organization. If you have any questions about a specific task or need assistance setting up your company, you can speak with your designated Setup Specialist by clicking “Support” on the left-hand navigation menu and selecting their name. This will allow you to message your Setup Specialist, and request a call if you need.
-
-# How to unlock additional Free Trial weeks
-When you begin a Free Trial, you'll have an initial seven-day period before you need to provide your billing information to continue using Expensify. Luckily, Expensify offers the option to extend your Free Trial by an additional five weeks!
-
-To access these extra free weeks, all you need to do is complete the tasks on your Home page marked with the "Free Week!" banner. Each task completed in this category will automatically add seven more days to your trial. You can easily keep track of the remaining days of your Free Trial by checking the top right-hand corner of your Expensify Home page.
-
-# How to make the most of your Free Trial
-- Complete all of the "Free Week!" tasks right away. These tasks are crucial for establishing your organization's Workspace, and finishing them will give you a clear idea of how much time you have left in your Free Trial.
-
-- Every Free Trial has dedicated access to a Setup Specialist who can help you set up your account to your preferences. We highly recommend booking a call with your dedicated Setup Specialist as soon as you start your Free Trial. If you ever need assistance with a setup task, your tasks also include demo videos.
-
-- Invite a few employees to join Expensify as early as possible during the Free Trial. Bringing employees on board and having them submit expenses will allow you to fully experience how all of the features and functionalities of Expensify work together to save time. We provide excellent resources to help employees get started with Expensify.
-
-- Establish a connection between Expensify and your accounting system from the outset. By doing this early, you can start testing Expensify comprehensively from end to end.
-
-{% include faq-begin.md %}
-## What happens when my Free Trial ends?
-If you’ve already added a billing card to Expensify, you will automatically start your organization’s Expensify subscription after your Free Trial ends. At the beginning of the following month, we'll bill the card you have on file for your subscription, adjusting the charge to exclude the Free Trial period.
-If your Free Trial concludes without a billing card on file, you will see a notification on your Home page saying, 'Your Free Trial has expired.'
-If you still have outstanding 'Free Week!' tasks, completing them will extend your Free Trial by additional days.
-If you continue without adding a billing card, you will be granted a five-day grace period after the following billing cycle before all Group Workspace functionality is disabled. To continue using Expensify's Group Workspace features, you will need to input your billing card information and initiate a subscription.
-
-## How can I downgrade my account after my Free Trial ends?
-If you’d like to downgrade to an individual account after your Free Trial has ended, you will need to delete any Group Workspace that you have created. This action will remove the Workspaces, subscription, and any amount owed. You can do this in one of two ways from the Expensify web app:
-- Select the “Downgrade” option on the billing card task on your Home page.
-- Go to **Settings > Workspaces > [Workspace name]**, then click the gear button next to the Workspace and select Delete.
-
-{% include faq-end.md %}
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index c76c947aafd9..ca8218d53d87 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageTypeAPPLCFBundleShortVersionString
- 1.4.25
+ 1.4.27CFBundleSignature????CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.25.7
+ 1.4.27.0ITSAppUsesNonExemptEncryptionLSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index 1dde1a528b3c..4a261dccc11e 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageTypeBNDLCFBundleShortVersionString
- 1.4.25
+ 1.4.27CFBundleSignature????CFBundleVersion
- 1.4.25.7
+ 1.4.27.0
diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist
index b840fa5cd80a..0ba053ad0026 100644
--- a/ios/NotificationServiceExtension/Info.plist
+++ b/ios/NotificationServiceExtension/Info.plist
@@ -3,9 +3,9 @@
CFBundleShortVersionString
- 1.4.25
+ 1.4.27CFBundleVersion
- 1.4.25.7
+ 1.4.27.0NSExtensionNSExtensionPointIdentifier
diff --git a/package-lock.json b/package-lock.json
index 148bf7157119..1dcdff30b536 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.25-7",
+ "version": "1.4.27-0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.25-7",
+ "version": "1.4.27-0",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -94,7 +94,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.126",
+ "react-native-onyx": "1.0.118",
"react-native-pager-view": "6.2.2",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
@@ -47034,17 +47034,17 @@
}
},
"node_modules/react-native-onyx": {
- "version": "1.0.126",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz",
- "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==",
+ "version": "1.0.118",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz",
+ "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==",
"dependencies": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
"underscore": "^1.13.6"
},
"engines": {
- "node": "20.9.0",
- "npm": "10.1.0"
+ "node": ">=16.15.1 <=20.9.0",
+ "npm": ">=8.11.0 <=10.1.0"
},
"peerDependencies": {
"idb-keyval": "^6.2.1",
@@ -89702,9 +89702,9 @@
}
},
"react-native-onyx": {
- "version": "1.0.126",
- "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.126.tgz",
- "integrity": "sha512-tUJI1mQaWXLfyBFYQQWM6mm9GiCqIXGvjbqJkH1fLY3OqbGW6DyH4CxC+qJrqfi4bKZgZHp5xlBHhkPV4pKK2A==",
+ "version": "1.0.118",
+ "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.118.tgz",
+ "integrity": "sha512-w54jO+Bpu1ElHsrxZXIIpcBqNkrUvuVCQmwWdfOW5LvO4UwsPSwmMxzExbUZ4ip+7CROmm10IgXFaAoyfeYSVQ==",
"requires": {
"ascii-table": "0.0.9",
"fast-equals": "^4.0.3",
diff --git a/package.json b/package.json
index a842ffd2fdb4..5664d2326a57 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.25-7",
+ "version": "1.4.27-0",
"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.",
@@ -142,7 +142,7 @@
"react-native-linear-gradient": "^2.8.1",
"react-native-localize": "^2.2.6",
"react-native-modal": "^13.0.0",
- "react-native-onyx": "1.0.126",
+ "react-native-onyx": "1.0.118",
"react-native-pager-view": "6.2.2",
"react-native-pdf": "6.7.3",
"react-native-performance": "^5.1.0",
diff --git a/patches/react-native-blob-util+0.17.3.patch b/patches/react-native-blob-util+0.17.3.patch
new file mode 100644
index 000000000000..2ade175a7b30
--- /dev/null
+++ b/patches/react-native-blob-util+0.17.3.patch
@@ -0,0 +1,17 @@
+diff --git a/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java b/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java
+index 4b41402..4f07fc6 100644
+--- a/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java
++++ b/node_modules/react-native-blob-util/android/src/main/java/com/ReactNativeBlobUtil/ReactNativeBlobUtilReq.java
+@@ -279,7 +279,11 @@ public class ReactNativeBlobUtilReq extends BroadcastReceiver implements Runnabl
+ DownloadManager dm = (DownloadManager) appCtx.getSystemService(Context.DOWNLOAD_SERVICE);
+ downloadManagerId = dm.enqueue(req);
+ androidDownloadManagerTaskTable.put(taskId, Long.valueOf(downloadManagerId));
+- appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
++ if(Build.VERSION.SDK_INT >= 34 ){
++ appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), Context.RECEIVER_EXPORTED);
++ }else{
++ appCtx.registerReceiver(this, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
++ }
+ future = scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
+ @Override
+ public void run() {
diff --git a/src/CONST.ts b/src/CONST.ts
index d6f3d3cdcef6..e5b44042a550 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -603,11 +603,9 @@ const CONST = {
},
THREAD_DISABLED: ['CREATED'],
},
- CANCEL_PAYMENT_REASONS: {
- ADMIN: 'CANCEL_REASON_ADMIN',
- },
ACTIONABLE_MENTION_WHISPER_RESOLUTION: {
INVITE: 'invited',
+ NOTHING: 'nothing',
},
ARCHIVE_REASON: {
DEFAULT: 'default',
@@ -3121,6 +3119,8 @@ const CONST = {
EMAIL: 'EMAIL',
REPORT: 'REPORT',
},
+
+ MINI_CONTEXT_MENU_MAX_ITEMS: 4,
} as const;
export default CONST;
diff --git a/src/components/AddressSearch/CurrentLocationButton.js b/src/components/AddressSearch/CurrentLocationButton.tsx
similarity index 72%
rename from src/components/AddressSearch/CurrentLocationButton.js
rename to src/components/AddressSearch/CurrentLocationButton.tsx
index 06541565f567..11bd0a64eba5 100644
--- a/src/components/AddressSearch/CurrentLocationButton.js
+++ b/src/components/AddressSearch/CurrentLocationButton.tsx
@@ -1,29 +1,16 @@
-import PropTypes from 'prop-types';
import React from 'react';
-import {Text} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import getButtonState from '@libs/getButtonState';
import colors from '@styles/theme/colors';
+import type {CurrentLocationButtonProps} from './types';
-const propTypes = {
- /** Callback that runs when location button is clicked */
- onPress: PropTypes.func,
-
- /** Boolean to indicate if the button is clickable */
- isDisabled: PropTypes.bool,
-};
-
-const defaultProps = {
- isDisabled: false,
- onPress: () => {},
-};
-
-function CurrentLocationButton({onPress, isDisabled}) {
+function CurrentLocationButton({onPress, isDisabled = false}: CurrentLocationButtonProps) {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
@@ -32,7 +19,7 @@ function CurrentLocationButton({onPress, isDisabled}) {
onPress?.()}
accessibilityLabel={translate('location.useCurrent')}
disabled={isDisabled}
onMouseDown={(e) => e.preventDefault()}
@@ -48,7 +35,5 @@ function CurrentLocationButton({onPress, isDisabled}) {
}
CurrentLocationButton.displayName = 'CurrentLocationButton';
-CurrentLocationButton.propTypes = propTypes;
-CurrentLocationButton.defaultProps = defaultProps;
export default CurrentLocationButton;
diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.tsx
similarity index 63%
rename from src/components/AddressSearch/index.js
rename to src/components/AddressSearch/index.tsx
index 357f5af8cb58..89e87eeebe54 100644
--- a/src/components/AddressSearch/index.js
+++ b/src/components/AddressSearch/index.tsx
@@ -1,184 +1,79 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
-import {ActivityIndicator, Keyboard, LogBox, ScrollView, Text, View} from 'react-native';
+import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import type {ForwardedRef} from 'react';
+import {ActivityIndicator, Keyboard, LogBox, ScrollView, View} from 'react-native';
+import type {LayoutChangeEvent} from 'react-native';
import {GooglePlacesAutocomplete} from 'react-native-google-places-autocomplete';
-import _ from 'underscore';
+import type {GooglePlaceData, GooglePlaceDetail} from 'react-native-google-places-autocomplete';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import LocationErrorMessage from '@components/LocationErrorMessage';
-import networkPropTypes from '@components/networkPropTypes';
-import {withNetwork} from '@components/OnyxProvider';
+import Text from '@components/Text';
import TextInput from '@components/TextInput';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ApiUtils from '@libs/ApiUtils';
-import compose from '@libs/compose';
import getCurrentPosition from '@libs/getCurrentPosition';
+import type {GeolocationErrorCodeType} from '@libs/getCurrentPosition/getCurrentPosition.types';
import * as GooglePlacesUtils from '@libs/GooglePlacesUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import CurrentLocationButton from './CurrentLocationButton';
import isCurrentTargetInsideContainer from './isCurrentTargetInsideContainer';
+import type {AddressSearchProps, RenamedInputKeysProps} from './types';
// The error that's being thrown below will be ignored until we fork the
// react-native-google-places-autocomplete repo and replace the
// VirtualizedList component with a VirtualizedList-backed instead
LogBox.ignoreLogs(['VirtualizedLists should never be nested']);
-const propTypes = {
- /** The ID used to uniquely identify the input in a Form */
- inputID: PropTypes.string,
-
- /** Saves a draft of the input value when used in a form */
- shouldSaveDraft: PropTypes.bool,
-
- /** Callback that is called when the text input is blurred */
- onBlur: PropTypes.func,
-
- /** Error text to display */
- errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]),
-
- /** Hint text to display */
- hint: PropTypes.string,
-
- /** The label to display for the field */
- label: PropTypes.string.isRequired,
-
- /** The value to set the field to initially */
- value: PropTypes.string,
-
- /** The value to set the field to initially */
- defaultValue: PropTypes.string,
-
- /** A callback function when the value of this field has changed */
- onInputChange: PropTypes.func.isRequired,
-
- /** A callback function when an address has been auto-selected */
- onPress: PropTypes.func,
-
- /** Customize the TextInput container */
- // eslint-disable-next-line react/forbid-prop-types
- containerStyles: PropTypes.arrayOf(PropTypes.object),
-
- /** Should address search be limited to results in the USA */
- isLimitedToUSA: PropTypes.bool,
-
- /** Shows a current location button in suggestion list */
- canUseCurrentLocation: PropTypes.bool,
-
- /** A list of predefined places that can be shown when the user isn't searching for something */
- predefinedPlaces: PropTypes.arrayOf(
- PropTypes.shape({
- /** A description of the location (usually the address) */
- description: PropTypes.string,
-
- /** The name of the location */
- name: PropTypes.string,
-
- /** Data required by the google auto complete plugin to know where to put the markers on the map */
- geometry: PropTypes.shape({
- /** Data about the location */
- location: PropTypes.shape({
- /** Lattitude of the location */
- lat: PropTypes.number,
-
- /** Longitude of the location */
- lng: PropTypes.number,
- }),
- }),
- }),
- ),
-
- /** A map of inputID key names */
- renamedInputKeys: PropTypes.shape({
- street: PropTypes.string,
- street2: PropTypes.string,
- city: PropTypes.string,
- state: PropTypes.string,
- lat: PropTypes.string,
- lng: PropTypes.string,
- zipCode: PropTypes.string,
- }),
-
- /** Maximum number of characters allowed in search input */
- maxInputLength: PropTypes.number,
-
- /** The result types to return from the Google Places Autocomplete request */
- resultTypes: PropTypes.string,
-
- /** Information about the network */
- network: networkPropTypes.isRequired,
-
- /** Location bias for querying search results. */
- locationBias: PropTypes.string,
-
- ...withLocalizePropTypes,
-};
-
-const defaultProps = {
- inputID: undefined,
- shouldSaveDraft: false,
- onBlur: () => {},
- onPress: () => {},
- errorText: '',
- hint: '',
- value: undefined,
- defaultValue: undefined,
- containerStyles: [],
- isLimitedToUSA: false,
- canUseCurrentLocation: false,
- renamedInputKeys: {
- street: 'addressStreet',
- street2: 'addressStreet2',
- city: 'addressCity',
- state: 'addressState',
- zipCode: 'addressZipCode',
- lat: 'addressLat',
- lng: 'addressLng',
- },
- maxInputLength: undefined,
- predefinedPlaces: [],
- resultTypes: 'address',
- locationBias: undefined,
-};
-
-function AddressSearch({
- canUseCurrentLocation,
- containerStyles,
- defaultValue,
- errorText,
- hint,
- innerRef,
- inputID,
- isLimitedToUSA,
- label,
- maxInputLength,
- network,
- onBlur,
- onInputChange,
- onPress,
- predefinedPlaces,
- preferredLocale,
- renamedInputKeys,
- resultTypes,
- shouldSaveDraft,
- translate,
- value,
- locationBias,
-}) {
+function AddressSearch(
+ {
+ canUseCurrentLocation = false,
+ containerStyles,
+ defaultValue,
+ errorText = '',
+ hint = '',
+ inputID,
+ isLimitedToUSA = false,
+ label,
+ maxInputLength,
+ onBlur,
+ onInputChange,
+ onPress,
+ predefinedPlaces = [],
+ preferredLocale,
+ renamedInputKeys = {
+ street: 'addressStreet',
+ street2: 'addressStreet2',
+ city: 'addressCity',
+ state: 'addressState',
+ zipCode: 'addressZipCode',
+ lat: 'addressLat',
+ lng: 'addressLng',
+ },
+ resultTypes = 'address',
+ shouldSaveDraft = false,
+ value,
+ locationBias,
+ }: AddressSearchProps,
+ ref: ForwardedRef,
+) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
const [displayListViewBorder, setDisplayListViewBorder] = useState(false);
const [isTyping, setIsTyping] = useState(false);
const [isFocused, setIsFocused] = useState(false);
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const [searchValue, setSearchValue] = useState(value || defaultValue || '');
- const [locationErrorCode, setLocationErrorCode] = useState(null);
+ const [locationErrorCode, setLocationErrorCode] = useState(null);
const [isFetchingCurrentLocation, setIsFetchingCurrentLocation] = useState(false);
const shouldTriggerGeolocationCallbacks = useRef(true);
- const containerRef = useRef();
+ const containerRef = useRef(null);
const query = useMemo(
() => ({
language: preferredLocale,
@@ -189,18 +84,18 @@ function AddressSearch({
[preferredLocale, resultTypes, isLimitedToUSA, locationBias],
);
const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;
- const saveLocationDetails = (autocompleteData, details) => {
- const addressComponents = details.address_components;
+ const saveLocationDetails = (autocompleteData: GooglePlaceData, details: GooglePlaceDetail | null) => {
+ const addressComponents = details?.address_components;
if (!addressComponents) {
// When there are details, but no address_components, this indicates that some predefined options have been passed
// to this component which don't match the usual properties coming from auto-complete. In that case, only a limited
// amount of data massaging needs to happen for what the parent expects to get from this function.
- if (_.size(details)) {
- onPress({
- address: autocompleteData.description || lodashGet(details, 'description', ''),
- lat: lodashGet(details, 'geometry.location.lat', 0),
- lng: lodashGet(details, 'geometry.location.lng', 0),
- name: lodashGet(details, 'name'),
+ if (details) {
+ onPress?.({
+ address: autocompleteData.description ?? '',
+ lat: details.geometry.location.lat ?? 0,
+ lng: details.geometry.location.lng ?? 0,
+ name: details.name,
});
}
return;
@@ -219,14 +114,19 @@ function AddressSearch({
administrative_area_level_2: stateFallback,
country: countryPrimary,
} = GooglePlacesUtils.getAddressComponents(addressComponents, {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
street_number: 'long_name',
route: 'long_name',
subpremise: 'long_name',
locality: 'long_name',
sublocality: 'long_name',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
postal_town: 'long_name',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
postal_code: 'long_name',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
administrative_area_level_1: 'short_name',
+ // eslint-disable-next-line @typescript-eslint/naming-convention
administrative_area_level_2: 'long_name',
country: 'short_name',
});
@@ -234,6 +134,7 @@ function AddressSearch({
// The state's iso code (short_name) is needed for the StatePicker component but we also
// need the state's full name (long_name) when we render the state in a TextInput.
const {administrative_area_level_1: longStateName} = GooglePlacesUtils.getAddressComponents(addressComponents, {
+ // eslint-disable-next-line @typescript-eslint/naming-convention
administrative_area_level_1: 'long_name',
});
@@ -243,15 +144,16 @@ function AddressSearch({
country: countryFallbackLongName = '',
state: stateAutoCompleteFallback = '',
city: cityAutocompleteFallback = '',
- } = GooglePlacesUtils.getPlaceAutocompleteTerms(autocompleteData.terms);
+ } = GooglePlacesUtils.getPlaceAutocompleteTerms(autocompleteData?.terms ?? []);
- const countryFallback = _.findKey(CONST.ALL_COUNTRIES, (country) => country === countryFallbackLongName);
+ const countryFallback = Object.keys(CONST.ALL_COUNTRIES).find((country) => country === countryFallbackLongName);
- const country = countryPrimary || countryFallback;
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ const country = countryPrimary || countryFallback || '';
const values = {
street: `${streetNumber} ${streetName}`.trim(),
- name: lodashGet(details, 'name', ''),
+ name: details.name ?? '',
// Autocomplete returns any additional valid address fragments (e.g. Apt #) as subpremise.
street2: subpremise,
// Make sure country is updated first, since city and state will be reset if the country changes
@@ -264,9 +166,9 @@ function AddressSearch({
city: locality || postalTown || sublocality || cityAutocompleteFallback,
zipCode,
- lat: lodashGet(details, 'geometry.location.lat', 0),
- lng: lodashGet(details, 'geometry.location.lng', 0),
- address: autocompleteData.description || lodashGet(details, 'formatted_address', ''),
+ lat: details.geometry.location.lat ?? 0,
+ lng: details.geometry.location.lng ?? 0,
+ address: autocompleteData.description || details.formatted_address || '',
};
// If the address is not in the US, use the full length state name since we're displaying the address's
@@ -282,7 +184,7 @@ function AddressSearch({
}
// Set the state to be the same as the city in case the state is empty.
- if (_.isEmpty(values.state)) {
+ if (!values.state) {
values.state = values.city;
}
@@ -290,8 +192,8 @@ function AddressSearch({
// We are setting up a fallback to ensure "values.street" is populated with a relevant value
if (!values.street && details.adr_address) {
const streetAddressRegex = /([^<]*)<\/span>/;
- const adr_address = details.adr_address.match(streetAddressRegex);
- const streetAddressFallback = lodashGet(adr_address, [1], null);
+ const adrAddress = details.adr_address.match(streetAddressRegex);
+ const streetAddressFallback = adrAddress ? adrAddress?.[1] : null;
if (streetAddressFallback) {
values.street = streetAddressFallback;
}
@@ -299,28 +201,28 @@ function AddressSearch({
// Not all pages define the Address Line 2 field, so in that case we append any additional address details
// (e.g. Apt #) to Address Line 1
- if (subpremise && typeof renamedInputKeys.street2 === 'undefined') {
+ if (subpremise && typeof renamedInputKeys?.street2 === 'undefined') {
values.street += `, ${subpremise}`;
}
- const isValidCountryCode = lodashGet(CONST.ALL_COUNTRIES, country);
+ const isValidCountryCode = !!Object.keys(CONST.ALL_COUNTRIES).find((foundCountry) => foundCountry === country);
if (isValidCountryCode) {
values.country = country;
}
if (inputID) {
- _.each(values, (inputValue, key) => {
- const inputKey = lodashGet(renamedInputKeys, key, key);
+ Object.entries(values).forEach(([key, inputValue]) => {
+ const inputKey = renamedInputKeys?.[key as keyof RenamedInputKeysProps] ?? key;
if (!inputKey) {
return;
}
- onInputChange(inputValue, inputKey);
+ onInputChange?.(inputValue, inputKey);
});
} else {
- onInputChange(values);
+ onInputChange?.(values);
}
- onPress(values);
+ onPress?.(values);
};
/** Gets the user's current location and registers success/error callbacks */
@@ -351,7 +253,7 @@ function AddressSearch({
address: CONST.YOUR_LOCATION_TEXT,
name: CONST.YOUR_LOCATION_TEXT,
};
- onPress(location);
+ onPress?.(location);
},
(errorData) => {
if (!shouldTriggerGeolocationCallbacks.current) {
@@ -368,19 +270,22 @@ function AddressSearch({
);
};
- const renderHeaderComponent = () =>
- predefinedPlaces.length > 0 && (
- <>
- {/* This will show current location button in list if there are some recent destinations */}
- {shouldShowCurrentLocationButton && (
-
- )}
- {!value && {translate('common.recentDestinations')}}
- >
- );
+ const renderHeaderComponent = () => (
+ <>
+ {predefinedPlaces.length > 0 && (
+ <>
+ {/* This will show current location button in list if there are some recent destinations */}
+ {shouldShowCurrentLocationButton && (
+
+ )}
+ {!value && {translate('common.recentDestinations')}}
+ >
+ )}
+ >
+ );
// eslint-disable-next-line arrow-body-style
useEffect(() => {
@@ -392,10 +297,8 @@ function AddressSearch({
const listEmptyComponent = useCallback(
() =>
- network.isOffline || !isTyping ? null : (
- {translate('common.noResultsFound')}
- ),
- [network.isOffline, isTyping, styles, translate],
+ !!isOffline || !isTyping ? null : {translate('common.noResultsFound')},
+ [isOffline, isTyping, styles, translate],
);
const listLoader = useCallback(
@@ -464,27 +367,15 @@ function AddressSearch({
query={query}
requestUrl={{
useOnPlatform: 'all',
- url: network.isOffline ? null : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
+ url: isOffline ? '' : ApiUtils.getCommandURL({command: 'Proxy_GooglePlaces&proxyUrl='}),
}}
textInputProps={{
InputComp: TextInput,
- ref: (node) => {
- if (!innerRef) {
- return;
- }
-
- if (_.isFunction(innerRef)) {
- innerRef(node);
- return;
- }
-
- // eslint-disable-next-line no-param-reassign
- innerRef.current = node;
- },
+ ref,
label,
containerStyles,
errorText,
- hint: displayListViewBorder || (predefinedPlaces.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint,
+ hint: displayListViewBorder || (predefinedPlaces?.length === 0 && shouldShowCurrentLocationButton) || (canUseCurrentLocation && isTyping) ? undefined : hint,
value,
defaultValue,
inputID,
@@ -498,20 +389,19 @@ function AddressSearch({
setIsFocused(false);
setIsTyping(false);
}
- onBlur();
+ onBlur?.();
},
autoComplete: 'off',
- onInputChange: (text) => {
+ onInputChange: (text: string) => {
setSearchValue(text);
setIsTyping(true);
if (inputID) {
- onInputChange(text);
+ onInputChange?.(text);
} else {
onInputChange({street: text});
}
-
// If the text is empty and we have no predefined places, we set displayListViewBorder to false to prevent UI flickering
- if (_.isEmpty(text) && _.isEmpty(predefinedPlaces)) {
+ if (!text && !predefinedPlaces.length) {
setDisplayListViewBorder(false);
}
},
@@ -530,22 +420,21 @@ function AddressSearch({
isRowScrollable={false}
listHoverColor={theme.border}
listUnderlayColor={theme.buttonPressedBG}
- onLayout={(event) => {
+ onLayout={(event: LayoutChangeEvent) => {
// We use the height of the element to determine if we should hide the border of the listView dropdown
// to prevent a lingering border when there are no address suggestions.
setDisplayListViewBorder(event.nativeEvent.layout.height > variables.googleEmptyListViewHeight);
}}
inbetweenCompo={
// We want to show the current location button even if there are no recent destinations
- predefinedPlaces.length === 0 && shouldShowCurrentLocationButton ? (
+ predefinedPlaces?.length === 0 &&
+ shouldShowCurrentLocationButton && (
- ) : (
- <>>
)
}
placeholder=""
@@ -561,18 +450,6 @@ function AddressSearch({
);
}
-AddressSearch.propTypes = propTypes;
-AddressSearch.defaultProps = defaultProps;
-AddressSearch.displayName = 'AddressSearch';
-
-const AddressSearchWithRef = React.forwardRef((props, ref) => (
-
-));
-
-AddressSearchWithRef.displayName = 'AddressSearchWithRef';
+AddressSearch.displayName = 'AddressSearchWithRef';
-export default compose(withNetwork(), withLocalize)(AddressSearchWithRef);
+export default forwardRef(AddressSearch);
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.js b/src/components/AddressSearch/isCurrentTargetInsideContainer.js
deleted file mode 100644
index 18bfc10a8dcb..000000000000
--- a/src/components/AddressSearch/isCurrentTargetInsideContainer.js
+++ /dev/null
@@ -1,8 +0,0 @@
-function isCurrentTargetInsideContainer(event, containerRef) {
- // The related target check is required here
- // because without it when we select an option, the onBlur will still trigger setting displayListViewBorder to false
- // it will make the auto complete component re-render before onPress is called making selecting an option not working.
- return containerRef.current && event.target && containerRef.current.contains(event.relatedTarget);
-}
-
-export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js
deleted file mode 100644
index dbf0004b08d9..000000000000
--- a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.js
+++ /dev/null
@@ -1,6 +0,0 @@
-function isCurrentTargetInsideContainer() {
- // The related target check is not required here because in native there is no race condition rendering like on the web
- return false;
-}
-
-export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.native.ts b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.ts
new file mode 100644
index 000000000000..b53b9e3ddec0
--- /dev/null
+++ b/src/components/AddressSearch/isCurrentTargetInsideContainer.native.ts
@@ -0,0 +1,6 @@
+import type {IsCurrentTargetInsideContainerType} from './types';
+
+// The related target check is not required here because in native there is no race condition rendering like on the web
+const isCurrentTargetInsideContainer: IsCurrentTargetInsideContainerType = () => false;
+
+export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/isCurrentTargetInsideContainer.ts b/src/components/AddressSearch/isCurrentTargetInsideContainer.ts
new file mode 100644
index 000000000000..a50eb747b400
--- /dev/null
+++ b/src/components/AddressSearch/isCurrentTargetInsideContainer.ts
@@ -0,0 +1,14 @@
+import type {IsCurrentTargetInsideContainerType} from './types';
+
+const isCurrentTargetInsideContainer: IsCurrentTargetInsideContainerType = (event, containerRef) => {
+ // The related target check is required here
+ // because without it when we select an option, the onBlur will still trigger setting displayListViewBorder to false
+ // it will make the auto complete component re-render before onPress is called making selecting an option not working.
+ if (!containerRef.current || !event.target || !('relatedTarget' in event) || !('contains' in containerRef.current)) {
+ return false;
+ }
+
+ return !!containerRef.current.contains(event.relatedTarget as Node);
+};
+
+export default isCurrentTargetInsideContainer;
diff --git a/src/components/AddressSearch/types.ts b/src/components/AddressSearch/types.ts
new file mode 100644
index 000000000000..8016f1b2ea39
--- /dev/null
+++ b/src/components/AddressSearch/types.ts
@@ -0,0 +1,96 @@
+import type {RefObject} from 'react';
+import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, View, ViewStyle} from 'react-native';
+import type {Place} from 'react-native-google-places-autocomplete';
+import type Locale from '@src/types/onyx/Locale';
+
+type CurrentLocationButtonProps = {
+ /** Callback that is called when the button is clicked */
+ onPress?: () => void;
+
+ /** Boolean to indicate if the button is clickable */
+ isDisabled?: boolean;
+};
+
+type RenamedInputKeysProps = {
+ street: string;
+ street2: string;
+ city: string;
+ state: string;
+ lat: string;
+ lng: string;
+ zipCode: string;
+};
+
+type OnPressProps = {
+ address: string;
+ lat: number;
+ lng: number;
+ name: string;
+};
+
+type StreetValue = {
+ street: string;
+};
+
+type AddressSearchProps = {
+ /** The ID used to uniquely identify the input in a Form */
+ inputID?: string;
+
+ /** Saves a draft of the input value when used in a form */
+ shouldSaveDraft?: boolean;
+
+ /** Callback that is called when the text input is blurred */
+ onBlur?: () => void;
+
+ /** Error text to display */
+ errorText?: string;
+
+ /** Hint text to display */
+ hint?: string;
+
+ /** The label to display for the field */
+ label: string;
+
+ /** The value to set the field to initially */
+ value?: string;
+
+ /** The value to set the field to initially */
+ defaultValue?: string;
+
+ /** A callback function when the value of this field has changed */
+ onInputChange: (value: string | number | RenamedInputKeysProps | StreetValue, key?: string) => void;
+
+ /** A callback function when an address has been auto-selected */
+ onPress?: (props: OnPressProps) => void;
+
+ /** Customize the TextInput container */
+ containerStyles?: StyleProp;
+
+ /** Should address search be limited to results in the USA */
+ isLimitedToUSA?: boolean;
+
+ /** Shows a current location button in suggestion list */
+ canUseCurrentLocation?: boolean;
+
+ /** A list of predefined places that can be shown when the user isn't searching for something */
+ predefinedPlaces?: Place[];
+
+ /** A map of inputID key names */
+ renamedInputKeys: RenamedInputKeysProps;
+
+ /** Maximum number of characters allowed in search input */
+ maxInputLength?: number;
+
+ /** The result types to return from the Google Places Autocomplete request */
+ resultTypes?: string;
+
+ /** Location bias for querying search results. */
+ locationBias?: string;
+
+ /** The user's preferred locale e.g. 'en', 'es-ES' */
+ preferredLocale?: Locale;
+};
+
+type IsCurrentTargetInsideContainerType = (event: FocusEvent | NativeSyntheticEvent, containerRef: RefObject) => boolean;
+
+export type {CurrentLocationButtonProps, AddressSearchProps, RenamedInputKeysProps, IsCurrentTargetInsideContainerType};
diff --git a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
index bb3792f59d9f..99a0ee3bf683 100644
--- a/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
+++ b/src/components/AnchorForCommentsOnly/BaseAnchorForCommentsOnly.tsx
@@ -1,5 +1,6 @@
import Str from 'expensify-common/lib/str';
import React, {useEffect, useRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {Text as RNText} from 'react-native';
import {StyleSheet} from 'react-native';
import PressableWithSecondaryInteraction from '@components/PressableWithSecondaryInteraction';
diff --git a/src/components/AnonymousReportFooter.tsx b/src/components/AnonymousReportFooter.tsx
index ad79e316baf3..04e8a5f8d55b 100644
--- a/src/components/AnonymousReportFooter.tsx
+++ b/src/components/AnonymousReportFooter.tsx
@@ -1,5 +1,5 @@
import React from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx/lib/types';
import useLocalize from '@hooks/useLocalize';
@@ -9,6 +9,7 @@ import type {PersonalDetails, Report} from '@src/types/onyx';
import AvatarWithDisplayName from './AvatarWithDisplayName';
import Button from './Button';
import ExpensifyWordmark from './ExpensifyWordmark';
+import Text from './Text';
type AnonymousReportFooterProps = {
/** The report currently being looked at */
diff --git a/src/components/AutoEmailLink.js b/src/components/AutoEmailLink.tsx
similarity index 68%
rename from src/components/AutoEmailLink.js
rename to src/components/AutoEmailLink.tsx
index af581525ab69..e1a9bdd2794b 100644
--- a/src/components/AutoEmailLink.js
+++ b/src/components/AutoEmailLink.tsx
@@ -1,18 +1,13 @@
import {CONST} from 'expensify-common/lib/CONST';
-import PropTypes from 'prop-types';
import React from 'react';
-import _ from 'underscore';
+import type {StyleProp, TextStyle} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import Text from './Text';
import TextLink from './TextLink';
-const propTypes = {
- text: PropTypes.string.isRequired,
- style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-};
-
-const defaultProps = {
- style: [],
+type AutoEmailLinkProps = {
+ text: string;
+ style?: StyleProp;
};
/*
@@ -21,14 +16,15 @@ const defaultProps = {
* - Else just render it inside `Text` component
*/
-function AutoEmailLink(props) {
+function AutoEmailLink({text, style}: AutoEmailLinkProps) {
const styles = useThemeStyles();
return (
-
- {_.map(props.text.split(CONST.REG_EXP.EXTRACT_EMAIL), (str, index) => {
+
+ {text.split(CONST.REG_EXP.EXTRACT_EMAIL).map((str, index) => {
if (CONST.REG_EXP.EMAIL.test(str)) {
return (
{str}
@@ -52,6 +49,5 @@ function AutoEmailLink(props) {
}
AutoEmailLink.displayName = 'AutoEmailLink';
-AutoEmailLink.propTypes = propTypes;
-AutoEmailLink.defaultProps = defaultProps;
+
export default AutoEmailLink;
diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx
index 19b7bb6bb30a..3c2caf020ef7 100755
--- a/src/components/Composer/index.tsx
+++ b/src/components/Composer/index.tsx
@@ -3,6 +3,7 @@ import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import type {BaseSyntheticEvent, ForwardedRef} from 'react';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import {flushSync} from 'react-dom';
+// eslint-disable-next-line no-restricted-imports
import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native';
import {StyleSheet, View} from 'react-native';
import type {AnimatedProps} from 'react-native-reanimated';
diff --git a/src/components/ConfirmModal.tsx b/src/components/ConfirmModal.tsx
index a345ec72ad11..f25fc978e3ee 100755
--- a/src/components/ConfirmModal.tsx
+++ b/src/components/ConfirmModal.tsx
@@ -15,7 +15,7 @@ type ConfirmModalProps = {
onConfirm: () => void;
/** A callback to call when the form has been closed */
- onCancel?: (ref?: React.RefObject) => void;
+ onCancel?: () => void;
/** Modal visibility */
isVisible: boolean;
diff --git a/src/components/ContextMenuItem.tsx b/src/components/ContextMenuItem.tsx
index 781d2f718bcf..4e9bd22e004c 100644
--- a/src/components/ContextMenuItem.tsx
+++ b/src/components/ContextMenuItem.tsx
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef, useImperativeHandle} from 'react';
+import type {GestureResponderEvent} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useThrottledButtonState from '@hooks/useThrottledButtonState';
@@ -27,7 +28,7 @@ type ContextMenuItemProps = {
isMini?: boolean;
/** Callback to fire when the item is pressed */
- onPress: () => void;
+ onPress: (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => void;
/** A description text to show under the title */
description?: string;
@@ -52,11 +53,11 @@ function ContextMenuItem(
const {windowWidth} = useWindowDimensions();
const [isThrottledButtonActive, setThrottledButtonInactive] = useThrottledButtonState();
- const triggerPressAndUpdateSuccess = () => {
+ const triggerPressAndUpdateSuccess = (event?: GestureResponderEvent | MouseEvent | KeyboardEvent) => {
if (!isThrottledButtonActive) {
return;
}
- onPress();
+ onPress(event);
// We only set the success state when we have icon or text to represent the success state
// We may want to replace this check by checking the Result from OnPress Callback in future.
@@ -104,3 +105,4 @@ function ContextMenuItem(
ContextMenuItem.displayName = 'ContextMenuItem';
export default forwardRef(ContextMenuItem);
+export type {ContextMenuItemHandle};
diff --git a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
index 9b3eaeb4e447..430c00cf8804 100644
--- a/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
+++ b/src/components/DisplayNames/DisplayNamesTooltipItem.tsx
@@ -1,5 +1,6 @@
import type {RefObject} from 'react';
import React, {useCallback} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {Text as RNText, StyleProp, TextStyle} from 'react-native';
import Text from '@components/Text';
import UserDetailsTooltip from '@components/UserDetailsTooltip';
diff --git a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
index 0d554baabeda..ce0ae7ddcf4f 100644
--- a/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
+++ b/src/components/DisplayNames/DisplayNamesWithTooltip.tsx
@@ -1,4 +1,5 @@
import React, {Fragment, useCallback, useRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {Text as RNText} from 'react-native';
import {View} from 'react-native';
import Text from '@components/Text';
diff --git a/src/components/EmojiPicker/EmojiPicker.js b/src/components/EmojiPicker/EmojiPicker.js
index 532eb61a99a9..eaf89b7f64ea 100644
--- a/src/components/EmojiPicker/EmojiPicker.js
+++ b/src/components/EmojiPicker/EmojiPicker.js
@@ -86,7 +86,13 @@ const EmojiPicker = forwardRef((props, ref) => {
if (isNavigating) {
onModalHide.current = () => {};
}
- emojiPopoverAnchorRef.current = null;
+ const currOnModalHide = onModalHide.current;
+ onModalHide.current = () => {
+ if (currOnModalHide) {
+ currOnModalHide();
+ }
+ emojiPopoverAnchorRef.current = null;
+ };
setIsEmojiPickerVisible(false);
};
diff --git a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js
index 806ab6587917..059f3fc5f848 100644
--- a/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js
+++ b/src/components/EmojiPicker/EmojiPickerMenu/BaseEmojiPickerMenu.js
@@ -1,10 +1,11 @@
import {FlashList} from '@shopify/flash-list';
import PropTypes from 'prop-types';
import React, {useMemo} from 'react';
-import {StyleSheet, Text, View} from 'react-native';
+import {StyleSheet, View} from 'react-native';
import CategoryShortcutBar from '@components/EmojiPicker/CategoryShortcutBar';
import EmojiSkinToneList from '@components/EmojiPicker/EmojiSkinToneList';
import refPropTypes from '@components/refPropTypes';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
diff --git a/src/components/Form/FormProvider.js b/src/components/Form/FormProvider.js
index 50b24e368fc6..4d1630dbbe06 100644
--- a/src/components/Form/FormProvider.js
+++ b/src/components/Form/FormProvider.js
@@ -73,6 +73,9 @@ const propTypes = {
/** Should validate function be called when the value of the input is changed */
shouldValidateOnChange: PropTypes.bool,
+
+ /** Should fix the errors alert be displayed when there is an error in the form */
+ shouldHideFixErrorsAlert: PropTypes.bool,
};
// In order to prevent Checkbox focus loss when the user are focusing a TextInput and proceeds to toggle a CheckBox in web and mobile web.
@@ -94,6 +97,7 @@ const defaultProps = {
validate: () => {},
shouldValidateOnBlur: true,
shouldValidateOnChange: true,
+ shouldHideFixErrorsAlert: false,
};
function getInitialValueByType(valueType) {
diff --git a/src/components/Form/FormWrapper.js b/src/components/Form/FormWrapper.js
index da34262a8af8..f1c5d6de9071 100644
--- a/src/components/Form/FormWrapper.js
+++ b/src/components/Form/FormWrapper.js
@@ -66,6 +66,8 @@ const propTypes = {
errors: errorsPropType.isRequired,
inputRefs: PropTypes.objectOf(refPropTypes).isRequired,
+
+ shouldHideFixErrorsAlert: PropTypes.bool,
};
const defaultProps = {
@@ -79,6 +81,7 @@ const defaultProps = {
footerContent: null,
style: [],
submitButtonStyles: [],
+ shouldHideFixErrorsAlert: false,
};
function FormWrapper(props) {
@@ -97,6 +100,7 @@ function FormWrapper(props) {
enabledWhenOffline,
isSubmitActionDangerous,
formID,
+ shouldHideFixErrorsAlert,
} = props;
const formRef = useRef(null);
const formContentRef = useRef(null);
@@ -117,7 +121,7 @@ function FormWrapper(props) {
{isSubmitButtonVisible && (
0 || Boolean(errorMessage) || !_.isEmpty(formState.errorFields)}
+ isAlertVisible={((_.size(errors) > 0 || !_.isEmpty(formState.errorFields)) && !shouldHideFixErrorsAlert) || Boolean(errorMessage)}
isLoading={formState.isLoading}
message={_.isEmpty(formState.errorFields) ? errorMessage : null}
onSubmit={onSubmit}
@@ -153,6 +157,7 @@ function FormWrapper(props) {
enabledWhenOffline={enabledWhenOffline}
isSubmitActionDangerous={isSubmitActionDangerous}
disablePressOnEnter
+ shouldHideFixErrorsAlert={shouldHideFixErrorsAlert}
/>
)}
@@ -176,6 +181,7 @@ function FormWrapper(props) {
styles.mt5,
submitButtonStyles,
submitButtonText,
+ shouldHideFixErrorsAlert,
],
);
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index 9ffb0b5ef2f3..55cc9e708771 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -7,7 +7,7 @@ import type {PersonalDetails, Policy, Report} from '@src/types/onyx';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type IconAsset from '@src/types/utils/IconAsset';
-type ThreeDotsMenuItems = {
+type ThreeDotsMenuItem = {
/** An icon element displayed on the left side */
icon?: IconAsset;
@@ -62,7 +62,7 @@ type HeaderWithBackButtonProps = Partial & {
shouldDisableThreeDotsButton?: boolean;
/** List of menu items for more(three dots) menu */
- threeDotsMenuItems?: ThreeDotsMenuItems[];
+ threeDotsMenuItems?: ThreeDotsMenuItem[];
/** The anchor position of the menu */
threeDotsAnchorPosition?: AnchorPosition;
@@ -110,4 +110,5 @@ type HeaderWithBackButtonProps = Partial & {
shouldEnableDetailPageNavigation?: boolean;
};
+export type {ThreeDotsMenuItem};
export default HeaderWithBackButtonProps;
diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts
index 2ddee8b2939b..797e6f34fc75 100644
--- a/src/components/Icon/Expensicons.ts
+++ b/src/components/Icon/Expensicons.ts
@@ -27,6 +27,8 @@ import Camera from '@assets/images/camera.svg';
import Car from '@assets/images/car.svg';
import Cash from '@assets/images/cash.svg';
import Chair from '@assets/images/chair.svg';
+import ChatBubbleAdd from '@assets/images/chatbubble-add.svg';
+import ChatBubbleUnread from '@assets/images/chatbubble-unread.svg';
import ChatBubble from '@assets/images/chatbubble.svg';
import ChatBubbles from '@assets/images/chatbubbles.svg';
import Checkmark from '@assets/images/checkmark.svg';
@@ -264,4 +266,6 @@ export {
Podcast,
Linkedin,
Instagram,
+ ChatBubbleAdd,
+ ChatBubbleUnread,
};
diff --git a/src/components/KYCWall/BaseKYCWall.js b/src/components/KYCWall/BaseKYCWall.js
deleted file mode 100644
index 5ad25d23f484..000000000000
--- a/src/components/KYCWall/BaseKYCWall.js
+++ /dev/null
@@ -1,247 +0,0 @@
-import lodashGet from 'lodash/get';
-import React, {useCallback, useEffect, useRef, useState} from 'react';
-import {Dimensions} from 'react-native';
-import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
-import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
-import * as BankAccounts from '@libs/actions/BankAccounts';
-import getClickedTargetLocation from '@libs/getClickedTargetLocation';
-import Log from '@libs/Log';
-import Navigation from '@libs/Navigation/Navigation';
-import * as PaymentUtils from '@libs/PaymentUtils';
-import * as ReportUtils from '@libs/ReportUtils';
-import * as PaymentMethods from '@userActions/PaymentMethods';
-import * as Policy from '@userActions/Policy';
-import * as Wallet from '@userActions/Wallet';
-import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
-import ROUTES from '@src/ROUTES';
-import {defaultProps, propTypes} from './kycWallPropTypes';
-
-// This sets the Horizontal anchor position offset for POPOVER MENU.
-const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20;
-
-// This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow
-// before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it
-// to render the AddPaymentMethodMenu in the correct location.
-function KYCWall({
- addBankAccountRoute,
- addDebitCardRoute,
- anchorAlignment,
- bankAccountList,
- chatReportID,
- children,
- enablePaymentsRoute,
- fundList,
- iouReport,
- onSelectPaymentMethod,
- onSuccessfulKYC,
- reimbursementAccount,
- shouldIncludeDebitCard,
- shouldListenForResize,
- source,
- userWallet,
- walletTerms,
- shouldShowPersonalBankAccountOption,
-}) {
- const anchorRef = useRef(null);
- const transferBalanceButtonRef = useRef(null);
-
- const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false);
- const [anchorPosition, setAnchorPosition] = useState({
- anchorPositionVertical: 0,
- anchorPositionHorizontal: 0,
- });
-
- /**
- * @param {DOMRect} domRect
- * @returns {Object}
- */
- const getAnchorPosition = useCallback(
- (domRect) => {
- if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) {
- return {
- anchorPositionVertical: domRect.top + domRect.height + CONST.MODAL.POPOVER_MENU_PADDING,
- anchorPositionHorizontal: domRect.left + POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET,
- };
- }
-
- return {
- anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING,
- anchorPositionHorizontal: domRect.left,
- };
- },
- [anchorAlignment.vertical],
- );
-
- /**
- * Set position of the transfer payment menu
- *
- * @param {Object} position
- */
- const setPositionAddPaymentMenu = ({anchorPositionVertical, anchorPositionHorizontal}) => {
- setAnchorPosition({
- anchorPositionVertical,
- anchorPositionHorizontal,
- });
- };
-
- const setMenuPosition = useCallback(() => {
- if (!transferBalanceButtonRef.current) {
- return;
- }
- const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current);
- const position = getAnchorPosition(buttonPosition);
-
- setPositionAddPaymentMenu(position);
- }, [getAnchorPosition]);
-
- useEffect(() => {
- let dimensionsSubscription = null;
-
- PaymentMethods.kycWallRef.current = this;
-
- if (shouldListenForResize) {
- dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition);
- }
-
- return () => {
- if (shouldListenForResize && dimensionsSubscription) {
- dimensionsSubscription.remove();
- }
-
- PaymentMethods.kycWallRef.current = null;
- };
- }, [chatReportID, setMenuPosition, shouldListenForResize]);
-
- /**
- * @param {String} paymentMethod
- */
- const selectPaymentMethod = (paymentMethod) => {
- onSelectPaymentMethod(paymentMethod);
- if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
- BankAccounts.openPersonalBankAccountSetupView();
- } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) {
- Navigation.navigate(addDebitCardRoute);
- } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) {
- if (ReportUtils.isIOUReport(iouReport)) {
- const policyID = Policy.createWorkspaceFromIOUPayment(iouReport);
-
- // Navigate to the bank account set up flow for this specific policy
- Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID));
- return;
- }
- Navigation.navigate(addBankAccountRoute);
- }
- };
-
- /**
- * Take the position of the button that calls this method and show the Add Payment method menu when the user has no valid payment method.
- * If they do have a valid payment method they are navigated to the "enable payments" route to complete KYC checks.
- * If they are already KYC'd we will continue whatever action is gated behind the KYC wall.
- *
- * @param {Event} event
- * @param {String} iouPaymentType
- */
- const continueAction = (event, iouPaymentType) => {
- const currentSource = lodashGet(walletTerms, 'source', source);
-
- /**
- * Set the source, so we can tailor the process according to how we got here.
- * We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold.
- */
- Wallet.setKYCWallSource(source, chatReportID);
-
- if (shouldShowAddPaymentMenu) {
- setShouldShowAddPaymentMenu(false);
-
- return;
- }
-
- // Use event target as fallback if anchorRef is null for safety
- const targetElement = anchorRef.current || event.nativeEvent.target;
-
- transferBalanceButtonRef.current = targetElement;
- const isExpenseReport = ReportUtils.isExpenseReport(iouReport);
- const paymentCardList = fundList || {};
-
- // Check to see if user has a valid payment method on file and display the add payment popover if they don't
- if (
- (isExpenseReport && lodashGet(reimbursementAccount, 'achData.state', '') !== CONST.BANK_ACCOUNT.STATE.OPEN) ||
- (!isExpenseReport && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard))
- ) {
- Log.info('[KYC Wallet] User does not have valid payment method');
- if (!shouldIncludeDebitCard) {
- selectPaymentMethod(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT);
- return;
- }
-
- const clickedElementLocation = getClickedTargetLocation(targetElement);
- const position = getAnchorPosition(clickedElementLocation);
-
- setPositionAddPaymentMenu(position);
- setShouldShowAddPaymentMenu(true);
-
- return;
- }
-
- if (!isExpenseReport) {
- // Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks
- const hasActivatedWallet = userWallet.tierName && _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], userWallet.tierName);
- if (!hasActivatedWallet) {
- Log.info('[KYC Wallet] User does not have active wallet');
- Navigation.navigate(enablePaymentsRoute);
- return;
- }
- }
- Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them');
- onSuccessfulKYC(iouPaymentType, currentSource);
- };
-
- return (
- <>
- setShouldShowAddPaymentMenu(false)}
- anchorRef={anchorRef}
- anchorPosition={{
- vertical: anchorPosition.anchorPositionVertical,
- horizontal: anchorPosition.anchorPositionHorizontal,
- }}
- anchorAlignment={anchorAlignment}
- onItemSelected={(item) => {
- setShouldShowAddPaymentMenu(false);
- selectPaymentMethod(item);
- }}
- shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption}
- />
- {children(continueAction, anchorRef)}
- >
- );
-}
-
-KYCWall.propTypes = propTypes;
-KYCWall.defaultProps = defaultProps;
-KYCWall.displayName = 'BaseKYCWall';
-
-export default withOnyx({
- userWallet: {
- key: ONYXKEYS.USER_WALLET,
- },
- walletTerms: {
- key: ONYXKEYS.WALLET_TERMS,
- },
- fundList: {
- key: ONYXKEYS.FUND_LIST,
- },
- bankAccountList: {
- key: ONYXKEYS.BANK_ACCOUNT_LIST,
- },
- reimbursementAccount: {
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- },
- chatReport: {
- key: ({chatReportID}) => `${ONYXKEYS.COLLECTION.REPORT}${chatReportID}`,
- },
-})(KYCWall);
diff --git a/src/components/KYCWall/BaseKYCWall.tsx b/src/components/KYCWall/BaseKYCWall.tsx
new file mode 100644
index 000000000000..04c8397bc33b
--- /dev/null
+++ b/src/components/KYCWall/BaseKYCWall.tsx
@@ -0,0 +1,286 @@
+import React, {useCallback, useEffect, useRef, useState} from 'react';
+import type {SyntheticEvent} from 'react';
+import {Dimensions} from 'react-native';
+import type {EmitterSubscription, NativeTouchEvent} from 'react-native';
+import {withOnyx} from 'react-native-onyx';
+import type {OnyxEntry} from 'react-native-onyx';
+import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu';
+import * as BankAccounts from '@libs/actions/BankAccounts';
+import getClickedTargetLocation from '@libs/getClickedTargetLocation';
+import Log from '@libs/Log';
+import Navigation from '@libs/Navigation/Navigation';
+import * as PaymentUtils from '@libs/PaymentUtils';
+import * as ReportUtils from '@libs/ReportUtils';
+import * as PaymentMethods from '@userActions/PaymentMethods';
+import * as Policy from '@userActions/Policy';
+import * as Wallet from '@userActions/Wallet';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import ROUTES from '@src/ROUTES';
+import type {BankAccountList, FundList, ReimbursementAccount, UserWallet, WalletTerms} from '@src/types/onyx';
+import type {AnchorPosition, DomRect, KYCWallProps, PaymentMethod, TransferMethod} from './types';
+
+// This sets the Horizontal anchor position offset for POPOVER MENU.
+const POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET = 20;
+
+type BaseKYCWallOnyxProps = {
+ /** The user's wallet */
+ userWallet: OnyxEntry;
+
+ /** Information related to the last step of the wallet activation flow */
+ walletTerms: OnyxEntry;
+
+ /** List of user's cards */
+ fundList: OnyxEntry;
+
+ /** List of bank accounts */
+ bankAccountList: OnyxEntry;
+
+ /** The reimbursement account linked to the Workspace */
+ reimbursementAccount: OnyxEntry;
+};
+
+type BaseKYCWallProps = KYCWallProps & BaseKYCWallOnyxProps;
+
+// This component allows us to block various actions by forcing the user to first add a default payment method and successfully make it through our Know Your Customer flow
+// before continuing to take whatever action they originally intended to take. It requires a button as a child and a native event so we can get the coordinates and use it
+// to render the AddPaymentMethodMenu in the correct location.
+function KYCWall({
+ addBankAccountRoute,
+ addDebitCardRoute,
+ anchorAlignment = {
+ horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
+ vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM,
+ },
+ bankAccountList = {},
+ chatReportID = '',
+ children,
+ enablePaymentsRoute,
+ fundList,
+ iouReport,
+ onSelectPaymentMethod = () => {},
+ onSuccessfulKYC,
+ reimbursementAccount,
+ shouldIncludeDebitCard = true,
+ shouldListenForResize = false,
+ source,
+ userWallet,
+ walletTerms,
+ shouldShowPersonalBankAccountOption = false,
+}: BaseKYCWallProps) {
+ const anchorRef = useRef(null);
+ const transferBalanceButtonRef = useRef(null);
+
+ const [shouldShowAddPaymentMenu, setShouldShowAddPaymentMenu] = useState(false);
+
+ const [anchorPosition, setAnchorPosition] = useState({
+ anchorPositionVertical: 0,
+ anchorPositionHorizontal: 0,
+ });
+
+ const getAnchorPosition = useCallback(
+ (domRect: DomRect): AnchorPosition => {
+ if (anchorAlignment.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP) {
+ return {
+ anchorPositionVertical: domRect.top + domRect.height + CONST.MODAL.POPOVER_MENU_PADDING,
+ anchorPositionHorizontal: domRect.left + POPOVER_MENU_ANCHOR_POSITION_HORIZONTAL_OFFSET,
+ };
+ }
+
+ return {
+ anchorPositionVertical: domRect.top - CONST.MODAL.POPOVER_MENU_PADDING,
+ anchorPositionHorizontal: domRect.left,
+ };
+ },
+ [anchorAlignment.vertical],
+ );
+
+ /**
+ * Set position of the transfer payment menu
+ */
+ const setPositionAddPaymentMenu = ({anchorPositionVertical, anchorPositionHorizontal}: AnchorPosition) => {
+ setAnchorPosition({
+ anchorPositionVertical,
+ anchorPositionHorizontal,
+ });
+ };
+
+ const setMenuPosition = useCallback(() => {
+ if (!transferBalanceButtonRef.current) {
+ return;
+ }
+
+ const buttonPosition = getClickedTargetLocation(transferBalanceButtonRef.current);
+ const position = getAnchorPosition(buttonPosition);
+
+ setPositionAddPaymentMenu(position);
+ }, [getAnchorPosition]);
+
+ const selectPaymentMethod = useCallback(
+ (paymentMethod: PaymentMethod) => {
+ onSelectPaymentMethod(paymentMethod);
+
+ if (paymentMethod === CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT) {
+ BankAccounts.openPersonalBankAccountSetupView();
+ } else if (paymentMethod === CONST.PAYMENT_METHODS.DEBIT_CARD) {
+ Navigation.navigate(addDebitCardRoute);
+ } else if (paymentMethod === CONST.PAYMENT_METHODS.BUSINESS_BANK_ACCOUNT) {
+ if (iouReport && ReportUtils.isIOUReport(iouReport)) {
+ const policyID = Policy.createWorkspaceFromIOUPayment(iouReport);
+
+ // Navigate to the bank account set up flow for this specific policy
+ Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute('', policyID));
+
+ return;
+ }
+ Navigation.navigate(addBankAccountRoute);
+ }
+ },
+ [addBankAccountRoute, addDebitCardRoute, iouReport, onSelectPaymentMethod],
+ );
+
+ /**
+ * Take the position of the button that calls this method and show the Add Payment method menu when the user has no valid payment method.
+ * If they do have a valid payment method they are navigated to the "enable payments" route to complete KYC checks.
+ * If they are already KYC'd we will continue whatever action is gated behind the KYC wall.
+ *
+ */
+ const continueAction = useCallback(
+ (event?: SyntheticEvent, iouPaymentType?: TransferMethod) => {
+ const currentSource = walletTerms?.source ?? source;
+
+ /**
+ * Set the source, so we can tailor the process according to how we got here.
+ * We do not want to set this on mount, as the source can change upon completing the flow, e.g. when upgrading the wallet to Gold.
+ */
+ Wallet.setKYCWallSource(source, chatReportID);
+
+ if (shouldShowAddPaymentMenu) {
+ setShouldShowAddPaymentMenu(false);
+ return;
+ }
+
+ // Use event target as fallback if anchorRef is null for safety
+ const targetElement = anchorRef.current ?? (event?.nativeEvent.target as HTMLDivElement);
+
+ transferBalanceButtonRef.current = targetElement;
+
+ const isExpenseReport = ReportUtils.isExpenseReport(iouReport ?? null);
+ const paymentCardList = fundList ?? {};
+
+ // Check to see if user has a valid payment method on file and display the add payment popover if they don't
+ if (
+ (isExpenseReport && reimbursementAccount?.achData?.state !== CONST.BANK_ACCOUNT.STATE.OPEN) ||
+ (!isExpenseReport && bankAccountList !== null && !PaymentUtils.hasExpensifyPaymentMethod(paymentCardList, bankAccountList, shouldIncludeDebitCard))
+ ) {
+ Log.info('[KYC Wallet] User does not have valid payment method');
+
+ if (!shouldIncludeDebitCard) {
+ selectPaymentMethod(CONST.PAYMENT_METHODS.PERSONAL_BANK_ACCOUNT);
+ return;
+ }
+
+ const clickedElementLocation = getClickedTargetLocation(targetElement);
+ const position = getAnchorPosition(clickedElementLocation);
+
+ setPositionAddPaymentMenu(position);
+ setShouldShowAddPaymentMenu(true);
+
+ return;
+ }
+ if (!isExpenseReport) {
+ // Ask the user to upgrade to a gold wallet as this means they have not yet gone through our Know Your Customer (KYC) checks
+ const hasActivatedWallet = userWallet?.tierName && [CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM].some((name) => name === userWallet.tierName);
+
+ if (!hasActivatedWallet) {
+ Log.info('[KYC Wallet] User does not have active wallet');
+
+ Navigation.navigate(enablePaymentsRoute);
+
+ return;
+ }
+ }
+
+ Log.info('[KYC Wallet] User has valid payment method and passed KYC checks or did not need them');
+
+ onSuccessfulKYC(currentSource, iouPaymentType);
+ },
+ [
+ bankAccountList,
+ chatReportID,
+ enablePaymentsRoute,
+ fundList,
+ getAnchorPosition,
+ iouReport,
+ onSuccessfulKYC,
+ reimbursementAccount?.achData?.state,
+ selectPaymentMethod,
+ shouldIncludeDebitCard,
+ shouldShowAddPaymentMenu,
+ source,
+ userWallet?.tierName,
+ walletTerms?.source,
+ ],
+ );
+
+ useEffect(() => {
+ let dimensionsSubscription: EmitterSubscription | null = null;
+
+ PaymentMethods.kycWallRef.current = {continueAction};
+
+ if (shouldListenForResize) {
+ dimensionsSubscription = Dimensions.addEventListener('change', setMenuPosition);
+ }
+
+ return () => {
+ if (shouldListenForResize && dimensionsSubscription) {
+ dimensionsSubscription.remove();
+ }
+
+ PaymentMethods.kycWallRef.current = null;
+ };
+ }, [chatReportID, setMenuPosition, shouldListenForResize, continueAction]);
+
+ return (
+ <>
+ setShouldShowAddPaymentMenu(false)}
+ anchorRef={anchorRef}
+ anchorPosition={{
+ vertical: anchorPosition.anchorPositionVertical,
+ horizontal: anchorPosition.anchorPositionHorizontal,
+ }}
+ anchorAlignment={anchorAlignment}
+ onItemSelected={(item: PaymentMethod) => {
+ setShouldShowAddPaymentMenu(false);
+ selectPaymentMethod(item);
+ }}
+ shouldShowPersonalBankAccountOption={shouldShowPersonalBankAccountOption}
+ />
+ {children(continueAction, anchorRef)}
+ >
+ );
+}
+
+KYCWall.displayName = 'BaseKYCWall';
+
+export default withOnyx({
+ userWallet: {
+ key: ONYXKEYS.USER_WALLET,
+ },
+ walletTerms: {
+ key: ONYXKEYS.WALLET_TERMS,
+ },
+ fundList: {
+ key: ONYXKEYS.FUND_LIST,
+ },
+ bankAccountList: {
+ key: ONYXKEYS.BANK_ACCOUNT_LIST,
+ },
+ reimbursementAccount: {
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ },
+})(KYCWall);
diff --git a/src/components/KYCWall/index.native.js b/src/components/KYCWall/index.native.ts
similarity index 100%
rename from src/components/KYCWall/index.native.js
rename to src/components/KYCWall/index.native.ts
diff --git a/src/components/KYCWall/index.js b/src/components/KYCWall/index.tsx
similarity index 66%
rename from src/components/KYCWall/index.js
rename to src/components/KYCWall/index.tsx
index 49329c73d474..e99efee67210 100644
--- a/src/components/KYCWall/index.js
+++ b/src/components/KYCWall/index.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import BaseKYCWall from './BaseKYCWall';
-import {defaultProps, propTypes} from './kycWallPropTypes';
+import type {KYCWallProps} from './types';
-function KYCWall(props) {
+function KYCWall(props: KYCWallProps) {
return (
{},
- shouldShowPersonalBankAccountOption: false,
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/KYCWall/types.ts b/src/components/KYCWall/types.ts
new file mode 100644
index 000000000000..aee5b569cc46
--- /dev/null
+++ b/src/components/KYCWall/types.ts
@@ -0,0 +1,73 @@
+import type {ForwardedRef, SyntheticEvent} from 'react';
+import type {NativeTouchEvent} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import type {ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+import type {Route} from '@src/ROUTES';
+import type {Report} from '@src/types/onyx';
+
+type Source = ValueOf;
+
+type TransferMethod = ValueOf;
+
+type DOMRectProperties = 'top' | 'bottom' | 'left' | 'right' | 'height' | 'x' | 'y';
+
+type DomRect = Pick;
+
+type AnchorAlignment = {
+ horizontal: ValueOf;
+ vertical: ValueOf;
+};
+
+type AnchorPosition = {
+ anchorPositionVertical: number;
+ anchorPositionHorizontal: number;
+};
+
+type PaymentMethod = ValueOf;
+
+type KYCWallProps = {
+ /** Route for the Add Bank Account screen for a given navigation stack */
+ addBankAccountRoute: Route;
+
+ /** Route for the Add Debit Card screen for a given navigation stack */
+ addDebitCardRoute?: Route;
+
+ /** Route for the KYC enable payments screen for a given navigation stack */
+ enablePaymentsRoute: Route;
+
+ /** Listen for window resize event on web and desktop */
+ shouldListenForResize?: boolean;
+
+ /** Wrapped components should be disabled, and not in spinner/loading state */
+ isDisabled?: boolean;
+
+ /** The source that triggered the KYC wall */
+ source?: Source;
+
+ /** When the button is opened via an IOU, ID for the chatReport that the IOU is linked to */
+ chatReportID?: string;
+
+ /** The IOU/Expense report we are paying */
+ iouReport?: OnyxEntry;
+
+ /** Where the popover should be positioned relative to the anchor points. */
+ anchorAlignment?: AnchorAlignment;
+
+ /** Whether the option to add a debit card should be included */
+ shouldIncludeDebitCard?: boolean;
+
+ /** Callback for when a payment method has been selected */
+ onSelectPaymentMethod?: (paymentMethod: PaymentMethod) => void;
+
+ /** Whether the personal bank account option should be shown */
+ shouldShowPersonalBankAccountOption?: boolean;
+
+ /** Callback for the end of the onContinue trigger on option selection */
+ onSuccessfulKYC: (currentSource?: Source, iouPaymentType?: TransferMethod) => void;
+
+ /** Children to build the KYC */
+ children: (continueAction: (event: SyntheticEvent, method: TransferMethod) => void, anchorRef: ForwardedRef) => void;
+};
+
+export type {AnchorPosition, KYCWallProps, PaymentMethod, TransferMethod, DomRect};
diff --git a/src/components/LocationErrorMessage/types.ts b/src/components/LocationErrorMessage/types.ts
index 41b71dbc3c69..27aa89d07ede 100644
--- a/src/components/LocationErrorMessage/types.ts
+++ b/src/components/LocationErrorMessage/types.ts
@@ -1,3 +1,5 @@
+import type {GeolocationErrorCodeType} from '@libs/getCurrentPosition/getCurrentPosition.types';
+
type LocationErrorMessageProps = {
/** A callback that runs when close icon is pressed */
onClose: () => void;
@@ -9,7 +11,7 @@ type LocationErrorMessageProps = {
* - code 2 = location is unavailable or there is some connection issue
* - code 3 = location fetch timeout
*/
- locationErrorCode?: -1 | 1 | 2 | 3;
+ locationErrorCode?: GeolocationErrorCodeType | null;
};
export default LocationErrorMessageProps;
diff --git a/src/components/MenuItemList.tsx b/src/components/MenuItemList.tsx
index f83f173a644f..4ba9260e23ff 100644
--- a/src/components/MenuItemList.tsx
+++ b/src/components/MenuItemList.tsx
@@ -10,7 +10,7 @@ type MenuItemLink = string | (() => Promise);
type MenuItemWithLink = MenuItemProps & {
/** The link to open when the menu item is clicked */
- link: MenuItemLink;
+ link?: MenuItemLink;
};
type MenuItemListProps = {
@@ -31,7 +31,7 @@ function MenuItemList({menuItems = [], shouldUseSingleExecution = false}: MenuIt
* @param link the menu item link or function to get the link
* @param event the interaction event
*/
- const secondaryInteraction = (link: MenuItemLink, event: GestureResponderEvent | MouseEvent) => {
+ const secondaryInteraction = (link: MenuItemLink | undefined, event: GestureResponderEvent | MouseEvent) => {
if (typeof link === 'function') {
link().then((url) => ReportActionContextMenu.showContextMenu(CONST.CONTEXT_MENU_TYPES.LINK, event, url, popoverAnchor.current));
} else if (link) {
diff --git a/src/components/Modal/index.android.tsx b/src/components/Modal/index.android.tsx
index 4d7ae128a114..86a1fd272185 100644
--- a/src/components/Modal/index.android.tsx
+++ b/src/components/Modal/index.android.tsx
@@ -1,6 +1,5 @@
import React from 'react';
import {AppState} from 'react-native';
-import withWindowDimensions from '@components/withWindowDimensions';
import ComposerFocusManager from '@libs/ComposerFocusManager';
import BaseModal from './BaseModal';
import type BaseModalProps from './types';
@@ -28,4 +27,4 @@ function Modal({useNativeDriver = true, ...rest}: BaseModalProps) {
}
Modal.displayName = 'Modal';
-export default withWindowDimensions(Modal);
+export default Modal;
diff --git a/src/components/Modal/index.ios.tsx b/src/components/Modal/index.ios.tsx
index cbe58a071d7d..b26ba6cd0f89 100644
--- a/src/components/Modal/index.ios.tsx
+++ b/src/components/Modal/index.ios.tsx
@@ -1,5 +1,4 @@
import React from 'react';
-import withWindowDimensions from '@components/withWindowDimensions';
import BaseModal from './BaseModal';
import type BaseModalProps from './types';
@@ -15,4 +14,4 @@ function Modal({children, ...rest}: BaseModalProps) {
}
Modal.displayName = 'Modal';
-export default withWindowDimensions(Modal);
+export default Modal;
diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx
index 56f3c76a8879..71c0fe47ffca 100644
--- a/src/components/Modal/index.tsx
+++ b/src/components/Modal/index.tsx
@@ -1,5 +1,4 @@
import React, {useState} from 'react';
-import withWindowDimensions from '@components/withWindowDimensions';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import StatusBar from '@libs/StatusBar';
@@ -55,4 +54,4 @@ function Modal({fullscreen = true, onModalHide = () => {}, type, onModalShow = (
}
Modal.displayName = 'Modal';
-export default withWindowDimensions(Modal);
+export default Modal;
diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts
index 0fed37ffea8b..0773f0741233 100644
--- a/src/components/Modal/types.ts
+++ b/src/components/Modal/types.ts
@@ -1,7 +1,6 @@
import type {ViewStyle} from 'react-native';
import type {ModalProps} from 'react-native-modal';
import type {ValueOf} from 'type-fest';
-import type {WindowDimensionsProps} from '@components/withWindowDimensions/types';
import type CONST from '@src/CONST';
type PopoverAnchorPosition = {
@@ -11,57 +10,56 @@ type PopoverAnchorPosition = {
left?: number;
};
-type BaseModalProps = WindowDimensionsProps &
- Partial & {
- /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */
- fullscreen?: boolean;
+type BaseModalProps = Partial & {
+ /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */
+ fullscreen?: boolean;
- /** Should we close modal on outside click */
- shouldCloseOnOutsideClick?: boolean;
+ /** Should we close modal on outside click */
+ shouldCloseOnOutsideClick?: boolean;
- /** Should we announce the Modal visibility changes? */
- shouldSetModalVisibility?: boolean;
+ /** Should we announce the Modal visibility changes? */
+ shouldSetModalVisibility?: boolean;
- /** Callback method fired when the user requests to close the modal */
- onClose: (ref?: React.RefObject) => void;
+ /** Callback method fired when the user requests to close the modal */
+ onClose: () => void;
- /** State that determines whether to display the modal or not */
- isVisible: boolean;
+ /** State that determines whether to display the modal or not */
+ isVisible: boolean;
- /** Callback method fired when the user requests to submit the modal content. */
- onSubmit?: () => void;
+ /** Callback method fired when the user requests to submit the modal content. */
+ onSubmit?: () => void;
- /** Callback method fired when the modal is hidden */
- onModalHide?: () => void;
+ /** Callback method fired when the modal is hidden */
+ onModalHide?: () => void;
- /** Callback method fired when the modal is shown */
- onModalShow?: () => void;
+ /** Callback method fired when the modal is shown */
+ onModalShow?: () => void;
- /** Style of modal to display */
- type?: ValueOf;
+ /** Style of modal to display */
+ type?: ValueOf;
- /** The anchor position of a popover modal. Has no effect on other modal types. */
- popoverAnchorPosition?: PopoverAnchorPosition;
+ /** The anchor position of a popover modal. Has no effect on other modal types. */
+ popoverAnchorPosition?: PopoverAnchorPosition;
- outerStyle?: ViewStyle;
+ outerStyle?: ViewStyle;
- /** Whether the modal should go under the system statusbar */
- statusBarTranslucent?: boolean;
+ /** Whether the modal should go under the system statusbar */
+ statusBarTranslucent?: boolean;
- /** Whether the modal should avoid the keyboard */
- avoidKeyboard?: boolean;
+ /** Whether the modal should avoid the keyboard */
+ avoidKeyboard?: boolean;
- /** Modal container styles */
- innerContainerStyle?: ViewStyle;
+ /** Modal container styles */
+ innerContainerStyle?: ViewStyle;
- /**
- * Whether the modal should hide its content while animating. On iOS, set to true
- * if `useNativeDriver` is also true, to avoid flashes in the UI.
- *
- * See: https://github.com/react-native-modal/react-native-modal/pull/116
- * */
- hideModalContentWhileAnimating?: boolean;
- };
+ /**
+ * Whether the modal should hide its content while animating. On iOS, set to true
+ * if `useNativeDriver` is also true, to avoid flashes in the UI.
+ *
+ * See: https://github.com/react-native-modal/react-native-modal/pull/116
+ * */
+ hideModalContentWhileAnimating?: boolean;
+};
export default BaseModalProps;
export type {PopoverAnchorPosition};
diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.tsx
similarity index 69%
rename from src/components/MoneyReportHeader.js
rename to src/components/MoneyReportHeader.tsx
index ce1c9611c733..afdc62218f95 100644
--- a/src/components/MoneyReportHeader.js
+++ b/src/components/MoneyReportHeader.tsx
@@ -1,108 +1,69 @@
-import lodashGet from 'lodash/get';
-import PropTypes from 'prop-types';
-import React, {useCallback, useMemo, useState} from 'react';
+import React, {useMemo} from 'react';
import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
-import _ from 'underscore';
import GoogleMeetIcon from '@assets/images/google-meet.svg';
import ZoomIcon from '@assets/images/zoom-icon.svg';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
-import compose from '@libs/compose';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as HeaderUtils from '@libs/HeaderUtils';
import Navigation from '@libs/Navigation/Navigation';
import * as ReportUtils from '@libs/ReportUtils';
-import iouReportPropTypes from '@pages/iouReportPropTypes';
-import nextStepPropTypes from '@pages/nextStepPropTypes';
-import reportPropTypes from '@pages/reportPropTypes';
import * as IOU from '@userActions/IOU';
import * as Link from '@userActions/Link';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
+import type * as OnyxTypes from '@src/types/onyx';
+import type DeepValueOf from '@src/types/utils/DeepValueOf';
import Button from './Button';
-import ConfirmModal from './ConfirmModal';
import HeaderWithBackButton from './HeaderWithBackButton';
-import * as Expensicons from './Icon/Expensicons';
import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar';
-import participantPropTypes from './participantPropTypes';
import SettlementButton from './SettlementButton';
-import withWindowDimensions, {windowDimensionsPropTypes} from './withWindowDimensions';
-const propTypes = {
- /** The report currently being looked at */
- report: iouReportPropTypes.isRequired,
-
- /** The policy tied to the money request report */
- policy: PropTypes.shape({
- /** Name of the policy */
- name: PropTypes.string,
-
- /** Type of the policy */
- type: PropTypes.string,
-
- /** The role of the current user in the policy */
- role: PropTypes.string,
-
- /** Whether Scheduled Submit is turned on for this policy */
- isHarvestingEnabled: PropTypes.bool,
- }),
+type PaymentType = DeepValueOf;
+type MoneyReportHeaderOnyxProps = {
/** The chat report this report is linked to */
- chatReport: reportPropTypes,
+ chatReport: OnyxEntry;
/** The next step for the report */
- nextStep: nextStepPropTypes,
-
- /** Personal details so we can get the ones for the report participants */
- personalDetails: PropTypes.objectOf(participantPropTypes).isRequired,
+ nextStep: OnyxEntry;
/** Session info for the currently logged in user. */
- session: PropTypes.shape({
- /** Currently logged in user email */
- email: PropTypes.string,
- }),
-
- ...windowDimensionsPropTypes,
+ session: OnyxEntry;
};
-const defaultProps = {
- chatReport: {},
- nextStep: {},
- session: {
- email: null,
- },
- policy: {
- isHarvestingEnabled: false,
- },
+type MoneyReportHeaderProps = MoneyReportHeaderOnyxProps & {
+ /** The report currently being looked at */
+ report: OnyxTypes.Report;
+
+ /** The policy tied to the money request report */
+ policy: OnyxTypes.Policy;
+
+ /** Personal details so we can get the ones for the report participants */
+ personalDetails: OnyxTypes.PersonalDetailsList;
};
-function MoneyReportHeader({session, personalDetails, policy, chatReport, nextStep, report: moneyRequestReport, isSmallScreenWidth}) {
+function MoneyReportHeader({session, personalDetails, policy, chatReport, nextStep, report: moneyRequestReport}: MoneyReportHeaderProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
- const {windowWidth} = useWindowDimensions();
+ const {windowWidth, isSmallScreenWidth} = useWindowDimensions();
const reimbursableTotal = ReportUtils.getMoneyRequestReimbursableTotal(moneyRequestReport);
const isApproved = ReportUtils.isReportApproved(moneyRequestReport);
const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID);
- const policyType = lodashGet(policy, 'type');
- const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && lodashGet(policy, 'role') === CONST.POLICY.ROLE.ADMIN;
+ const policyType = policy?.type;
+ const isPolicyAdmin = policyType !== CONST.POLICY.TYPE.PERSONAL && policy?.role === CONST.POLICY.ROLE.ADMIN;
const isPaidGroupPolicy = ReportUtils.isPaidGroupPolicy(moneyRequestReport);
- const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && lodashGet(session, 'accountID', null) === moneyRequestReport.managerID;
+ const isManager = ReportUtils.isMoneyRequestReport(moneyRequestReport) && session?.accountID === moneyRequestReport.managerID;
const isPayer = isPaidGroupPolicy
? // In a group policy, the admin approver can pay the report directly by skipping the approval step
isPolicyAdmin && (isApproved || isManager)
: isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager);
const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport);
- const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);
-
- const cancelPayment = useCallback(() => {
- IOU.cancelPayment(moneyRequestReport, chatReport);
- setIsConfirmModalVisible(false);
- }, [moneyRequestReport, chatReport]);
-
const shouldShowPayButton = useMemo(
() => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport),
[isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport],
@@ -116,7 +77,7 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
const shouldShowSettlementButton = shouldShowPayButton || shouldShowApproveButton;
const shouldShowSubmitButton = isDraft && reimbursableTotal !== 0;
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
- const shouldShowNextStep = isFromPaidPolicy && nextStep && !_.isEmpty(nextStep.message);
+ const shouldShowNextStep = isFromPaidPolicy && !!nextStep?.message?.length;
const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableTotal, moneyRequestReport.currency);
@@ -124,18 +85,11 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
// The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
const isWaitingForSubmissionFromCurrentUser = useMemo(
- () => chatReport.isOwnPolicyExpenseChat && !policy.isHarvestingEnabled,
- [chatReport.isOwnPolicyExpenseChat, policy.isHarvestingEnabled],
+ () => chatReport?.isOwnPolicyExpenseChat && !policy.isHarvestingEnabled,
+ [chatReport?.isOwnPolicyExpenseChat, policy.isHarvestingEnabled],
);
const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)];
- if (isPayer && isSettled && ReportUtils.isExpenseReport(moneyRequestReport)) {
- threeDotsMenuItems.push({
- icon: Expensicons.Trashcan,
- text: translate('iou.cancelPayment'),
- onSelected: () => setIsConfirmModalVisible(true),
- });
- }
if (!ReportUtils.isArchivedRoom(chatReport)) {
threeDotsMenuItems.push({
icon: ZoomIcon,
@@ -173,11 +127,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
{shouldShowSettlementButton && !isSmallScreenWidth && (
IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
+ // @ts-expect-error TODO: Remove this once IOU (https://github.com/Expensify/App/issues/24926) is migrated to TypeScript.
+ onPress={(paymentType: PaymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
@@ -203,11 +159,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
{shouldShowSettlementButton && isSmallScreenWidth && (
IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
+ // @ts-expect-error TODO: Remove this once IOU (https://github.com/Expensify/App/issues/24926) is migrated to TypeScript.
+ onPress={(paymentType: PaymentType) => IOU.payMoneyRequest(paymentType, chatReport, moneyRequestReport)}
enablePaymentsRoute={ROUTES.ENABLE_PAYMENTS}
addBankAccountRoute={bankAccountRoute}
shouldHidePaymentOptions={!shouldShowPayButton}
@@ -233,35 +191,20 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
)}
- setIsConfirmModalVisible(false)}
- prompt={translate('iou.cancelPaymentConfirmation')}
- confirmText={translate('iou.cancelPayment')}
- cancelText={translate('common.dismiss')}
- danger
- />
);
}
MoneyReportHeader.displayName = 'MoneyReportHeader';
-MoneyReportHeader.propTypes = propTypes;
-MoneyReportHeader.defaultProps = defaultProps;
-export default compose(
- withWindowDimensions,
- withOnyx({
- chatReport: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`,
- },
- nextStep: {
- key: ({report}) => `${ONYXKEYS.COLLECTION.NEXT_STEP}${report.reportID}`,
- },
- session: {
- key: ONYXKEYS.SESSION,
- },
- }),
-)(MoneyReportHeader);
+export default withOnyx({
+ chatReport: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT}${report.chatReportID}`,
+ },
+ nextStep: {
+ key: ({report}) => `${ONYXKEYS.COLLECTION.NEXT_STEP}${report.reportID}`,
+ },
+ session: {
+ key: ONYXKEYS.SESSION,
+ },
+})(MoneyReportHeader);
diff --git a/src/components/MoneyReportHeaderStatusBar.tsx b/src/components/MoneyReportHeaderStatusBar.tsx
index 4b30276a204f..7d2b749cce0a 100644
--- a/src/components/MoneyReportHeaderStatusBar.tsx
+++ b/src/components/MoneyReportHeaderStatusBar.tsx
@@ -1,11 +1,12 @@
import React, {useMemo} from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as NextStepUtils from '@libs/NextStepUtils';
import CONST from '@src/CONST';
import type ReportNextStep from '@src/types/onyx/ReportNextStep';
import RenderHTML from './RenderHTML';
+import Text from './Text';
type MoneyReportHeaderStatusBarProps = {
/** The next step for the report */
diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts
index 3d1f95822e6a..87a09895d50f 100644
--- a/src/components/Popover/types.ts
+++ b/src/components/Popover/types.ts
@@ -1,8 +1,11 @@
+import type {RefObject} from 'react';
+import type {View} from 'react-native';
import type {ValueOf} from 'type-fest';
import type {PopoverAnchorPosition} from '@components/Modal/types';
import type BaseModalProps from '@components/Modal/types';
import type {WindowDimensionsProps} from '@components/withWindowDimensions/types';
import type CONST from '@src/CONST';
+import type ChildrenProps from '@src/types/utils/ChildrenProps';
type AnchorAlignment = {
/** The horizontal anchor alignment of the popover */
@@ -17,34 +20,32 @@ type PopoverDimensions = {
height: number;
};
-type PopoverProps = BaseModalProps & {
- /** The anchor position of the popover */
- anchorPosition?: PopoverAnchorPosition;
+type PopoverProps = BaseModalProps &
+ ChildrenProps & {
+ /** The anchor position of the popover */
+ anchorPosition?: PopoverAnchorPosition;
- /** The anchor alignment of the popover */
- anchorAlignment: AnchorAlignment;
+ /** The anchor alignment of the popover */
+ anchorAlignment?: AnchorAlignment;
- /** The anchor ref of the popover */
- anchorRef: React.RefObject;
+ /** The anchor ref of the popover */
+ anchorRef: RefObject;
- /** Whether disable the animations */
- disableAnimation: boolean;
+ /** Whether disable the animations */
+ disableAnimation?: boolean;
- /** Whether we don't want to show overlay */
- withoutOverlay: boolean;
+ /** Whether we don't want to show overlay */
+ withoutOverlay: boolean;
- /** The dimensions of the popover */
- popoverDimensions?: PopoverDimensions;
+ /** The dimensions of the popover */
+ popoverDimensions?: PopoverDimensions;
- /** The ref of the popover */
- withoutOverlayRef?: React.RefObject;
+ /** The ref of the popover */
+ withoutOverlayRef?: RefObject;
- /** Whether we want to show the popover on the right side of the screen */
- fromSidebarMediumScreen?: boolean;
-
- /** The popover children */
- children: React.ReactNode;
-};
+ /** Whether we want to show the popover on the right side of the screen */
+ fromSidebarMediumScreen?: boolean;
+ };
type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps;
diff --git a/src/components/PopoverProvider/index.tsx b/src/components/PopoverProvider/index.tsx
index b50b04289813..b1a6ebb0c5c0 100644
--- a/src/components/PopoverProvider/index.tsx
+++ b/src/components/PopoverProvider/index.tsx
@@ -1,18 +1,27 @@
-import React from 'react';
+import type {RefObject} from 'react';
+import React, {createContext, useCallback, useEffect, useMemo, useRef, useState} from 'react';
+import type {View} from 'react-native';
import type {AnchorRef, PopoverContextProps, PopoverContextValue} from './types';
-const PopoverContext = React.createContext({
+const PopoverContext = createContext({
onOpen: () => {},
popover: {},
close: () => {},
isOpen: false,
});
+function elementContains(ref: RefObject | undefined, target: EventTarget | null) {
+ if (ref?.current && 'contains' in ref?.current && ref?.current?.contains(target as Node)) {
+ return true;
+ }
+ return false;
+}
+
function PopoverContextProvider(props: PopoverContextProps) {
- const [isOpen, setIsOpen] = React.useState(false);
- const activePopoverRef = React.useRef(null);
+ const [isOpen, setIsOpen] = useState(false);
+ const activePopoverRef = useRef(null);
- const closePopover = React.useCallback((anchorRef?: React.RefObject) => {
+ const closePopover = useCallback((anchorRef?: RefObject) => {
if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) {
return;
}
@@ -25,10 +34,9 @@ function PopoverContextProvider(props: PopoverContextProps) {
setIsOpen(false);
}, []);
- React.useEffect(() => {
+ useEffect(() => {
const listener = (e: Event) => {
- // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
- if (activePopoverRef.current?.ref?.current?.contains(e.target as Node) || activePopoverRef.current?.anchorRef?.current?.contains(e.target as Node)) {
+ if (elementContains(activePopoverRef.current?.ref, e.target) || elementContains(activePopoverRef.current?.anchorRef, e.target)) {
return;
}
const ref = activePopoverRef.current?.anchorRef;
@@ -40,9 +48,9 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- React.useEffect(() => {
+ useEffect(() => {
const listener = (e: Event) => {
- if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) {
+ if (elementContains(activePopoverRef.current?.ref, e.target)) {
return;
}
closePopover();
@@ -53,7 +61,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- React.useEffect(() => {
+ useEffect(() => {
const listener = (e: KeyboardEvent) => {
if (e.key !== 'Escape') {
return;
@@ -66,7 +74,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- React.useEffect(() => {
+ useEffect(() => {
const listener = () => {
if (document.hasFocus()) {
return;
@@ -79,9 +87,9 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- React.useEffect(() => {
+ useEffect(() => {
const listener = (e: Event) => {
- if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) {
+ if (elementContains(activePopoverRef.current?.ref, e.target)) {
return;
}
@@ -93,7 +101,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
};
}, [closePopover]);
- const onOpen = React.useCallback(
+ const onOpen = useCallback(
(popoverParams: AnchorRef) => {
if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams?.ref) {
closePopover(activePopoverRef.current.anchorRef);
@@ -107,7 +115,7 @@ function PopoverContextProvider(props: PopoverContextProps) {
[closePopover],
);
- const contextValue = React.useMemo(
+ const contextValue = useMemo(
() => ({
onOpen,
close: closePopover,
diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts
index ffd0087cd5ff..49705d7ea7a8 100644
--- a/src/components/PopoverProvider/types.ts
+++ b/src/components/PopoverProvider/types.ts
@@ -1,18 +1,21 @@
+import type {ReactNode, RefObject} from 'react';
+import type {View} from 'react-native';
+
type PopoverContextProps = {
- children: React.ReactNode;
+ children: ReactNode;
};
type PopoverContextValue = {
onOpen?: (popoverParams: AnchorRef) => void;
popover?: AnchorRef | Record | null;
- close: (anchorRef?: React.RefObject) => void;
+ close: (anchorRef?: RefObject) => void;
isOpen: boolean;
};
type AnchorRef = {
- ref: React.RefObject;
- close: (anchorRef?: React.RefObject) => void;
- anchorRef: React.RefObject;
+ ref: RefObject;
+ close: (anchorRef?: RefObject) => void;
+ anchorRef: RefObject;
onOpenCallback?: () => void;
onCloseCallback?: () => void;
};
diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx
index 6aed275bd2dc..58d022ef9d65 100644
--- a/src/components/PopoverWithoutOverlay/index.tsx
+++ b/src/components/PopoverWithoutOverlay/index.tsx
@@ -8,6 +8,7 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Modal from '@userActions/Modal';
+import viewRef from '@src/types/utils/viewRef';
import type PopoverWithoutOverlayProps from './types';
function PopoverWithoutOverlay(
@@ -52,7 +53,7 @@ function PopoverWithoutOverlay(
close: onClose,
anchorRef,
});
- removeOnClose = Modal.setCloseModal(() => onClose(anchorRef));
+ removeOnClose = Modal.setCloseModal(onClose);
} else {
onModalHide();
close(anchorRef);
@@ -119,7 +120,7 @@ function PopoverWithoutOverlay(
return (
;
+ anchorRef: RefObject;
/** A react-native-animatable animation timing for the modal display animation */
animationInTiming?: number;
@@ -22,7 +23,7 @@ type PopoverWithoutOverlayProps = ChildrenProps &
disableAnimation?: boolean;
/** The ref of the popover */
- withoutOverlayRef: React.RefObject;
+ withoutOverlayRef: RefObject;
};
export default PopoverWithoutOverlayProps;
diff --git a/src/components/ProcessMoneyRequestHoldMenu.tsx b/src/components/ProcessMoneyRequestHoldMenu.tsx
index 1b711633ed3b..5f32240aca9b 100644
--- a/src/components/ProcessMoneyRequestHoldMenu.tsx
+++ b/src/components/ProcessMoneyRequestHoldMenu.tsx
@@ -1,3 +1,4 @@
+import type {RefObject} from 'react';
import React from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
@@ -27,7 +28,7 @@ type ProcessMoneyRequestHoldMenuProps = {
anchorAlignment: AnchorAlignment;
/** The anchor ref of the popover menu */
- anchorRef: React.RefObject;
+ anchorRef: RefObject;
};
function ProcessMoneyRequestHoldMenu({isVisible, onClose, onConfirm, anchorPosition, anchorAlignment, anchorRef}: ProcessMoneyRequestHoldMenuProps) {
diff --git a/src/components/QRShare/QRShareWithDownload/index.native.tsx b/src/components/QRShare/QRShareWithDownload/index.native.tsx
index d1d9f13147f1..7d192c84c454 100644
--- a/src/components/QRShare/QRShareWithDownload/index.native.tsx
+++ b/src/components/QRShare/QRShareWithDownload/index.native.tsx
@@ -3,6 +3,7 @@ import React, {forwardRef, useImperativeHandle, useRef} from 'react';
import ViewShot from 'react-native-view-shot';
import getQrCodeFileName from '@components/QRShare/getQrCodeDownloadFileName';
import type {QRShareProps} from '@components/QRShare/types';
+import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import fileDownload from '@libs/fileDownload';
import QRShare from '..';
@@ -10,14 +11,16 @@ import type QRShareWithDownloadHandle from './types';
function QRShareWithDownload(props: QRShareProps, ref: ForwardedRef) {
const {isOffline} = useNetwork();
+ const {translate} = useLocalize();
+
const qrCodeScreenshotRef = useRef(null);
useImperativeHandle(
ref,
() => ({
- download: () => qrCodeScreenshotRef.current?.capture?.().then((uri) => fileDownload(uri, getQrCodeFileName(props.title))),
+ download: () => qrCodeScreenshotRef.current?.capture?.().then((uri) => fileDownload(uri, getQrCodeFileName(props.title), translate('fileDownload.success.qrMessage'))),
}),
- [props.title],
+ [props.title, translate],
);
return (
diff --git a/src/components/RadioButtons.tsx b/src/components/RadioButtons.tsx
index 8aa3ef7e8ffe..cb6843af65c0 100644
--- a/src/components/RadioButtons.tsx
+++ b/src/components/RadioButtons.tsx
@@ -1,4 +1,5 @@
import React, {useState} from 'react';
+import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import RadioButtonWithLabel from './RadioButtonWithLabel';
@@ -20,7 +21,7 @@ function RadioButtons({items, onPress}: RadioButtonsProps) {
const [checkedValue, setCheckedValue] = useState('');
return (
- <>
+
{items.map((item) => (
))}
- >
+
);
}
diff --git a/src/components/Reactions/MiniQuickEmojiReactions.tsx b/src/components/Reactions/MiniQuickEmojiReactions.tsx
index 9f38da6bdb3d..815736d8af76 100644
--- a/src/components/Reactions/MiniQuickEmojiReactions.tsx
+++ b/src/components/Reactions/MiniQuickEmojiReactions.tsx
@@ -1,6 +1,5 @@
import React, {useRef} from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
import {withOnyx} from 'react-native-onyx';
import type {Emoji} from '@assets/emojis/types';
import BaseMiniContextMenuItem from '@components/BaseMiniContextMenuItem';
@@ -12,20 +11,12 @@ import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as EmojiUtils from '@libs/EmojiUtils';
import getButtonState from '@libs/getButtonState';
+import variables from '@styles/variables';
import * as EmojiPickerAction from '@userActions/EmojiPickerAction';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
-import type {ReportActionReactions} from '@src/types/onyx';
-import type {BaseQuickEmojiReactionsProps} from './QuickEmojiReactions/types';
-
-type MiniQuickEmojiReactionsOnyxProps = {
- /** All the emoji reactions for the report action. */
- emojiReactions: OnyxEntry;
-
- /** The user's preferred skin tone. */
- preferredSkinTone: OnyxEntry;
-};
+import type {BaseQuickEmojiReactionsOnyxProps, BaseQuickEmojiReactionsProps} from './QuickEmojiReactions/types';
type MiniQuickEmojiReactionsProps = BaseQuickEmojiReactionsProps & {
/**
@@ -71,7 +62,7 @@ function MiniQuickEmojiReactions({
return (
- {CONST.QUICK_REACTIONS.map((emoji: Emoji) => (
+ {CONST.QUICK_REACTIONS.slice(0, 3).map((emoji: Emoji) => (
{({hovered, pressed}) => (
@@ -112,11 +104,14 @@ function MiniQuickEmojiReactions({
MiniQuickEmojiReactions.displayName = 'MiniQuickEmojiReactions';
-export default withOnyx({
+export default withOnyx({
preferredSkinTone: {
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
},
emojiReactions: {
key: ({reportActionID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS_REACTIONS}${reportActionID}`,
},
+ preferredLocale: {
+ key: ONYXKEYS.NVP_PREFERRED_LOCALE,
+ },
})(MiniQuickEmojiReactions);
diff --git a/src/components/Reactions/QuickEmojiReactions/types.ts b/src/components/Reactions/QuickEmojiReactions/types.ts
index d782d5ae35c7..9c17a87c56c0 100644
--- a/src/components/Reactions/QuickEmojiReactions/types.ts
+++ b/src/components/Reactions/QuickEmojiReactions/types.ts
@@ -11,18 +11,7 @@ type OpenPickerCallback = (element?: PickerRefElement, anchorOrigin?: AnchorOrig
type CloseContextMenuCallback = () => void;
-type BaseQuickEmojiReactionsOnyxProps = {
- /** All the emoji reactions for the report action. */
- emojiReactions: OnyxEntry;
-
- /** The user's preferred locale. */
- preferredLocale: OnyxEntry;
-
- /** The user's preferred skin tone. */
- preferredSkinTone: OnyxEntry;
-};
-
-type BaseQuickEmojiReactionsProps = BaseQuickEmojiReactionsOnyxProps & {
+type BaseReactionsProps = {
/** Callback to fire when an emoji is selected. */
onEmojiSelected: (emoji: Emoji, emojiReactions: OnyxEntry) => void;
@@ -45,7 +34,20 @@ type BaseQuickEmojiReactionsProps = BaseQuickEmojiReactionsOnyxProps & {
reportActionID: string;
};
-type QuickEmojiReactionsProps = BaseQuickEmojiReactionsProps & {
+type BaseQuickEmojiReactionsOnyxProps = {
+ /** All the emoji reactions for the report action. */
+ emojiReactions: OnyxEntry;
+
+ /** The user's preferred locale. */
+ preferredLocale: OnyxEntry;
+
+ /** The user's preferred skin tone. */
+ preferredSkinTone: OnyxEntry;
+};
+
+type BaseQuickEmojiReactionsProps = BaseReactionsProps & BaseQuickEmojiReactionsOnyxProps;
+
+type QuickEmojiReactionsProps = BaseReactionsProps & {
/**
* Function that can be called to close the context menu
* in which this component is rendered.
diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js
index 96c9e1b364d6..9cb27e6fac4a 100644
--- a/src/components/ReportActionItem/MoneyRequestPreview.js
+++ b/src/components/ReportActionItem/MoneyRequestPreview.js
@@ -1,3 +1,4 @@
+import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import {truncate} from 'lodash';
import lodashGet from 'lodash/get';
import PropTypes from 'prop-types';
@@ -12,6 +13,7 @@ import MultipleAvatars from '@components/MultipleAvatars';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import PressableWithFeedback from '@components/Pressable/PressableWithoutFeedback';
import refPropTypes from '@components/refPropTypes';
+import RenderHTML from '@components/RenderHTML';
import {showContextMenuForReport} from '@components/ShowContextMenuContext';
import Text from '@components/Text';
import transactionPropTypes from '@components/transactionPropTypes';
@@ -132,6 +134,7 @@ function MoneyRequestPreview(props) {
const StyleUtils = useStyleUtils();
const {translate} = useLocalize();
const {isSmallScreenWidth, windowWidth} = useWindowDimensions();
+ const parser = new ExpensiMark();
if (_.isEmpty(props.iouReport) && !props.isBillSplit) {
return null;
@@ -328,7 +331,8 @@ function MoneyRequestPreview(props) {
{!isCurrentUserManager && props.shouldShowPendingConversionMessage && (
{translate('iou.pendingConversionMessage')}
)}
- {(shouldShowDescription || shouldShowMerchant) && {merchantOrDescription}}
+ {shouldShowDescription && }
+ {shouldShowMerchant && {merchantOrDescription}}
{props.isBillSplit && !_.isEmpty(participantAccountIDs) && requestAmount > 0 && (
diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js
index 622cd75a568b..8483b7a481f2 100644
--- a/src/components/ReportActionItem/ReportPreview.js
+++ b/src/components/ReportActionItem/ReportPreview.js
@@ -166,6 +166,9 @@ function ReportPreview(props) {
const lastThreeTransactionsWithReceipts = transactionsWithReceipts.slice(-3);
const lastThreeReceipts = _.map(lastThreeTransactionsWithReceipts, (transaction) => ReceiptUtils.getThumbnailAndImageURIs(transaction));
let formattedMerchant = numberOfRequests === 1 && hasReceipts ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : null;
+ if (TransactionUtils.isPartialMerchant(formattedMerchant)) {
+ formattedMerchant = null;
+ }
const hasPendingWaypoints = formattedMerchant && hasOnlyDistanceRequests && _.every(transactionsWithReceipts, (transaction) => lodashGet(transaction, 'pendingFields.waypoints', null));
if (hasPendingWaypoints) {
formattedMerchant = formattedMerchant.replace(CONST.REGEX.FIRST_SPACE, props.translate('common.tbd'));
@@ -294,7 +297,7 @@ function ReportPreview(props) {
)}
- {!isScanning && (numberOfRequests > 1 || hasReceipts) && (
+ {!isScanning && (numberOfRequests > 1 || (hasReceipts && numberOfRequests === 1 && formattedMerchant)) && (
{previewSubtitle || moneyRequestComment}
diff --git a/src/components/ReportActionItem/TaskPreview.tsx b/src/components/ReportActionItem/TaskPreview.tsx
index a509d8d922e1..8ef837ed986d 100644
--- a/src/components/ReportActionItem/TaskPreview.tsx
+++ b/src/components/ReportActionItem/TaskPreview.tsx
@@ -1,5 +1,7 @@
import Str from 'expensify-common/lib/str';
import React from 'react';
+// eslint-disable-next-line no-restricted-imports
+import type {Text as RNText} from 'react-native';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
@@ -63,7 +65,7 @@ type TaskPreviewProps = WithCurrentUserPersonalDetailsProps &
chatReportID: string;
/** Popover context menu anchor, used for showing context menu */
- contextMenuAnchor: Element;
+ contextMenuAnchor: RNText | null;
/** Callback for updating context menu active state, used for showing context menu */
checkIfContextMenuActive: () => void;
@@ -112,7 +114,7 @@ function TaskPreview({
onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(taskReportID))}
onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()}
onPressOut={() => ControlSelection.unblock()}
- onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action ?? {}, checkIfContextMenuActive)}
+ onLongPress={(event) => showContextMenuForReport(event, contextMenuAnchor, chatReportID, action, checkIfContextMenuActive)}
style={[styles.flexRow, styles.justifyContentBetween]}
role={CONST.ROLE.BUTTON}
accessibilityLabel={translate('task.task')}
diff --git a/src/components/SectionList/index.android.tsx b/src/components/SectionList/index.android.tsx
index 1aa9b501146c..c119e8e9bcf3 100644
--- a/src/components/SectionList/index.android.tsx
+++ b/src/components/SectionList/index.android.tsx
@@ -1,19 +1,22 @@
import React, {forwardRef} from 'react';
+import type {ForwardedRef} from 'react';
import {SectionList as RNSectionList} from 'react-native';
-import type ForwardedSectionList from './types';
+import type {SectionListProps} from 'react-native';
// eslint-disable-next-line react/function-component-definition
-const SectionListWithRef: ForwardedSectionList = (props, ref) => (
-
-);
+function SectionListWithRef(props: SectionListProps, ref: ForwardedRef>) {
+ return (
+
+ );
+}
SectionListWithRef.displayName = 'SectionListWithRef';
diff --git a/src/components/SectionList/index.tsx b/src/components/SectionList/index.tsx
index 4af7ad33705c..1129b2bdbb8f 100644
--- a/src/components/SectionList/index.tsx
+++ b/src/components/SectionList/index.tsx
@@ -1,16 +1,17 @@
import React, {forwardRef} from 'react';
+import type {ForwardedRef} from 'react';
import {SectionList as RNSectionList} from 'react-native';
-import type ForwardedSectionList from './types';
+import type {SectionListProps} from 'react-native';
// eslint-disable-next-line react/function-component-definition
-const SectionList: ForwardedSectionList = (props, ref) => (
-
-);
-
-SectionList.displayName = 'SectionList';
+function SectionList(props: SectionListProps, ref: ForwardedRef>) {
+ return (
+
+ );
+}
export default forwardRef(SectionList);
diff --git a/src/components/SectionList/types.ts b/src/components/SectionList/types.ts
deleted file mode 100644
index 4648172aabfd..000000000000
--- a/src/components/SectionList/types.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import type {ForwardedRef} from 'react';
-import type {SectionList, SectionListProps} from 'react-native';
-
-type ForwardedSectionList = {
- (props: SectionListProps, ref: ForwardedRef): React.ReactNode;
- displayName: string;
-};
-
-export default ForwardedSectionList;
diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.tsx
similarity index 73%
rename from src/components/SelectionList/BaseListItem.js
rename to src/components/SelectionList/BaseListItem.tsx
index cfd39ab0ebb8..59a1c4dd08ce 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,13 +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 ListItem = isUserItem ? UserListItem : RadioListItem;
+ const isRadioItem = item.rightElement === undefined;
const rightHandSideComponentRender = () => {
if (canSelectMultiple || !rightHandSideComponent) {
@@ -70,7 +68,7 @@ function BaseListItem({
styles.justifyContentBetween,
styles.sidebarLinkInner,
styles.userSelectNone,
- isUserItem ? styles.peopleRow : styles.optionRow,
+ isRadioItem ? styles.optionRow : styles.peopleRow,
isFocused && styles.sidebarLinkActive,
]}
>
@@ -100,20 +98,32 @@ function BaseListItem({
)}
-
+
+ {isRadioItem ? (
+ onSelectRow(item)}
+ showTooltip={showTooltip}
+ />
+ ) : (
+ onSelectRow(item)}
+ showTooltip={showTooltip}
+ />
+ )}
{!canSelectMultiple && item.isSelected && !rightHandSideComponent && (
- {Boolean(item.invitedSecondaryLogin) && (
+ {!!item.invitedSecondaryLogin && (
{translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})}
@@ -140,6 +150,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..cc55b8e4fc17 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,60 @@ 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 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 +78,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 +104,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,19 +144,19 @@ 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) {
@@ -182,7 +171,7 @@ function BaseSelectionList({
// 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 +186,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 +222,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 +243,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 +252,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 +271,8 @@ function BaseSelectionList({
};
};
- const renderSectionHeader = ({section}) => {
- if (!section.title || _.isEmpty(section.data)) {
+ const renderSectionHeader = ({section}: {section: SectionListDataType}) => {
+ if (!section.title || !section.data) {
return null;
}
@@ -300,9 +287,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 +300,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 +312,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 +329,7 @@ function BaseSelectionList({
);
const updateAndScrollToFocusedIndex = useCallback(
- (newFocusedIndex) => {
+ (newFocusedIndex: number) => {
setFocusedIndex(newFocusedIndex);
scrollToIndex(newFocusedIndex, true);
},
@@ -355,7 +340,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 +372,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 +380,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 +391,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 +420,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 ? (
) : (
@@ -472,9 +465,9 @@ function BaseSelectionList({
getItemLayout={getItemLayout}
onScroll={onScroll}
onScrollBeginDrag={onScrollBeginDrag}
- keyExtractor={(item) => item.keyForList}
+ keyExtractor={(item: TItem) => item.keyForList}
extraData={focusedIndex}
- indicatorStyle={theme.white}
+ indicatorStyle="white"
keyboardShouldPersistTaps="always"
showsVerticalScrollIndicator={showScrollIndicator}
initialNumToRender={12}
@@ -500,7 +493,7 @@ function BaseSelectionList({
/>
)}
- {Boolean(footerContent) && {footerContent}}
+ {!!footerContent && {footerContent}}
)}
@@ -509,6 +502,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..5c28a139903d
--- /dev/null
+++ b/src/components/SelectionList/types.ts
@@ -0,0 +1,277 @@
+import type {ReactElement, ReactNode} from 'react';
+import type {GestureResponderEvent, InputModeOptions, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native';
+import type {SubAvatar} from '@components/SubscriptAvatar';
+import type {Errors, 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?: SubAvatar[];
+
+ /** 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;
+
+ /** Element to show on the right side of the item */
+ rightElement?: undefined;
+
+ /** Whether this option is disabled for selection */
+ isDisabled?: undefined;
+
+ invitedSecondaryLogin?: undefined;
+
+ /** Errors that this user may contain */
+ errors?: undefined;
+
+ /** The type of action that's pending */
+ pendingAction?: undefined;
+
+ /** 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/ShowContextMenuContext.js b/src/components/ShowContextMenuContext.js
deleted file mode 100644
index 04ccd5002b60..000000000000
--- a/src/components/ShowContextMenuContext.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import React from 'react';
-import * as DeviceCapabilities from '@libs/DeviceCapabilities';
-import * as ReportUtils from '@libs/ReportUtils';
-import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
-import CONST from '@src/CONST';
-
-const ShowContextMenuContext = React.createContext({
- anchor: null,
- report: null,
- action: undefined,
- checkIfContextMenuActive: () => {},
-});
-
-ShowContextMenuContext.displayName = 'ShowContextMenuContext';
-
-/**
- * Show the report action context menu.
- *
- * @param {Object} event - Press event object
- * @param {Element} anchor - Context menu anchor
- * @param {String} reportID - Active Report ID
- * @param {Object} action - ReportAction for ContextMenu
- * @param {Function} checkIfContextMenuActive Callback to update context menu active state
- * @param {Boolean} [isArchivedRoom=false] - Is the report an archived room
- */
-function showContextMenuForReport(event, anchor, reportID, action, checkIfContextMenuActive, isArchivedRoom = false) {
- if (!DeviceCapabilities.canUseTouchScreen()) {
- return;
- }
- ReportActionContextMenu.showContextMenu(
- CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
- event,
- '',
- anchor,
- reportID,
- action.reportActionID,
- ReportUtils.getOriginalReportID(reportID, action),
- undefined,
- checkIfContextMenuActive,
- checkIfContextMenuActive,
- isArchivedRoom,
- );
-}
-
-export {ShowContextMenuContext, showContextMenuForReport};
diff --git a/src/components/ShowContextMenuContext.ts b/src/components/ShowContextMenuContext.ts
new file mode 100644
index 000000000000..17557051bef9
--- /dev/null
+++ b/src/components/ShowContextMenuContext.ts
@@ -0,0 +1,64 @@
+import {createContext} from 'react';
+// eslint-disable-next-line no-restricted-imports
+import type {GestureResponderEvent, Text as RNText} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import * as DeviceCapabilities from '@libs/DeviceCapabilities';
+import * as ReportUtils from '@libs/ReportUtils';
+import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu';
+import CONST from '@src/CONST';
+import type {Report, ReportAction} from '@src/types/onyx';
+
+type ShowContextMenuContextProps = {
+ anchor: RNText | null;
+ report: OnyxEntry;
+ action: OnyxEntry;
+ checkIfContextMenuActive: () => void;
+};
+
+const ShowContextMenuContext = createContext({
+ anchor: null,
+ report: null,
+ action: null,
+ checkIfContextMenuActive: () => {},
+});
+
+ShowContextMenuContext.displayName = 'ShowContextMenuContext';
+
+/**
+ * Show the report action context menu.
+ *
+ * @param event - Press event object
+ * @param anchor - Context menu anchor
+ * @param reportID - Active Report ID
+ * @param action - ReportAction for ContextMenu
+ * @param checkIfContextMenuActive Callback to update context menu active state
+ * @param isArchivedRoom - Is the report an archived room
+ */
+function showContextMenuForReport(
+ event: GestureResponderEvent | MouseEvent,
+ anchor: RNText | null,
+ reportID: string,
+ action: OnyxEntry,
+ checkIfContextMenuActive: () => void,
+ isArchivedRoom = false,
+) {
+ if (!DeviceCapabilities.canUseTouchScreen()) {
+ return;
+ }
+
+ ReportActionContextMenu.showContextMenu(
+ CONST.CONTEXT_MENU_TYPES.REPORT_ACTION,
+ event,
+ '',
+ anchor,
+ reportID,
+ action?.reportActionID,
+ ReportUtils.getOriginalReportID(reportID, action),
+ undefined,
+ checkIfContextMenuActive,
+ checkIfContextMenuActive,
+ isArchivedRoom,
+ );
+}
+
+export {ShowContextMenuContext, showContextMenuForReport};
diff --git a/src/components/ShowMoreButton/index.js b/src/components/ShowMoreButton/index.js
index 34b55fa5dcf1..28c33d185cff 100644
--- a/src/components/ShowMoreButton/index.js
+++ b/src/components/ShowMoreButton/index.js
@@ -1,9 +1,10 @@
import PropTypes from 'prop-types';
import React from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import _ from 'underscore';
import Button from '@components/Button';
import * as Expensicons from '@components/Icon/Expensicons';
+import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/components/SingleChoiceQuestion.tsx b/src/components/SingleChoiceQuestion.tsx
index 4c2ba9c34a9f..c8bf783032ad 100644
--- a/src/components/SingleChoiceQuestion.tsx
+++ b/src/components/SingleChoiceQuestion.tsx
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import React, {forwardRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {Text as RNText} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import type {MaybePhraseKey} from '@libs/Localize';
diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx
index 00cf248ad838..2e2ae6d06e0f 100644
--- a/src/components/SubscriptAvatar.tsx
+++ b/src/components/SubscriptAvatar.tsx
@@ -104,3 +104,4 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV
SubscriptAvatar.displayName = 'SubscriptAvatar';
export default memo(SubscriptAvatar);
+export type {SubAvatar};
diff --git a/src/components/Text.tsx b/src/components/Text.tsx
index f436b9f4495a..b94530a423f7 100644
--- a/src/components/Text.tsx
+++ b/src/components/Text.tsx
@@ -1,5 +1,6 @@
import type {ForwardedRef} from 'react';
import React from 'react';
+// eslint-disable-next-line no-restricted-imports
import {Text as RNText, StyleSheet} from 'react-native';
import type {TextProps as RNTextProps, TextStyle} from 'react-native';
import useTheme from '@hooks/useTheme';
diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx
index d19d835d68bb..99b3e98588ac 100644
--- a/src/components/TextInput/BaseTextInput/index.native.tsx
+++ b/src/components/TextInput/BaseTextInput/index.native.tsx
@@ -263,7 +263,7 @@ function BaseTextInput(
return (
<>
diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx
index 507d40f475a7..8f6d3efdcd8d 100644
--- a/src/components/TextInput/TextInputLabel/index.tsx
+++ b/src/components/TextInput/TextInputLabel/index.tsx
@@ -1,4 +1,5 @@
import React, {useEffect, useRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {Text} from 'react-native';
import {Animated} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/components/TextLink.tsx b/src/components/TextLink.tsx
index d3c515115d56..c8cd39b05fcc 100644
--- a/src/components/TextLink.tsx
+++ b/src/components/TextLink.tsx
@@ -1,5 +1,6 @@
import type {ForwardedRef, KeyboardEventHandler, MouseEventHandler} from 'react';
import React, {forwardRef} from 'react';
+// eslint-disable-next-line no-restricted-imports
import type {GestureResponderEvent, Text as RNText, StyleProp, TextStyle} from 'react-native';
import useEnvironment from '@hooks/useEnvironment';
import useThemeStyles from '@hooks/useThemeStyles';
diff --git a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx
index 0df9993f8c69..21e19ac7c2e8 100644
--- a/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx
+++ b/src/components/UserDetailsTooltip/BaseUserDetailsTooltip/index.tsx
@@ -1,8 +1,9 @@
import Str from 'expensify-common/lib/str';
import React, {useCallback} from 'react';
-import {Text, View} from 'react-native';
+import {View} from 'react-native';
import Avatar from '@components/Avatar';
import {usePersonalDetails} from '@components/OnyxProvider';
+import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import type UserDetailsTooltipProps from '@components/UserDetailsTooltip/types';
import useLocalize from '@hooks/useLocalize';
diff --git a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx
similarity index 71%
rename from src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js
rename to src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx
index 54e7309ee48b..9f615cef525d 100755
--- a/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.js
+++ b/src/components/VideoChatButtonAndMenu/BaseVideoChatButtonAndMenu.tsx
@@ -1,7 +1,5 @@
-import PropTypes from 'prop-types';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import {Dimensions, View} from 'react-native';
-import _ from 'underscore';
import GoogleMeetIcon from '@assets/images/google-meet.svg';
import ZoomIcon from '@assets/images/zoom-icon.svg';
import Icon from '@components/Icon';
@@ -10,37 +8,34 @@ import MenuItem from '@components/MenuItem';
import Popover from '@components/Popover';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
import Tooltip from '@components/Tooltip/PopoverAnchorTooltip';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions';
+import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
-import compose from '@libs/compose';
+import useWindowDimensions from '@hooks/useWindowDimensions';
import * as Link from '@userActions/Link';
import * as Session from '@userActions/Session';
import CONST from '@src/CONST';
-import {defaultProps, propTypes as videoChatButtonAndMenuPropTypes} from './videoChatButtonAndMenuPropTypes';
+import type VideoChatButtonAndMenuProps from './types';
-const propTypes = {
+type BaseVideoChatButtonAndMenuProps = VideoChatButtonAndMenuProps & {
/** Link to open when user wants to create a new google meet meeting */
- googleMeetURL: PropTypes.string.isRequired,
-
- ...videoChatButtonAndMenuPropTypes,
- ...withLocalizePropTypes,
- ...windowDimensionsPropTypes,
+ googleMeetURL: string;
};
-function BaseVideoChatButtonAndMenu(props) {
+function BaseVideoChatButtonAndMenu({googleMeetURL, isConcierge = false, guideCalendarLink}: BaseVideoChatButtonAndMenuProps) {
const theme = useTheme();
const styles = useThemeStyles();
+ const {translate} = useLocalize();
+ const {isSmallScreenWidth} = useWindowDimensions();
const [isVideoChatMenuActive, setIsVideoChatMenuActive] = useState(false);
const [videoChatIconPosition, setVideoChatIconPosition] = useState({x: 0, y: 0});
- const videoChatIconWrapperRef = useRef(null);
- const videoChatButtonRef = useRef(null);
+ const videoChatIconWrapperRef = useRef(null);
+ const videoChatButtonRef = useRef(null);
const menuItemData = [
{
icon: ZoomIcon,
- text: props.translate('videoChatButtonAndMenu.zoom'),
+ text: translate('videoChatButtonAndMenu.zoom'),
onPress: () => {
setIsVideoChatMenuActive(false);
Link.openExternalLink(CONST.NEW_ZOOM_MEETING_URL);
@@ -48,10 +43,10 @@ function BaseVideoChatButtonAndMenu(props) {
},
{
icon: GoogleMeetIcon,
- text: props.translate('videoChatButtonAndMenu.googleMeet'),
+ text: translate('videoChatButtonAndMenu.googleMeet'),
onPress: () => {
setIsVideoChatMenuActive(false);
- Link.openExternalLink(props.googleMeetURL);
+ Link.openExternalLink(googleMeetURL);
},
},
];
@@ -87,22 +82,22 @@ function BaseVideoChatButtonAndMenu(props) {
ref={videoChatIconWrapperRef}
onLayout={measureVideoChatIconPosition}
>
-
+ {
// Drop focus to avoid blue focus ring.
- videoChatButtonRef.current.blur();
+ videoChatButtonRef.current?.blur();
// If this is the Concierge chat, we'll open the modal for requesting a setup call instead
- if (props.isConcierge && props.guideCalendarLink) {
- Link.openExternalLink(props.guideCalendarLink);
+ if (isConcierge && guideCalendarLink) {
+ Link.openExternalLink(guideCalendarLink);
return;
}
setIsVideoChatMenuActive((previousVal) => !previousVal);
})}
style={styles.touchableButtonImage}
- accessibilityLabel={props.translate('videoChatButtonAndMenu.tooltip')}
+ accessibilityLabel={translate('videoChatButtonAndMenu.tooltip')}
role={CONST.ROLE.BUTTON}
>
-
- {_.map(menuItemData, ({icon, text, onPress}) => (
+
+ {menuItemData.map(({icon, text, onPress}) => (