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 && ( - +