diff --git a/.github/actions/composite/setupNode/action.yml b/.github/actions/composite/setupNode/action.yml
index 6bdf500912c0..ffaa55c0b3be 100644
--- a/.github/actions/composite/setupNode/action.yml
+++ b/.github/actions/composite/setupNode/action.yml
@@ -24,6 +24,31 @@ runs:
path: desktop/node_modules
key: ${{ runner.os }}-desktop-node-modules-${{ hashFiles('desktop/package-lock.json') }}
+ - name: Check if patch files changed
+ id: patchCheck
+ shell: bash
+ run: |
+ set -e
+ if [[ `git diff main --name-only | grep \.patch` != null ]]; then
+ echo 'CHANGES_IN_PATCH_FILES=true' >> "$GITHUB_OUTPUT"
+ else
+ echo 'CHANGES_IN_PATCH_FILES=false' >> "$GITHUB_OUTPUT"
+ fi
+
+ - name: Patch root project node packages
+ shell: bash
+ if: |
+ steps.patchCheck.outputs.CHANGES_IN_PATCH_FILES == 'true' &&
+ steps.cache-node-modules.outputs.cache-hit == 'true'
+ run: npx patch-package
+
+ - name: Patch node packages for desktop submodule
+ shell: bash
+ if: |
+ steps.patchCheck.outputs.CHANGES_IN_PATCH_FILES == 'true' &&
+ steps.cache-desktop-node-modules.outputs.cache-hit == 'true'
+ run: cd desktop && npx patch-package
+
- name: Install root project node packages
if: steps.cache-node-modules.outputs.cache-hit != 'true'
uses: nick-fields/retry@v2
diff --git a/android/app/build.gradle b/android/app/build.gradle
index c6c2e308bac2..294d2d334ffd 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -90,8 +90,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001035703
- versionName "1.3.57-3"
+ versionCode 1001035705
+ versionName "1.3.57-5"
}
flavorDimensions "default"
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 66186890d68f..384c96a5712b 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.3.57.3
+ 1.3.57.5
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index d74d2f154b38..90502c109aab 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -19,6 +19,6 @@
CFBundleSignature
????
CFBundleVersion
- 1.3.57.3
+ 1.3.57.5
diff --git a/package-lock.json b/package-lock.json
index 2b3df7229b67..1d6b5ce003ba 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.3.57-3",
+ "version": "1.3.57-5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.3.57-3",
+ "version": "1.3.57-5",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -45,7 +45,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
"fbjs": "^3.0.2",
"htmlparser2": "^7.2.0",
"idb-keyval": "^6.2.1",
@@ -29543,8 +29543,8 @@
},
"node_modules/expensify-common": {
"version": "1.0.0",
- "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
- "integrity": "sha512-nNYAweSE5bwjKyFTi9tz+p1z+gxkytCnIa8M11vnseV60ZzJespcwB/2SbWkdaAL5wpvcgHLlFTTGbPUwIiTvw==",
+ "resolved": "git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
+ "integrity": "sha512-uhcd3sV276dFdrJCvk+KFWjMQ6rMKUNrNcBiZNB58S9NECCVioRuqFJXgfy90DcQz2CELHzZATdciXQ+fJ0Qhw==",
"license": "MIT",
"dependencies": {
"classnames": "2.3.1",
@@ -70813,9 +70813,9 @@
}
},
"expensify-common": {
- "version": "git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
- "integrity": "sha512-nNYAweSE5bwjKyFTi9tz+p1z+gxkytCnIa8M11vnseV60ZzJespcwB/2SbWkdaAL5wpvcgHLlFTTGbPUwIiTvw==",
- "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
+ "version": "git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
+ "integrity": "sha512-uhcd3sV276dFdrJCvk+KFWjMQ6rMKUNrNcBiZNB58S9NECCVioRuqFJXgfy90DcQz2CELHzZATdciXQ+fJ0Qhw==",
+ "from": "expensify-common@git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
"requires": {
"classnames": "2.3.1",
"clipboard": "2.0.4",
diff --git a/package.json b/package.json
index 17dd83bffe35..eeb52419e1a0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.3.57-3",
+ "version": "1.3.57-5",
"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.",
@@ -85,7 +85,7 @@
"date-fns-tz": "^2.0.0",
"dom-serializer": "^0.2.2",
"domhandler": "^4.3.0",
- "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#4cc5f72b69bd77d2c8052a3c167d039e502a2796",
+ "expensify-common": "git+ssh://git@github.com/Expensify/expensify-common.git#7735de14112a968fd6a4f5af710d2fbaefc8809d",
"fbjs": "^3.0.2",
"htmlparser2": "^7.2.0",
"idb-keyval": "^6.2.1",
diff --git a/src/CONFIG.js b/src/CONFIG.ts
similarity index 62%
rename from src/CONFIG.js
rename to src/CONFIG.ts
index 4f9eab573a9e..e08b771d4b34 100644
--- a/src/CONFIG.js
+++ b/src/CONFIG.ts
@@ -1,25 +1,24 @@
-import get from 'lodash/get';
import {Platform} from 'react-native';
-import Config from 'react-native-config';
-import getPlatform from './libs/getPlatform/index';
+import Config, {NativeConfig} from 'react-native-config';
+import getPlatform from './libs/getPlatform';
import * as Url from './libs/Url';
import CONST from './CONST';
// react-native-config doesn't trim whitespace on iOS for some reason so we
-// add a trim() call to lodashGet here to prevent headaches
-const lodashGet = (config, key, defaultValue) => get(config, key, defaultValue).trim();
+// add a trim() call to prevent headaches
+const get = (config: NativeConfig, key: string, defaultValue: string): string => (config?.[key] ?? defaultValue).trim();
// Set default values to contributor friendly values to make development work out of the box without an .env file
-const ENVIRONMENT = lodashGet(Config, 'ENVIRONMENT', CONST.ENVIRONMENT.DEV);
-const newExpensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com/'));
-const expensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'EXPENSIFY_URL', 'https://www.expensify.com/'));
-const stagingExpensifyURL = Url.addTrailingForwardSlash(lodashGet(Config, 'STAGING_EXPENSIFY_URL', 'https://staging.expensify.com/'));
-const stagingSecureExpensifyUrl = Url.addTrailingForwardSlash(lodashGet(Config, 'STAGING_SECURE_EXPENSIFY_URL', 'https://staging-secure.expensify.com/'));
-const ngrokURL = Url.addTrailingForwardSlash(lodashGet(Config, 'NGROK_URL', ''));
-const secureNgrokURL = Url.addTrailingForwardSlash(lodashGet(Config, 'SECURE_NGROK_URL', ''));
-const secureExpensifyUrl = Url.addTrailingForwardSlash(lodashGet(Config, 'SECURE_EXPENSIFY_URL', 'https://secure.expensify.com/'));
-const useNgrok = lodashGet(Config, 'USE_NGROK', 'false') === 'true';
-const useWebProxy = lodashGet(Config, 'USE_WEB_PROXY', 'true') === 'true';
+const ENVIRONMENT = get(Config, 'ENVIRONMENT', CONST.ENVIRONMENT.DEV);
+const newExpensifyURL = Url.addTrailingForwardSlash(get(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com/'));
+const expensifyURL = Url.addTrailingForwardSlash(get(Config, 'EXPENSIFY_URL', 'https://www.expensify.com/'));
+const stagingExpensifyURL = Url.addTrailingForwardSlash(get(Config, 'STAGING_EXPENSIFY_URL', 'https://staging.expensify.com/'));
+const stagingSecureExpensifyUrl = Url.addTrailingForwardSlash(get(Config, 'STAGING_SECURE_EXPENSIFY_URL', 'https://staging-secure.expensify.com/'));
+const ngrokURL = Url.addTrailingForwardSlash(get(Config, 'NGROK_URL', ''));
+const secureNgrokURL = Url.addTrailingForwardSlash(get(Config, 'SECURE_NGROK_URL', ''));
+const secureExpensifyUrl = Url.addTrailingForwardSlash(get(Config, 'SECURE_EXPENSIFY_URL', 'https://secure.expensify.com/'));
+const useNgrok = get(Config, 'USE_NGROK', 'false') === 'true';
+const useWebProxy = get(Config, 'USE_WEB_PROXY', 'true') === 'true';
const expensifyComWithProxy = getPlatform() === 'web' && useWebProxy ? '/' : expensifyURL;
// Throw errors on dev if config variables are not set correctly
@@ -58,8 +57,8 @@ export default {
DEFAULT_SECURE_API_ROOT: secureURLRoot,
STAGING_API_ROOT: stagingExpensifyURL,
STAGING_SECURE_API_ROOT: stagingSecureExpensifyUrl,
- PARTNER_NAME: lodashGet(Config, 'EXPENSIFY_PARTNER_NAME', 'chat-expensify-com'),
- PARTNER_PASSWORD: lodashGet(Config, 'EXPENSIFY_PARTNER_PASSWORD', 'e21965746fd75f82bb66'),
+ PARTNER_NAME: get(Config, 'EXPENSIFY_PARTNER_NAME', 'chat-expensify-com'),
+ PARTNER_PASSWORD: get(Config, 'EXPENSIFY_PARTNER_PASSWORD', 'e21965746fd75f82bb66'),
EXPENSIFY_CASH_REFERER: 'ecash',
CONCIERGE_URL_PATHNAME: 'concierge/',
DEVPORTAL_URL_PATHNAME: '_devportal/',
@@ -69,8 +68,8 @@ export default {
IS_IN_STAGING: ENVIRONMENT === CONST.ENVIRONMENT.STAGING,
IS_USING_LOCAL_WEB: useNgrok || expensifyURLRoot.includes('dev'),
PUSHER: {
- APP_KEY: lodashGet(Config, 'PUSHER_APP_KEY', '268df511a204fbb60884'),
- SUFFIX: lodashGet(Config, 'PUSHER_DEV_SUFFIX', ''),
+ APP_KEY: get(Config, 'PUSHER_APP_KEY', '268df511a204fbb60884'),
+ SUFFIX: get(Config, 'PUSHER_DEV_SUFFIX', ''),
CLUSTER: 'mt1',
},
SITE_TITLE: 'New Expensify',
@@ -78,11 +77,11 @@ export default {
DEFAULT: '/favicon.png',
UNREAD: '/favicon-unread.png',
},
- CAPTURE_METRICS: lodashGet(Config, 'CAPTURE_METRICS', 'false') === 'true',
- ONYX_METRICS: lodashGet(Config, 'ONYX_METRICS', 'false') === 'true',
- DEV_PORT: process.env.PORT || 8082,
- E2E_TESTING: lodashGet(Config, 'E2E_TESTING', 'false') === 'true',
- SEND_CRASH_REPORTS: lodashGet(Config, 'SEND_CRASH_REPORTS', 'false') === 'true',
+ CAPTURE_METRICS: get(Config, 'CAPTURE_METRICS', 'false') === 'true',
+ ONYX_METRICS: get(Config, 'ONYX_METRICS', 'false') === 'true',
+ DEV_PORT: process.env.PORT ?? 8082,
+ E2E_TESTING: get(Config, 'E2E_TESTING', 'false') === 'true',
+ SEND_CRASH_REPORTS: get(Config, 'SEND_CRASH_REPORTS', 'false') === 'true',
IS_USING_WEB_PROXY: getPlatform() === 'web' && useWebProxy,
APPLE_SIGN_IN: {
SERVICE_ID: 'com.chat.expensify.chat.AppleSignIn',
@@ -92,4 +91,4 @@ export default {
WEB_CLIENT_ID: '921154746561-gpsoaqgqfuqrfsjdf8l7vohfkfj7b9up.apps.googleusercontent.com',
IOS_CLIENT_ID: '921154746561-s3uqn2oe4m85tufi6mqflbfbuajrm2i3.apps.googleusercontent.com',
},
-};
+} as const;
diff --git a/src/CONST.js b/src/CONST.ts
similarity index 96%
rename from src/CONST.js
rename to src/CONST.ts
index e86a9c4660da..864d078ab04b 100755
--- a/src/CONST.js
+++ b/src/CONST.ts
@@ -1,4 +1,4 @@
-import lodashGet from 'lodash/get';
+/* eslint-disable @typescript-eslint/naming-convention */
import Config from 'react-native-config';
import * as KeyCommand from 'react-native-key-command';
import * as Url from './libs/Url';
@@ -6,24 +6,24 @@ import SCREENS from './SCREENS';
const CLOUDFRONT_DOMAIN = 'cloudfront.net';
const CLOUDFRONT_URL = `https://d2k5nsl2zxldvw.${CLOUDFRONT_DOMAIN}`;
-const ACTIVE_EXPENSIFY_URL = Url.addTrailingForwardSlash(lodashGet(Config, 'NEW_EXPENSIFY_URL', 'https://new.expensify.com'));
+const ACTIVE_EXPENSIFY_URL = Url.addTrailingForwardSlash(Config?.NEW_EXPENSIFY_URL ?? 'https://new.expensify.com');
const USE_EXPENSIFY_URL = 'https://use.expensify.com';
const PLATFORM_OS_MACOS = 'Mac OS';
const PLATFORM_IOS = 'iOS';
const ANDROID_PACKAGE_NAME = 'com.expensify.chat';
const CURRENT_YEAR = new Date().getFullYear();
-const PULL_REQUEST_NUMBER = lodashGet(Config, 'PULL_REQUEST_NUMBER', '');
-
-const keyModifierControl = lodashGet(KeyCommand, 'constants.keyModifierControl', 'keyModifierControl');
-const keyModifierCommand = lodashGet(KeyCommand, 'constants.keyModifierCommand', 'keyModifierCommand');
-const keyModifierShiftControl = lodashGet(KeyCommand, 'constants.keyModifierShiftControl', 'keyModifierShiftControl');
-const keyModifierShiftCommand = lodashGet(KeyCommand, 'constants.keyModifierShiftCommand', 'keyModifierShiftCommand');
-const keyInputEscape = lodashGet(KeyCommand, 'constants.keyInputEscape', 'keyInputEscape');
-const keyInputEnter = lodashGet(KeyCommand, 'constants.keyInputEnter', 'keyInputEnter');
-const keyInputUpArrow = lodashGet(KeyCommand, 'constants.keyInputUpArrow', 'keyInputUpArrow');
-const keyInputDownArrow = lodashGet(KeyCommand, 'constants.keyInputDownArrow', 'keyInputDownArrow');
-const keyInputLeftArrow = lodashGet(KeyCommand, 'constants.keyInputLeftArrow', 'keyInputLeftArrow');
-const keyInputRightArrow = lodashGet(KeyCommand, 'constants.keyInputRightArrow', 'keyInputRightArrow');
+const PULL_REQUEST_NUMBER = Config?.PULL_REQUEST_NUMBER ?? '';
+
+const keyModifierControl = KeyCommand?.constants?.keyModifierControl ?? 'keyModifierControl';
+const keyModifierCommand = KeyCommand?.constants?.keyModifierCommand ?? 'keyModifierCommand';
+const keyModifierShiftControl = KeyCommand?.constants?.keyModifierShiftControl ?? 'keyModifierShiftControl';
+const keyModifierShiftCommand = KeyCommand?.constants?.keyModifierShiftCommand ?? 'keyModifierShiftCommand';
+const keyInputEscape = KeyCommand?.constants?.keyInputEscape ?? 'keyInputEscape';
+const keyInputEnter = KeyCommand?.constants?.keyInputEnter ?? 'keyInputEnter';
+const keyInputUpArrow = KeyCommand?.constants?.keyInputUpArrow ?? 'keyInputUpArrow';
+const keyInputDownArrow = KeyCommand?.constants?.keyInputDownArrow ?? 'keyInputDownArrow';
+const keyInputLeftArrow = KeyCommand?.constants?.keyInputLeftArrow ?? 'keyInputLeftArrow';
+const keyInputRightArrow = KeyCommand?.constants?.keyInputRightArrow ?? 'keyInputRightArrow';
// describes if a shortcut key can cause navigation
const KEYBOARD_SHORTCUT_NAVIGATION_TYPE = 'NAVIGATION_SHORTCUT';
@@ -818,8 +818,10 @@ const CONST = {
},
FILE_TYPE_REGEX: {
- IMAGE: /\.(jpg|jpeg|png|webp|avif|gif|tiff|wbmp|ico|jng|bmp|heic|svg|svg2)$/,
- VIDEO: /\.(3gp|h261|h263|h264|m4s|jpgv|jpm|jpgm|mp4|mp4v|mpg4|mpeg|mpg|ogv|ogg|mov|qt|webm|flv|mkv|wmv|wav|avi|movie|f4v|avchd|mp2|mpe|mpv|m4v|swf)$/,
+ // Image MimeTypes allowed by iOS photos app.
+ IMAGE: /\.(jpg|jpeg|png|webp|gif|tiff|bmp|heic|heif)$/,
+ // Video MimeTypes allowed by iOS photos app.
+ VIDEO: /\.(mov|mp4)$/,
},
IOS_CAMERAROLL_ACCESS_ERROR: 'Access to photo library was denied',
ADD_PAYMENT_MENU_POSITION_Y: 226,
@@ -882,22 +884,22 @@ const CONST = {
},
ACCOUNT_ID: {
- ACCOUNTING: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_ACCOUNTING', 9645353)),
- ADMIN: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_ADMIN', -1)),
- BILLS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_BILLS', 1371)),
- CHRONOS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_CHRONOS', 10027416)),
- CONCIERGE: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_CONCIERGE', 8392101)),
- CONTRIBUTORS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_CONTRIBUTORS', 9675014)),
- FIRST_RESPONDER: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_FIRST_RESPONDER', 9375152)),
- HELP: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_HELP', -1)),
- INTEGRATION_TESTING_CREDS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_INTEGRATION_TESTING_CREDS', -1)),
- PAYROLL: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_PAYROLL', 9679724)),
- QA: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_QA', 3126513)),
- QA_TRAVIS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_QA_TRAVIS', 8595733)),
- RECEIPTS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_RECEIPTS', -1)),
- REWARDS: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_REWARDS', 11023767)), // rewards@expensify.com
- STUDENT_AMBASSADOR: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_STUDENT_AMBASSADOR', 10476956)),
- SVFG: Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_SVFG', 2012843)),
+ ACCOUNTING: Number(Config?.EXPENSIFY_ACCOUNT_ID_ACCOUNTING ?? 9645353),
+ ADMIN: Number(Config?.EXPENSIFY_ACCOUNT_ID_ADMIN ?? -1),
+ BILLS: Number(Config?.EXPENSIFY_ACCOUNT_ID_BILLS ?? 1371),
+ CHRONOS: Number(Config?.EXPENSIFY_ACCOUNT_ID_CHRONOS ?? 10027416),
+ CONCIERGE: Number(Config?.EXPENSIFY_ACCOUNT_ID_CONCIERGE ?? 8392101),
+ CONTRIBUTORS: Number(Config?.EXPENSIFY_ACCOUNT_ID_CONTRIBUTORS ?? 9675014),
+ FIRST_RESPONDER: Number(Config?.EXPENSIFY_ACCOUNT_ID_FIRST_RESPONDER ?? 9375152),
+ HELP: Number(Config?.EXPENSIFY_ACCOUNT_ID_HELP ?? -1),
+ INTEGRATION_TESTING_CREDS: Number(Config?.EXPENSIFY_ACCOUNT_ID_INTEGRATION_TESTING_CREDS ?? -1),
+ PAYROLL: Number(Config?.EXPENSIFY_ACCOUNT_ID_PAYROLL ?? 9679724),
+ QA: Number(Config?.EXPENSIFY_ACCOUNT_ID_QA ?? 3126513),
+ QA_TRAVIS: Number(Config?.EXPENSIFY_ACCOUNT_ID_QA_TRAVIS ?? 8595733),
+ RECEIPTS: Number(Config?.EXPENSIFY_ACCOUNT_ID_RECEIPTS ?? -1),
+ REWARDS: Number(Config?.EXPENSIFY_ACCOUNT_ID_REWARDS ?? 11023767), // rewards@expensify.com
+ STUDENT_AMBASSADOR: Number(Config?.EXPENSIFY_ACCOUNT_ID_STUDENT_AMBASSADOR ?? 10476956),
+ SVFG: Number(Config?.EXPENSIFY_ACCOUNT_ID_SVFG ?? 2012843),
},
ENVIRONMENT: {
@@ -2597,10 +2599,11 @@ const CONST = {
NAVIGATE: 'NAVIGATE',
},
},
+
DEMO_PAGES: {
SAASTR: 'SaaStrDemoSetup',
SBE: 'SbeDemoSetup',
},
-};
+} as const;
export default CONST;
diff --git a/src/components/AmountTextInput.js b/src/components/AmountTextInput.js
index 8472ef271be0..98166cabd944 100644
--- a/src/components/AmountTextInput.js
+++ b/src/components/AmountTextInput.js
@@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
import TextInput from './TextInput';
import styles from '../styles/styles';
import CONST from '../CONST';
+import refPropTypes from './refPropTypes';
const propTypes = {
/** Formatted amount in local currency */
formattedAmount: PropTypes.string.isRequired,
/** A ref to forward to amount text input */
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+ forwardedRef: refPropTypes,
/** Function to call when amount in text input is changed */
onChangeAmount: PropTypes.func.isRequired,
diff --git a/src/components/Attachments/AttachmentCarousel/Pager/index.js b/src/components/Attachments/AttachmentCarousel/Pager/index.js
index 636a041cbb83..9779963dfc4a 100644
--- a/src/components/Attachments/AttachmentCarousel/Pager/index.js
+++ b/src/components/Attachments/AttachmentCarousel/Pager/index.js
@@ -8,6 +8,7 @@ import PagerView from 'react-native-pager-view';
import _ from 'underscore';
import styles from '../../../../styles/styles';
import AttachmentCarouselPagerContext from './AttachmentCarouselPagerContext';
+import refPropTypes from '../../../refPropTypes';
const AnimatedPagerView = Animated.createAnimatedComponent(createNativeWrapper(PagerView));
@@ -50,7 +51,7 @@ const pagerPropTypes = {
onSwipeSuccess: PropTypes.func,
onSwipeDown: PropTypes.func,
onPinchGestureChange: PropTypes.func,
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]),
+ forwardedRef: refPropTypes,
containerWidth: PropTypes.number.isRequired,
containerHeight: PropTypes.number.isRequired,
};
diff --git a/src/components/Button/index.js b/src/components/Button/index.js
index a850a43d2fb0..bfde528a4750 100644
--- a/src/components/Button/index.js
+++ b/src/components/Button/index.js
@@ -15,6 +15,7 @@ import * as Expensicons from '../Icon/Expensicons';
import withNavigationFocus from '../withNavigationFocus';
import validateSubmitShortcut from './validateSubmitShortcut';
import PressableWithFeedback from '../Pressable/PressableWithFeedback';
+import refPropTypes from '../refPropTypes';
const propTypes = {
/** The text for the button label */
@@ -118,8 +119,7 @@ const propTypes = {
accessibilityLabel: PropTypes.string,
/** A ref to forward the button */
- // eslint-disable-next-line react/forbid-prop-types
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.object})]),
+ forwardedRef: refPropTypes,
};
const defaultProps = {
diff --git a/src/components/Checkbox.js b/src/components/Checkbox.js
index 86b6e05d5ed7..1bb5824f612a 100644
--- a/src/components/Checkbox.js
+++ b/src/components/Checkbox.js
@@ -9,6 +9,7 @@ import * as Expensicons from './Icon/Expensicons';
import * as StyleUtils from '../styles/StyleUtils';
import CONST from '../CONST';
import PressableWithFeedback from './Pressable/PressableWithFeedback';
+import refPropTypes from './refPropTypes';
const propTypes = {
/** Whether checkbox is checked */
@@ -45,7 +46,7 @@ const propTypes = {
caretSize: PropTypes.number,
/** A ref to forward to the Pressable */
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+ forwardedRef: refPropTypes,
/** An accessibility label for the checkbox */
accessibilityLabel: PropTypes.string.isRequired,
diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js
index dc9b5ba4ac67..cbd22cc39dfd 100755
--- a/src/components/Composer/index.js
+++ b/src/components/Composer/index.js
@@ -279,7 +279,14 @@ function Composer({
}
if (textInput.current !== event.target) {
- return;
+ // To make sure the composer does not capture paste events from other inputs, we check where the event originated
+ // If it did originate in another input, we return early to prevent the composer from handling the paste
+ const isTargetInput = event.target.nodeName === 'INPUT' || event.target.nodeName === 'TEXTAREA' || event.target.contentEditable === 'true';
+ if (isTargetInput) {
+ return;
+ }
+
+ textInput.current.focus();
}
event.preventDefault();
diff --git a/src/components/CountryPicker/index.js b/src/components/CountryPicker/index.js
index 6d1435dca796..684296d20b11 100644
--- a/src/components/CountryPicker/index.js
+++ b/src/components/CountryPicker/index.js
@@ -7,6 +7,7 @@ import MenuItemWithTopDescription from '../MenuItemWithTopDescription';
import useLocalize from '../../hooks/useLocalize';
import CountrySelectorModal from './CountrySelectorModal';
import FormHelpMessage from '../FormHelpMessage';
+import refPropTypes from '../refPropTypes';
const propTypes = {
/** Form Error description */
@@ -19,7 +20,7 @@ const propTypes = {
onInputChange: PropTypes.func,
/** A ref to forward to MenuItemWithTopDescription */
- forwardedRef: PropTypes.oneOfType([PropTypes.func, PropTypes.shape({current: PropTypes.instanceOf(React.Component)})]),
+ forwardedRef: refPropTypes,
};
const defaultProps = {
diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
index 643785ab09d1..bfe39459ed74 100644
--- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
+++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.js
@@ -60,7 +60,17 @@ function ImageRenderer(props) {
const route = ROUTES.getReportAttachmentRoute(report.reportID, source);
Navigation.navigate(route);
}}
- onLongPress={(event) => showContextMenuForReport(event, anchor, report.reportID, action, checkIfContextMenuActive, ReportUtils.isArchivedRoom(report))}
+ onLongPress={(event) =>
+ showContextMenuForReport(
+ // Imitate the web event for native renderers
+ {nativeEvent: {...(event.nativeEvent || {}), target: {tagName: 'IMG'}}},
+ anchor,
+ report.reportID,
+ action,
+ checkIfContextMenuActive,
+ ReportUtils.isArchivedRoom(report),
+ )
+ }
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
accessibilityLabel={props.translate('accessibilityHints.viewAttachment')}
>
diff --git a/src/components/ImageView/index.native.js b/src/components/ImageView/index.native.js
index bcfcaf78d5c6..b365f507a4fc 100644
--- a/src/components/ImageView/index.native.js
+++ b/src/components/ImageView/index.native.js
@@ -1,13 +1,13 @@
-import React, {PureComponent} from 'react';
+import React, {useState, useRef, useEffect} from 'react';
import PropTypes from 'prop-types';
import {View, PanResponder} from 'react-native';
import ImageZoom from 'react-native-image-pan-zoom';
import _ from 'underscore';
import styles from '../../styles/styles';
import variables from '../../styles/variables';
-import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions';
import FullscreenLoadingIndicator from '../FullscreenLoadingIndicator';
import Image from '../Image';
+import useWindowDimensions from '../../hooks/useWindowDimensions';
/**
* On the native layer, we use a image library to handle zoom functionality
@@ -25,59 +25,34 @@ const propTypes = {
/** Function for handle on press */
onPress: PropTypes.func,
- ...windowDimensionsPropTypes,
+ /** Additional styles to add to the component */
+ style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
};
const defaultProps = {
isAuthTokenRequired: false,
onPress: () => {},
+ style: {},
};
-class ImageView extends PureComponent {
- constructor(props) {
- super(props);
-
- this.state = {
- isLoading: true,
- imageWidth: 0,
- imageHeight: 0,
- interactionPromise: undefined,
- };
-
- // Use the default double click interval from the ImageZoom library
- // https://github.com/ascoders/react-native-image-zoom/blob/master/src/image-zoom/image-zoom.type.ts#L79
- this.doubleClickInterval = 175;
- this.imageZoomScale = 1;
- this.lastClickTime = 0;
- this.amountOfTouches = 0;
-
- // PanResponder used to capture how many touches are active on the attachment image
- this.panResponder = PanResponder.create({
- onStartShouldSetPanResponder: this.updatePanResponderTouches.bind(this),
- });
-
- this.configureImageZoom = this.configureImageZoom.bind(this);
- this.imageLoadingStart = this.imageLoadingStart.bind(this);
- }
+// Use the default double click interval from the ImageZoom library
+// https://github.com/ascoders/react-native-image-zoom/blob/master/src/image-zoom/image-zoom.type.ts#L79
+const DOUBLE_CLICK_INTERVAL = 175;
- componentDidUpdate(prevProps) {
- if (this.props.url === prevProps.url) {
- return;
- }
+function ImageView({isAuthTokenRequired, url, onScaleChanged, onPress, style}) {
+ const {windowWidth, windowHeight} = useWindowDimensions();
- this.imageLoadingStart();
+ const [isLoading, setIsLoading] = useState(true);
+ const [imageDimensions, setImageDimensions] = useState({
+ width: 0,
+ height: 0,
+ });
+ const [containerHeight, setContainerHeight] = useState(null);
- if (this.interactionPromise) {
- this.state.interactionPromise.cancel();
- }
- }
-
- componentWillUnmount() {
- if (!this.state.interactionPromise) {
- return;
- }
- this.state.interactionPromise.cancel();
- }
+ const imageZoomScale = useRef(1);
+ const lastClickTime = useRef(0);
+ const numberOfTouches = useRef(0);
+ const zoom = useRef(null);
/**
* Updates the amount of active touches on the PanResponder on our ImageZoom overlay View
@@ -86,14 +61,58 @@ class ImageView extends PureComponent {
* @param {GestureState} gestureState
* @returns {Boolean}
*/
- updatePanResponderTouches(e, gestureState) {
+ const updatePanResponderTouches = (e, gestureState) => {
if (_.isNumber(gestureState.numberActiveTouches)) {
- this.amountOfTouches = gestureState.numberActiveTouches;
+ numberOfTouches.current = gestureState.numberActiveTouches;
}
// We don't need to set the panResponder since all we care about is checking the gestureState, so return false
return false;
- }
+ };
+
+ // PanResponder used to capture how many touches are active on the attachment image
+ const panResponder = useRef(
+ PanResponder.create({
+ onStartShouldSetPanResponder: updatePanResponderTouches,
+ }),
+ ).current;
+
+ /**
+ * When the url changes and the image must load again,
+ * this resets the zoom to ensure the next image loads with the correct dimensions.
+ */
+ const resetImageZoom = () => {
+ if (imageZoomScale.current !== 1) {
+ imageZoomScale.current = 1;
+ }
+
+ if (zoom.current) {
+ zoom.current.centerOn({
+ x: 0,
+ y: 0,
+ scale: 1,
+ duration: 0,
+ });
+ }
+ };
+
+ const imageLoadingStart = () => {
+ if (isLoading) {
+ return;
+ }
+
+ resetImageZoom();
+ setImageDimensions({
+ width: 0,
+ height: 0,
+ });
+ setIsLoading(true);
+ };
+
+ useEffect(() => {
+ imageLoadingStart();
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- this effect only needs to run when the url changes
+ }, [url]);
/**
* The `ImageZoom` component requires image dimensions which
@@ -102,148 +121,126 @@ class ImageView extends PureComponent {
*
* @param {Object} nativeEvent
*/
- configureImageZoom({nativeEvent}) {
- let imageWidth = nativeEvent.width;
- let imageHeight = nativeEvent.height;
- const containerWidth = Math.round(this.props.windowWidth);
- const containerHeight = Math.round(this.state.containerHeight ? this.state.containerHeight : this.props.windowHeight);
+ const configureImageZoom = ({nativeEvent}) => {
+ let imageZoomWidth = nativeEvent.width;
+ let imageZoomHeight = nativeEvent.height;
+ const roundedContainerWidth = Math.round(windowWidth);
+ const roundedContainerHeight = Math.round(containerHeight || windowHeight);
- const aspectRatio = Math.min(containerHeight / imageHeight, containerWidth / imageWidth);
+ const aspectRatio = Math.min(roundedContainerHeight / imageZoomHeight, roundedContainerWidth / imageZoomWidth);
- imageHeight *= aspectRatio;
- imageWidth *= aspectRatio;
+ imageZoomHeight *= aspectRatio;
+ imageZoomWidth *= aspectRatio;
// Resize the image to max dimensions possible on the Native platforms to prevent crashes on Android. To keep the same behavior, apply to IOS as well.
const maxDimensionsScale = 11;
- imageWidth = Math.min(imageWidth, containerWidth * maxDimensionsScale);
- imageHeight = Math.min(imageHeight, containerHeight * maxDimensionsScale);
- this.setState({imageHeight, imageWidth, isLoading: false});
- }
+ imageZoomWidth = Math.min(imageZoomWidth, roundedContainerWidth * maxDimensionsScale);
+ imageZoomHeight = Math.min(imageZoomHeight, roundedContainerHeight * maxDimensionsScale);
- /**
- * When the url changes and the image must load again,
- * this resets the zoom to ensure the next image loads with the correct dimensions.
- */
- resetImageZoom() {
- if (this.imageZoomScale !== 1) {
- this.imageZoomScale = 1;
+ setImageDimensions({
+ height: imageZoomHeight,
+ width: imageZoomWidth,
+ });
+ setIsLoading(false);
+ };
+
+ const configurePanResponder = () => {
+ const currentTimestamp = new Date().getTime();
+ const isDoubleClick = currentTimestamp - lastClickTime.current <= DOUBLE_CLICK_INTERVAL;
+ lastClickTime.current = currentTimestamp;
+
+ // Let ImageZoom handle the event if the tap is more than one touchPoint or if we are zoomed in
+ if (numberOfTouches.current === 2 || imageZoomScale.current !== 1) {
+ return true;
}
- if (this.zoom) {
- this.zoom.centerOn({
+ // When we have a double click and the zoom scale is 1 then programmatically zoom the image
+ // but let the tap fall through to the parent so we can register a swipe down to dismiss
+ if (isDoubleClick) {
+ zoom.current.centerOn({
x: 0,
y: 0,
- scale: 1,
- duration: 0,
+ scale: 2,
+ duration: 100,
});
- }
- }
- imageLoadingStart() {
- if (this.state.isLoading) {
- return;
+ // onMove will be called after the zoom animation.
+ // So it's possible to zoom and swipe and stuck in between the images.
+ // Sending scale just when we actually trigger the animation makes this nearly impossible.
+ // you should be really fast to catch in between state updates.
+ // And this lucky case will be fixed by migration to UI thread only code
+ // with gesture handler and reanimated.
+ onScaleChanged(2);
}
- this.resetImageZoom();
- this.setState({imageHeight: 0, imageWidth: 0, isLoading: true});
- }
-
- render() {
- // Default windowHeight accounts for the modal header height
- const windowHeight = this.props.windowHeight - variables.contentHeaderHeight;
- const hasImageDimensions = this.state.imageWidth !== 0 && this.state.imageHeight !== 0;
- const shouldShowLoadingIndicator = this.state.isLoading || !hasImageDimensions;
-
- // Zoom view should be loaded only after measuring actual image dimensions, otherwise it causes blurred images on Android
- return (
- {
- const layout = event.nativeEvent.layout;
- this.setState({
- containerHeight: layout.height,
- });
- }}
- >
- {Boolean(this.state.containerHeight) && (
- (this.zoom = el)}
- onClick={() => this.props.onPress()}
- cropWidth={this.props.windowWidth}
- cropHeight={windowHeight}
- imageWidth={this.state.imageWidth}
- imageHeight={this.state.imageHeight}
- onStartShouldSetPanResponder={() => {
- const isDoubleClick = new Date().getTime() - this.lastClickTime <= this.doubleClickInterval;
- this.lastClickTime = new Date().getTime();
-
- // Let ImageZoom handle the event if the tap is more than one touchPoint or if we are zoomed in
- if (this.amountOfTouches === 2 || this.imageZoomScale !== 1) {
- return true;
- }
-
- // When we have a double click and the zoom scale is 1 then programmatically zoom the image
- // but let the tap fall through to the parent so we can register a swipe down to dismiss
- if (isDoubleClick) {
- this.zoom.centerOn({
- x: 0,
- y: 0,
- scale: 2,
- duration: 100,
- });
-
- // onMove will be called after the zoom animation.
- // So it's possible to zoom and swipe and stuck in between the images.
- // Sending scale just when we actually trigger the animation makes this nearly impossible.
- // you should be really fast to catch in between state updates.
- // And this lucky case will be fixed by migration to UI thread only code
- // with gesture handler and reanimated.
- this.props.onScaleChanged(2);
- }
-
- // We must be either swiping down or double tapping since we are at zoom scale 1
- return false;
- }}
- onMove={({scale}) => {
- this.props.onScaleChanged(scale);
- this.imageZoomScale = scale;
- }}
- >
-
- {/**
- Create an invisible view on top of the image so we can capture and set the amount of touches before
- the ImageZoom's PanResponder does. Children will be triggered first, so this needs to be inside the
- ImageZoom to work
- */}
-
-
- )}
- {shouldShowLoadingIndicator && }
-
- );
- }
+
+ // We must be either swiping down or double tapping since we are at zoom scale 1
+ return false;
+ };
+
+ // Default windowHeight accounts for the modal header height
+ const calculatedWindowHeight = windowHeight - variables.contentHeaderHeight;
+ const hasImageDimensions = imageDimensions.width !== 0 && imageDimensions.height !== 0;
+ const shouldShowLoadingIndicator = isLoading || !hasImageDimensions;
+
+ // Zoom view should be loaded only after measuring actual image dimensions, otherwise it causes blurred images on Android
+ return (
+ {
+ const layout = event.nativeEvent.layout;
+ setContainerHeight(layout.height);
+ }}
+ >
+ {Boolean(containerHeight) && (
+ {
+ onScaleChanged(scale);
+ imageZoomScale.current = scale;
+ }}
+ >
+
+ {/**
+ Create an invisible view on top of the image so we can capture and set the amount of touches before
+ the ImageZoom's PanResponder does. Children will be triggered first, so this needs to be inside the
+ ImageZoom to work
+ */}
+
+
+ )}
+ {shouldShowLoadingIndicator && }
+
+ );
}
ImageView.propTypes = propTypes;
ImageView.defaultProps = defaultProps;
+ImageView.displayName = 'ImageView';
-export default withWindowDimensions(ImageView);
+export default ImageView;
diff --git a/src/components/Modal/BaseModal.js b/src/components/Modal/BaseModal.js
index 79dd98d0e876..8de95d9b3b2a 100644
--- a/src/components/Modal/BaseModal.js
+++ b/src/components/Modal/BaseModal.js
@@ -1,15 +1,17 @@
-import React, {PureComponent} from 'react';
+import React, {forwardRef, useCallback, useEffect, useMemo} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import ReactNativeModal from 'react-native-modal';
-import {SafeAreaInsetsContext} from 'react-native-safe-area-context';
+import {useSafeAreaInsets} from 'react-native-safe-area-context';
import styles from '../../styles/styles';
+import * as Modal from '../../libs/actions/Modal';
import * as StyleUtils from '../../styles/StyleUtils';
import themeColors from '../../styles/themes/default';
import {propTypes as modalPropTypes, defaultProps as modalDefaultProps} from './modalPropTypes';
-import * as Modal from '../../libs/actions/Modal';
import getModalStyles from '../../styles/getModalStyles';
+import useWindowDimensions from '../../hooks/useWindowDimensions';
import variables from '../../styles/variables';
+import CONST from '../../CONST';
import ComposerFocusManager from '../../libs/ComposerFocusManager';
const propTypes = {
@@ -24,173 +26,191 @@ const defaultProps = {
forwardedRef: () => {},
};
-class BaseModal extends PureComponent {
- constructor(props) {
- super(props);
-
- this.hideModal = this.hideModal.bind(this);
- }
-
- componentDidMount() {
- if (!this.props.isVisible) {
- return;
- }
-
- Modal.willAlertModalBecomeVisible(true);
-
- // To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu
- Modal.setCloseModal(this.props.onClose);
- }
-
- componentDidUpdate(prevProps) {
- if (prevProps.isVisible === this.props.isVisible) {
- return;
- }
-
- Modal.willAlertModalBecomeVisible(this.props.isVisible);
- Modal.setCloseModal(this.props.isVisible ? this.props.onClose : null);
- }
-
- componentWillUnmount() {
- // Only trigger onClose and setModalVisibility if the modal is unmounting while visible.
- if (this.props.isVisible) {
- this.hideModal(true);
- Modal.willAlertModalBecomeVisible(false);
- }
-
- // To prevent closing any modal already unmounted when this modal still remains as visible state
- Modal.setCloseModal(null);
- }
+function BaseModal({
+ isVisible,
+ onClose,
+ shouldSetModalVisibility,
+ onModalHide,
+ type,
+ popoverAnchorPosition,
+ innerContainerStyle,
+ outerStyle,
+ onModalShow,
+ propagateSwipe,
+ fullscreen,
+ animationIn,
+ animationOut,
+ useNativeDriver,
+ hideModalContentWhileAnimating,
+ animationInTiming,
+ animationOutTiming,
+ statusBarTranslucent,
+ onLayout,
+ avoidKeyboard,
+ forwardedRef,
+ children,
+}) {
+ const {windowWidth, windowHeight, isSmallScreenWidth} = useWindowDimensions();
+
+ const safeAreaInsets = useSafeAreaInsets();
/**
* Hides modal
* @param {Boolean} [callHideCallback=true] Should we call the onModalHide callback
*/
- hideModal(callHideCallback = true) {
- if (this.props.shouldSetModalVisibility) {
- Modal.setModalVisibility(false);
- }
- if (callHideCallback) {
- this.props.onModalHide();
+ const hideModal = useCallback(
+ (callHideCallback = true) => {
+ if (shouldSetModalVisibility) {
+ Modal.setModalVisibility(false);
+ }
+ if (callHideCallback) {
+ onModalHide();
+ }
+ Modal.onModalDidClose();
+ if (!fullscreen) {
+ ComposerFocusManager.setReadyToFocus();
+ }
+ },
+ [shouldSetModalVisibility, onModalHide, fullscreen],
+ );
+
+ useEffect(() => {
+ Modal.willAlertModalBecomeVisible(isVisible);
+
+ // To handle closing any modal already visible when this modal is mounted, i.e. PopoverReportActionContextMenu
+ Modal.setCloseModal(isVisible ? onClose : null);
+ }, [isVisible, onClose]);
+
+ useEffect(
+ () => () => {
+ // Only trigger onClose and setModalVisibility if the modal is unmounting while visible.
+ if (isVisible) {
+ hideModal(true);
+ Modal.willAlertModalBecomeVisible(false);
+ }
+
+ // To prevent closing any modal already unmounted when this modal still remains as visible state
+ Modal.setCloseModal(null);
+ },
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ [],
+ );
+
+ const handleShowModal = () => {
+ if (shouldSetModalVisibility) {
+ Modal.setModalVisibility(true);
}
- Modal.onModalDidClose();
- if (!this.props.fullscreen) {
- ComposerFocusManager.setReadyToFocus();
+ onModalShow();
+ };
+
+ const handleBackdropPress = (e) => {
+ if (e && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) {
+ return;
}
- }
-
- render() {
- const {
- modalStyle,
- modalContainerStyle,
- swipeDirection,
- animationIn,
- animationOut,
- shouldAddTopSafeAreaMargin,
- shouldAddBottomSafeAreaMargin,
- shouldAddTopSafeAreaPadding,
- shouldAddBottomSafeAreaPadding,
- hideBackdrop,
- } = getModalStyles(
- this.props.type,
- {
- windowWidth: this.props.windowWidth,
- windowHeight: this.props.windowHeight,
- isSmallScreenWidth: this.props.isSmallScreenWidth,
- },
- this.props.popoverAnchorPosition,
- this.props.innerContainerStyle,
- this.props.outerStyle,
- );
- return (
- {
- if (e && e.key === 'Enter') {
- return;
- }
- this.props.onClose();
- }}
- // Note: Escape key on web/desktop will trigger onBackButtonPress callback
- // eslint-disable-next-line react/jsx-props-no-multi-spaces
- onBackButtonPress={this.props.onClose}
- onModalWillShow={() => {
- ComposerFocusManager.resetReadyToFocus();
- }}
- onModalShow={() => {
- if (this.props.shouldSetModalVisibility) {
- Modal.setModalVisibility(true);
- }
- this.props.onModalShow();
- }}
- propagateSwipe={this.props.propagateSwipe}
- onModalHide={this.hideModal}
- onDismiss={() => ComposerFocusManager.setReadyToFocus()}
- onSwipeComplete={this.props.onClose}
- swipeDirection={swipeDirection}
- isVisible={this.props.isVisible}
- backdropColor={themeColors.overlay}
- backdropOpacity={hideBackdrop ? 0 : variables.overlayOpacity}
- backdropTransitionOutTiming={0}
- hasBackdrop={this.props.fullscreen}
- coverScreen={this.props.fullscreen}
- style={modalStyle}
- deviceHeight={this.props.windowHeight}
- deviceWidth={this.props.windowWidth}
- animationIn={this.props.animationIn || animationIn}
- animationOut={this.props.animationOut || animationOut}
- useNativeDriver={this.props.useNativeDriver}
- hideModalContentWhileAnimating={this.props.hideModalContentWhileAnimating}
- animationInTiming={this.props.animationInTiming}
- animationOutTiming={this.props.animationOutTiming}
- statusBarTranslucent={this.props.statusBarTranslucent}
- onLayout={this.props.onLayout}
- avoidKeyboard={this.props.avoidKeyboard}
+ onClose();
+ };
+
+ const handleDismissModal = () => {
+ ComposerFocusManager.setReadyToFocus();
+ };
+
+ const {
+ modalStyle,
+ modalContainerStyle,
+ swipeDirection,
+ animationIn: modalStyleAnimationIn,
+ animationOut: modalStyleAnimationOut,
+ shouldAddTopSafeAreaMargin,
+ shouldAddBottomSafeAreaMargin,
+ shouldAddTopSafeAreaPadding,
+ shouldAddBottomSafeAreaPadding,
+ hideBackdrop,
+ } = useMemo(
+ () =>
+ getModalStyles(
+ type,
+ {
+ windowWidth,
+ windowHeight,
+ isSmallScreenWidth,
+ },
+ popoverAnchorPosition,
+ innerContainerStyle,
+ outerStyle,
+ ),
+ [innerContainerStyle, isSmallScreenWidth, outerStyle, popoverAnchorPosition, type, windowHeight, windowWidth],
+ );
+
+ const {
+ paddingTop: safeAreaPaddingTop,
+ paddingBottom: safeAreaPaddingBottom,
+ paddingLeft: safeAreaPaddingLeft,
+ paddingRight: safeAreaPaddingRight,
+ } = StyleUtils.getSafeAreaPadding(safeAreaInsets);
+
+ const modalPaddingStyles = StyleUtils.getModalPaddingStyles({
+ safeAreaPaddingTop,
+ safeAreaPaddingBottom,
+ safeAreaPaddingLeft,
+ safeAreaPaddingRight,
+ shouldAddBottomSafeAreaMargin,
+ shouldAddTopSafeAreaMargin,
+ shouldAddBottomSafeAreaPadding,
+ shouldAddTopSafeAreaPadding,
+ modalContainerStyleMarginTop: modalContainerStyle.marginTop,
+ modalContainerStyleMarginBottom: modalContainerStyle.marginBottom,
+ modalContainerStylePaddingTop: modalContainerStyle.paddingTop,
+ modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom,
+ insets: safeAreaInsets,
+ });
+
+ return (
+
+
-
- {(insets) => {
- const {
- paddingTop: safeAreaPaddingTop,
- paddingBottom: safeAreaPaddingBottom,
- paddingLeft: safeAreaPaddingLeft,
- paddingRight: safeAreaPaddingRight,
- } = StyleUtils.getSafeAreaPadding(insets);
-
- const modalPaddingStyles = StyleUtils.getModalPaddingStyles({
- safeAreaPaddingTop,
- safeAreaPaddingBottom,
- safeAreaPaddingLeft,
- safeAreaPaddingRight,
- shouldAddBottomSafeAreaMargin,
- shouldAddTopSafeAreaMargin,
- shouldAddBottomSafeAreaPadding,
- shouldAddTopSafeAreaPadding,
- modalContainerStyleMarginTop: modalContainerStyle.marginTop,
- modalContainerStyleMarginBottom: modalContainerStyle.marginBottom,
- modalContainerStylePaddingTop: modalContainerStyle.paddingTop,
- modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom,
- insets,
- });
-
- return (
-
- {this.props.children}
-
- );
- }}
-
-
- );
- }
+ {children}
+
+
+ );
}
BaseModal.propTypes = propTypes;
BaseModal.defaultProps = defaultProps;
+BaseModal.displayName = 'BaseModal';
-export default React.forwardRef((props, ref) => (
+export default forwardRef((props, ref) => (
{!showAllFields && (
-
+
@@ -105,7 +104,6 @@ function DisplayNamePage(props) {
accessibilityRole={CONST.ACCESSIBILITY_ROLE.TEXT}
defaultValue={lodashGet(currentUserDetails, 'lastName', '')}
maxLength={CONST.DISPLAY_NAME.MAX_LENGTH}
- autoCapitalize="words"
spellCheck={false}
/>
diff --git a/src/pages/settings/Profile/PronounsPage.js b/src/pages/settings/Profile/PronounsPage.js
index 50a231523834..ce460bc30ff4 100644
--- a/src/pages/settings/Profile/PronounsPage.js
+++ b/src/pages/settings/Profile/PronounsPage.js
@@ -1,6 +1,6 @@
import _ from 'underscore';
import lodashGet from 'lodash/get';
-import React, {useState, useEffect, useMemo, useCallback} from 'react';
+import React, {useState, useEffect, useCallback} from 'react';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../../components/withCurrentUserPersonalDetails';
import ScreenWrapper from '../../../components/ScreenWrapper';
import HeaderWithBackButton from '../../../components/HeaderWithBackButton';
@@ -78,13 +78,7 @@ function PronounsPage(props) {
* Pronouns list filtered by searchValue needed for the OptionsSelector.
* Empty array if the searchValue is empty.
*/
- const filteredPronounsList = useMemo(() => {
- const searchedValue = searchValue.trim();
- if (searchedValue.length === 0) {
- return [];
- }
- return _.filter(pronounsList, (pronous) => pronous.text.toLowerCase().indexOf(searchedValue.toLowerCase()) >= 0);
- }, [pronounsList, searchValue]);
+ const filteredPronounsList = _.filter(pronounsList, (pronous) => pronous.text.toLowerCase().indexOf(searchValue.trim().toLowerCase()) >= 0);
const headerMessage = searchValue.trim() && !filteredPronounsList.length ? props.translate('common.noResultsFound') : '';
@@ -104,16 +98,20 @@ function PronounsPage(props) {
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS_PROFILE)}
/>
{props.translate('pronounsPage.isShownOnProfile')}
-
+ {/* Only render pronouns if list was loaded (not filtered list), otherwise initially focused item will be empty */}
+ {pronounsList.length > 0 && (
+
+ )}
);
}
diff --git a/src/pages/settings/Profile/TimezoneSelectPage.js b/src/pages/settings/Profile/TimezoneSelectPage.js
index 7bdf1afd498a..d0a50acdeb17 100644
--- a/src/pages/settings/Profile/TimezoneSelectPage.js
+++ b/src/pages/settings/Profile/TimezoneSelectPage.js
@@ -78,6 +78,8 @@ function TimezoneSelectPage(props) {
onSelectRow={saveSelectedTimezone}
sections={[{data: timezoneOptions, indexOffset: 0, isDisabled: timezone.automatic}]}
initiallyFocusedOptionKey={_.get(_.filter(timezoneOptions, (tz) => tz.text === timezone.selected)[0], 'keyForList')}
+ shouldDelayFocus
+ showScrollIndicator
/>
);
diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js
index b22421167478..6db3a20a3e4a 100644
--- a/src/pages/workspace/WorkspaceInvitePage.js
+++ b/src/pages/workspace/WorkspaceInvitePage.js
@@ -225,7 +225,10 @@ function WorkspaceInvitePage(props) {
onChangeText={setSearchTerm}
headerMessage={headerMessage}
onSelectRow={toggleOption}
+ onConfirm={inviteUser}
showScrollIndicator
+ shouldDelayFocus
+ showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)}
/>
toggleAllUsers(data)}
onDismissError={dismissError}
showLoadingPlaceholder={!OptionsListUtils.isPersonalDetailsReady(props.personalDetails) || _.isEmpty(props.policyMembers)}
- initiallyFocusedOptionKey={lodashGet(
- _.find(data, (item) => !item.isDisabled),
- 'keyForList',
- undefined,
- )}
+ shouldDelayFocus
+ showScrollIndicator
/>
diff --git a/src/styles/optionAlternateTextPlatformStyles/index.ios.js b/src/styles/optionAlternateTextPlatformStyles/index.ios.js
deleted file mode 100644
index 0f506d6675a8..000000000000
--- a/src/styles/optionAlternateTextPlatformStyles/index.ios.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- paddingTop: 1,
-};
diff --git a/src/styles/optionAlternateTextPlatformStyles/index.ios.ts b/src/styles/optionAlternateTextPlatformStyles/index.ios.ts
new file mode 100644
index 000000000000..14b024757fb8
--- /dev/null
+++ b/src/styles/optionAlternateTextPlatformStyles/index.ios.ts
@@ -0,0 +1,7 @@
+import OptionAlternateTextPlatformStyles from './types';
+
+const optionAlternateTextPlatformStyles: OptionAlternateTextPlatformStyles = {
+ paddingTop: 1,
+};
+
+export default optionAlternateTextPlatformStyles;
diff --git a/src/styles/optionAlternateTextPlatformStyles/index.js b/src/styles/optionAlternateTextPlatformStyles/index.js
deleted file mode 100644
index ff8b4c56321a..000000000000
--- a/src/styles/optionAlternateTextPlatformStyles/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export default {};
diff --git a/src/styles/optionAlternateTextPlatformStyles/index.ts b/src/styles/optionAlternateTextPlatformStyles/index.ts
new file mode 100644
index 000000000000..e3232b810e06
--- /dev/null
+++ b/src/styles/optionAlternateTextPlatformStyles/index.ts
@@ -0,0 +1,5 @@
+import OptionAlternateTextPlatformStyles from './types';
+
+const optionAlternateTextPlatformStyles: OptionAlternateTextPlatformStyles = {};
+
+export default optionAlternateTextPlatformStyles;
diff --git a/src/styles/optionAlternateTextPlatformStyles/types.ts b/src/styles/optionAlternateTextPlatformStyles/types.ts
new file mode 100644
index 000000000000..b2e8e4745fff
--- /dev/null
+++ b/src/styles/optionAlternateTextPlatformStyles/types.ts
@@ -0,0 +1,5 @@
+import {TextStyle} from 'react-native';
+
+type OptionAlternateTextPlatformStyles = Partial>;
+
+export default OptionAlternateTextPlatformStyles;
diff --git a/src/styles/optionRowStyles/index.native.js b/src/styles/optionRowStyles/index.native.ts
similarity index 76%
rename from src/styles/optionRowStyles/index.native.js
rename to src/styles/optionRowStyles/index.native.ts
index b95382777f7e..11371509ce73 100644
--- a/src/styles/optionRowStyles/index.native.js
+++ b/src/styles/optionRowStyles/index.native.ts
@@ -1,3 +1,4 @@
+import OptionRowStyles from './types';
import styles from '../styles';
/**
@@ -7,7 +8,7 @@ import styles from '../styles';
* https://github.com/Expensify/App/issues/14148
*/
-const compactContentContainerStyles = styles.alignItemsCenter;
+const compactContentContainerStyles: OptionRowStyles = styles.alignItemsCenter;
export {
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/styles/optionRowStyles/index.js b/src/styles/optionRowStyles/index.ts
similarity index 53%
rename from src/styles/optionRowStyles/index.js
rename to src/styles/optionRowStyles/index.ts
index 2bef2a0cd094..fbeca3c702d9 100644
--- a/src/styles/optionRowStyles/index.js
+++ b/src/styles/optionRowStyles/index.ts
@@ -1,6 +1,7 @@
+import OptionRowStyles from './types';
import styles from '../styles';
-const compactContentContainerStyles = styles.alignItemsBaseline;
+const compactContentContainerStyles: OptionRowStyles = styles.alignItemsBaseline;
export {
// eslint-disable-next-line import/prefer-default-export
diff --git a/src/styles/optionRowStyles/types.ts b/src/styles/optionRowStyles/types.ts
new file mode 100644
index 000000000000..c08174470701
--- /dev/null
+++ b/src/styles/optionRowStyles/types.ts
@@ -0,0 +1,6 @@
+import {CSSProperties} from 'react';
+import {ViewStyle} from 'react-native';
+
+type OptionRowStyles = CSSProperties | ViewStyle;
+
+export default OptionRowStyles;
diff --git a/src/styles/utilities/overflowAuto/index.js b/src/styles/utilities/overflowAuto/index.js
deleted file mode 100644
index 358f781077d7..000000000000
--- a/src/styles/utilities/overflowAuto/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-export default {
- overflow: 'auto',
-};
diff --git a/src/styles/utilities/overflowAuto/index.native.js b/src/styles/utilities/overflowAuto/index.native.js
deleted file mode 100644
index 2075ce53e3e1..000000000000
--- a/src/styles/utilities/overflowAuto/index.native.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// Overflow auto doesn't exist in react-native so we'll default to overflow: visible
-export default {
- overflow: 'visible',
-};
diff --git a/src/styles/utilities/overflowAuto/index.native.ts b/src/styles/utilities/overflowAuto/index.native.ts
new file mode 100644
index 000000000000..34ee18db1d0a
--- /dev/null
+++ b/src/styles/utilities/overflowAuto/index.native.ts
@@ -0,0 +1,8 @@
+import OverflowAutoStyles from './types';
+
+// Overflow auto doesn't exist in react-native so we'll default to overflow: visible
+const overflowAuto: OverflowAutoStyles = {
+ overflow: 'visible',
+};
+
+export default overflowAuto;
diff --git a/src/styles/utilities/overflowAuto/index.ts b/src/styles/utilities/overflowAuto/index.ts
new file mode 100644
index 000000000000..0eb19068738f
--- /dev/null
+++ b/src/styles/utilities/overflowAuto/index.ts
@@ -0,0 +1,7 @@
+import OverflowAutoStyles from './types';
+
+const overflowAuto: OverflowAutoStyles = {
+ overflow: 'auto',
+};
+
+export default overflowAuto;
diff --git a/src/styles/utilities/overflowAuto/types.ts b/src/styles/utilities/overflowAuto/types.ts
new file mode 100644
index 000000000000..faba7c2cbdb8
--- /dev/null
+++ b/src/styles/utilities/overflowAuto/types.ts
@@ -0,0 +1,5 @@
+import {CSSProperties} from 'react';
+
+type OverflowAutoStyles = Pick;
+
+export default OverflowAutoStyles;
diff --git a/src/styles/utilities/userSelect/index.native.js b/src/styles/utilities/userSelect/index.native.ts
similarity index 52%
rename from src/styles/utilities/userSelect/index.native.js
rename to src/styles/utilities/userSelect/index.native.ts
index 0d1dfe97aa1d..0d1a34ef2473 100644
--- a/src/styles/utilities/userSelect/index.native.js
+++ b/src/styles/utilities/userSelect/index.native.ts
@@ -1,4 +1,6 @@
-export default {
+import UserSelectStyles from './types';
+
+const userSelect: UserSelectStyles = {
userSelectText: {
userSelect: 'text',
},
@@ -6,3 +8,5 @@ export default {
userSelect: 'none',
},
};
+
+export default userSelect;
diff --git a/src/styles/utilities/userSelect/index.js b/src/styles/utilities/userSelect/index.ts
similarity index 63%
rename from src/styles/utilities/userSelect/index.js
rename to src/styles/utilities/userSelect/index.ts
index c6b71170cf14..6b9f26131b5e 100644
--- a/src/styles/utilities/userSelect/index.js
+++ b/src/styles/utilities/userSelect/index.ts
@@ -1,4 +1,6 @@
-export default {
+import UserSelectStyles from './types';
+
+const userSelect: UserSelectStyles = {
userSelectText: {
userSelect: 'text',
WebkitUserSelect: 'text',
@@ -8,3 +10,5 @@ export default {
WebkitUserSelect: 'none',
},
};
+
+export default userSelect;
diff --git a/src/styles/utilities/userSelect/types.ts b/src/styles/utilities/userSelect/types.ts
new file mode 100644
index 000000000000..67a8c9c7b9b6
--- /dev/null
+++ b/src/styles/utilities/userSelect/types.ts
@@ -0,0 +1,5 @@
+import {CSSProperties} from 'react';
+
+type UserSelectStyles = Record<'userSelectText' | 'userSelectNone', Partial>>;
+
+export default UserSelectStyles;
diff --git a/src/global.d.ts b/src/types/global.d.ts
similarity index 60%
rename from src/global.d.ts
rename to src/types/global.d.ts
index 0dc745d5e0ea..1910b5a994b8 100644
--- a/src/global.d.ts
+++ b/src/types/global.d.ts
@@ -1,13 +1,13 @@
-import {OnyxKey, OnyxCollectionKey, OnyxValues} from './ONYXKEYS';
-
declare module '*.png' {
const value: import('react-native').ImageSourcePropType;
export default value;
}
+
declare module '*.jpg' {
const value: import('react-native').ImageSourcePropType;
export default value;
}
+
declare module '*.svg' {
import React from 'react';
import {SvgProps} from 'react-native-svg';
@@ -17,12 +17,3 @@ declare module '*.svg' {
}
declare module 'react-native-device-info/jest/react-native-device-info-mock';
-
-declare module 'react-native-onyx' {
- // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
- interface CustomTypeOptions {
- keys: OnyxKey;
- collectionKeys: OnyxCollectionKey;
- values: OnyxValues;
- }
-}
diff --git a/src/types/modules/react-native-key-command.d.ts b/src/types/modules/react-native-key-command.d.ts
new file mode 100644
index 000000000000..f93204891e84
--- /dev/null
+++ b/src/types/modules/react-native-key-command.d.ts
@@ -0,0 +1,30 @@
+declare module 'react-native-key-command' {
+ declare const constants = {
+ keyInputDownArrow: 'keyInputDownArrow',
+ keyInputEscape: 'keyInputEscape',
+ keyInputLeftArrow: 'keyInputLeftArrow',
+ keyInputRightArrow: 'keyInputRightArrow',
+ keyInputUpArrow: 'keyInputUpArrow',
+ keyInputEnter: 'keyInputEnter',
+ keyModifierCapsLock: 'keyModifierCapsLock',
+ keyModifierCommand: 'keyModifierCommand',
+ keyModifierControl: 'keyModifierControl',
+ keyModifierControlCommand: 'keyModifierControlCommand',
+ keyModifierControlOption: 'keyModifierControlOption',
+ keyModifierControlOptionCommand: 'keyModifierControlOptionCommand',
+ keyModifierNumericPad: 'keyModifierNumericPad',
+ keyModifierOption: 'keyModifierOption',
+ keyModifierOptionCommand: 'keyModifierOptionCommand',
+ keyModifierShift: 'keyModifierShift',
+ keyModifierShiftCommand: 'keyModifierShiftCommand',
+ keyModifierShiftControl: 'keyModifierShiftControl',
+ keyModifierAlternate: 'keyModifierAlternate',
+ } as const;
+
+ type KeyCommand = {input: string; modifierFlags: string};
+
+ declare function addListener(keyCommand: KeyCommand, callback: (keycommandEvent: KeyCommand, event: Event) => void): () => void;
+
+ // eslint-disable-next-line import/prefer-default-export
+ export {constants, addListener};
+}
diff --git a/src/types/modules/react-native-onyx.d.ts b/src/types/modules/react-native-onyx.d.ts
new file mode 100644
index 000000000000..4979f8fd0dbb
--- /dev/null
+++ b/src/types/modules/react-native-onyx.d.ts
@@ -0,0 +1,10 @@
+import {OnyxKey, OnyxCollectionKey, OnyxValues} from '../../ONYXKEYS';
+
+declare module 'react-native-onyx' {
+ // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
+ interface CustomTypeOptions {
+ keys: OnyxKey;
+ collectionKeys: OnyxCollectionKey;
+ values: OnyxValues;
+ }
+}
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index d52489c36da9..8ed25cb286b0 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -1,5 +1,3 @@
-// TODO: Remove this after CONST.ts is migrated to TS
-/* eslint-disable @typescript-eslint/no-duplicate-type-constituents */
import {ValueOf} from 'type-fest';
import CONST from '../../CONST';
diff --git a/src/types/onyx/WalletTransfer.ts b/src/types/onyx/WalletTransfer.ts
index c5d92250f91d..3dd28729ba96 100644
--- a/src/types/onyx/WalletTransfer.ts
+++ b/src/types/onyx/WalletTransfer.ts
@@ -9,8 +9,6 @@ type WalletTransfer = {
selectedAccountType?: string;
/** Type to filter the payment Method list */
- // TODO: Remove this after CONST.ts is migrated to TS
- // eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
filterPaymentMethodType?: typeof CONST.PAYMENT_METHODS.DEBIT_CARD | typeof CONST.PAYMENT_METHODS.BANK_ACCOUNT;
/** Whether the success screen is shown to user. */