diff --git a/android/app/build.gradle b/android/app/build.gradle index 806fd8d3efda..a53c6dfb62af 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043505 - versionName "1.4.35-5" + versionCode 1001043600 + versionName "1.4.36-0" } flavorDimensions "default" diff --git a/contributingGuides/APPLE_GOOGLE_SIGNIN.md b/contributingGuides/APPLE_GOOGLE_SIGNIN.md index 43485a28b353..4bb86e31b486 100644 --- a/contributingGuides/APPLE_GOOGLE_SIGNIN.md +++ b/contributingGuides/APPLE_GOOGLE_SIGNIN.md @@ -259,12 +259,33 @@ if (CONFIG.ENVIRONMENT === CONST.ENVIRONMENT.DEV) { } ``` -#### Port requirements +#### Host/Port requirements Google allows the web app to be hosted at localhost, but according to the current Google console configuration for the Expensify client ID, it must be hosted on port 8082. +Also note that you'll need to update the webpack.dev.js config to change `host` from `dev.new.expensify.com` to `localhost` and server type from `https` to `http`. The reason for this is that Google Sign In allows localhost, but `dev.new.expensify.com` is not a registered Google Sign In domain. + +```diff +diff --git a/config/webpack/webpack.dev.js b/config/webpack/webpack.dev.js +index e28383eff5..b14f6f34aa 100644 +--- a/config/webpack/webpack.dev.js ++++ b/config/webpack/webpack.dev.js +@@ -44,9 +44,9 @@ module.exports = (env = {}) => + ...proxySettings, + historyApiFallback: true, + port, +- host: 'dev.new.expensify.com', ++ host: 'localhost', + server: { +- type: 'https', ++ type: 'http', + options: { + key: path.join(__dirname, 'key.pem'), + cert: path.join(__dirname, 'certificate.pem'), +``` + ### Desktop #### Set Environment to something other than "Development" diff --git a/docs/articles/expensify-classic/getting-started/Security.md b/docs/articles/expensify-classic/getting-started/Security.md deleted file mode 100644 index 5a0036e3e161..000000000000 --- a/docs/articles/expensify-classic/getting-started/Security.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Security -description: Security ---- -## Resource Coming Soon! diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 5dc09b4564bb..6a6a54e58c49 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.35 + 1.4.36 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.35.5 + 1.4.36.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 7dcdc2302ec4..763896fd45f1 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.35 + 1.4.36 CFBundleSignature ???? CFBundleVersion - 1.4.35.5 + 1.4.36.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index fca74ce93fc0..a4bef0847c95 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -11,9 +11,9 @@ CFBundleName $(PRODUCT_NAME) CFBundleShortVersionString - 1.4.35 + 1.4.36 CFBundleVersion - 1.4.35.5 + 1.4.36.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 57203662f350..674325508da5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.35-5", + "version": "1.4.36-0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.35-5", + "version": "1.4.36-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index a6971f881fd3..0c1105f978c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.35-5", + "version": "1.4.36-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/patches/react-native-web+0.19.9+005+image-header-support.patch b/patches/react-native-web+0.19.9+005+image-header-support.patch deleted file mode 100644 index 4652e22662f0..000000000000 --- a/patches/react-native-web+0.19.9+005+image-header-support.patch +++ /dev/null @@ -1,200 +0,0 @@ -diff --git a/node_modules/react-native-web/dist/exports/Image/index.js b/node_modules/react-native-web/dist/exports/Image/index.js -index 95355d5..19109fc 100644 ---- a/node_modules/react-native-web/dist/exports/Image/index.js -+++ b/node_modules/react-native-web/dist/exports/Image/index.js -@@ -135,7 +135,22 @@ function resolveAssetUri(source) { - } - return uri; - } --var Image = /*#__PURE__*/React.forwardRef((props, ref) => { -+function raiseOnErrorEvent(uri, _ref) { -+ var onError = _ref.onError, -+ onLoadEnd = _ref.onLoadEnd; -+ if (onError) { -+ onError({ -+ nativeEvent: { -+ error: "Failed to load resource " + uri + " (404)" -+ } -+ }); -+ } -+ if (onLoadEnd) onLoadEnd(); -+} -+function hasSourceDiff(a, b) { -+ return a.uri !== b.uri || JSON.stringify(a.headers) !== JSON.stringify(b.headers); -+} -+var BaseImage = /*#__PURE__*/React.forwardRef((props, ref) => { - var ariaLabel = props['aria-label'], - blurRadius = props.blurRadius, - defaultSource = props.defaultSource, -@@ -236,16 +251,10 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { - } - }, function error() { - updateState(ERRORED); -- if (onError) { -- onError({ -- nativeEvent: { -- error: "Failed to load resource " + uri + " (404)" -- } -- }); -- } -- if (onLoadEnd) { -- onLoadEnd(); -- } -+ raiseOnErrorEvent(uri, { -+ onError, -+ onLoadEnd -+ }); - }); - } - function abortPendingRequest() { -@@ -277,10 +286,78 @@ var Image = /*#__PURE__*/React.forwardRef((props, ref) => { - suppressHydrationWarning: true - }), hiddenImage, createTintColorSVG(tintColor, filterRef.current)); - }); --Image.displayName = 'Image'; -+BaseImage.displayName = 'Image'; -+ -+/** -+ * This component handles specifically loading an image source with headers -+ * default source is never loaded using headers -+ */ -+var ImageWithHeaders = /*#__PURE__*/React.forwardRef((props, ref) => { -+ // $FlowIgnore: This component would only be rendered when `source` matches `ImageSource` -+ var nextSource = props.source; -+ var _React$useState3 = React.useState(''), -+ blobUri = _React$useState3[0], -+ setBlobUri = _React$useState3[1]; -+ var request = React.useRef({ -+ cancel: () => {}, -+ source: { -+ uri: '', -+ headers: {} -+ }, -+ promise: Promise.resolve('') -+ }); -+ var onError = props.onError, -+ onLoadStart = props.onLoadStart, -+ onLoadEnd = props.onLoadEnd; -+ React.useEffect(() => { -+ if (!hasSourceDiff(nextSource, request.current.source)) { -+ return; -+ } -+ -+ // When source changes we want to clean up any old/running requests -+ request.current.cancel(); -+ if (onLoadStart) { -+ onLoadStart(); -+ } -+ -+ // Store a ref for the current load request so we know what's the last loaded source, -+ // and so we can cancel it if a different source is passed through props -+ request.current = ImageLoader.loadWithHeaders(nextSource); -+ request.current.promise.then(uri => setBlobUri(uri)).catch(() => raiseOnErrorEvent(request.current.source.uri, { -+ onError, -+ onLoadEnd -+ })); -+ }, [nextSource, onLoadStart, onError, onLoadEnd]); -+ -+ // Cancel any request on unmount -+ React.useEffect(() => request.current.cancel, []); -+ var propsToPass = _objectSpread(_objectSpread({}, props), {}, { -+ // `onLoadStart` is called from the current component -+ // We skip passing it down to prevent BaseImage raising it a 2nd time -+ onLoadStart: undefined, -+ // Until the current component resolves the request (using headers) -+ // we skip forwarding the source so the base component doesn't attempt -+ // to load the original source -+ source: blobUri ? _objectSpread(_objectSpread({}, nextSource), {}, { -+ uri: blobUri -+ }) : undefined -+ }); -+ return /*#__PURE__*/React.createElement(BaseImage, _extends({ -+ ref: ref -+ }, propsToPass)); -+}); - - // $FlowIgnore: This is the correct type, but casting makes it unhappy since the variables aren't defined yet --var ImageWithStatics = Image; -+var ImageWithStatics = /*#__PURE__*/React.forwardRef((props, ref) => { -+ if (props.source && props.source.headers) { -+ return /*#__PURE__*/React.createElement(ImageWithHeaders, _extends({ -+ ref: ref -+ }, props)); -+ } -+ return /*#__PURE__*/React.createElement(BaseImage, _extends({ -+ ref: ref -+ }, props)); -+}); - ImageWithStatics.getSize = function (uri, success, failure) { - ImageLoader.getSize(uri, success, failure); - }; -diff --git a/node_modules/react-native-web/dist/modules/ImageLoader/index.js b/node_modules/react-native-web/dist/modules/ImageLoader/index.js -index bc06a87..e309394 100644 ---- a/node_modules/react-native-web/dist/modules/ImageLoader/index.js -+++ b/node_modules/react-native-web/dist/modules/ImageLoader/index.js -@@ -76,7 +76,7 @@ var ImageLoader = { - var image = requests["" + requestId]; - if (image) { - var naturalHeight = image.naturalHeight, -- naturalWidth = image.naturalWidth; -+ naturalWidth = image.naturalWidth; - if (naturalHeight && naturalWidth) { - success(naturalWidth, naturalHeight); - complete = true; -@@ -102,11 +102,19 @@ var ImageLoader = { - id += 1; - var image = new window.Image(); - image.onerror = onError; -- image.onload = e => { -+ image.onload = nativeEvent => { - // avoid blocking the main thread -- var onDecode = () => onLoad({ -- nativeEvent: e -- }); -+ var onDecode = () => { -+ // Append `source` to match RN's ImageLoadEvent interface -+ nativeEvent.source = { -+ uri: image.src, -+ width: image.naturalWidth, -+ height: image.naturalHeight -+ }; -+ onLoad({ -+ nativeEvent -+ }); -+ }; - if (typeof image.decode === 'function') { - // Safari currently throws exceptions when decoding svgs. - // We want to catch that error and allow the load handler -@@ -120,6 +128,32 @@ var ImageLoader = { - requests["" + id] = image; - return id; - }, -+ loadWithHeaders(source) { -+ var uri; -+ var abortController = new AbortController(); -+ var request = new Request(source.uri, { -+ headers: source.headers, -+ signal: abortController.signal -+ }); -+ request.headers.append('accept', 'image/*'); -+ var promise = fetch(request).then(response => response.blob()).then(blob => { -+ uri = URL.createObjectURL(blob); -+ return uri; -+ }).catch(error => { -+ if (error.name === 'AbortError') { -+ return ''; -+ } -+ throw error; -+ }); -+ return { -+ promise, -+ source, -+ cancel: () => { -+ abortController.abort(); -+ URL.revokeObjectURL(uri); -+ } -+ }; -+ }, - prefetch(uri) { - return new Promise((resolve, reject) => { - ImageLoader.load(uri, () => { diff --git a/src/CONST.ts b/src/CONST.ts index 69933a623bed..b27923465a1f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -46,6 +46,7 @@ const CONST = { IN: 'in', OUT: 'out', }, + BACKGROUND_IMAGE_TRANSITION_DURATION: 1000, ARROW_HIDE_DELAY: 3000, API_ATTACHMENT_VALIDATIONS: { diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index eac526a4fe28..7c83d34ccecc 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -493,8 +493,8 @@ type OnyxValues = { [ONYXKEYS.FORMS.WORKSPACE_SETTINGS_FORM_DRAFT]: OnyxTypes.WorkspaceSettingsForm; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM_DRAFT]: OnyxTypes.Form; - [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: OnyxTypes.Form; - [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM_DRAFT]: OnyxTypes.Form; + [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: OnyxTypes.CloseAccountForm; + [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM_DRAFT]: OnyxTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: OnyxTypes.Form; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM_DRAFT]: OnyxTypes.Form; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: OnyxTypes.DisplayNameForm; diff --git a/src/components/Image/BaseImage.native.tsx b/src/components/Image/BaseImage.native.tsx deleted file mode 100644 index c517efd04515..000000000000 --- a/src/components/Image/BaseImage.native.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import {Image as ExpoImage} from 'expo-image'; -import type {ImageProps as ExpoImageProps, ImageLoadEventData} from 'expo-image'; -import {useCallback} from 'react'; -import type {BaseImageProps} from './types'; - -function BaseImage({onLoad, ...props}: ExpoImageProps & BaseImageProps) { - const imageLoadedSuccessfully = useCallback( - (event: ImageLoadEventData) => { - if (!onLoad) { - return; - } - - // We override `onLoad`, so both web and native have the same signature - const {width, height} = event.source; - onLoad({nativeEvent: {width, height}}); - }, - [onLoad], - ); - - return ( - - ); -} - -BaseImage.displayName = 'BaseImage'; - -export default BaseImage; diff --git a/src/components/Image/BaseImage.tsx b/src/components/Image/BaseImage.tsx deleted file mode 100644 index ebdd76840267..000000000000 --- a/src/components/Image/BaseImage.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, {useCallback} from 'react'; -import {Image as RNImage} from 'react-native'; -import type {ImageLoadEventData, ImageProps as WebImageProps} from 'react-native'; -import type {BaseImageProps} from './types'; - -function BaseImage({onLoad, ...props}: WebImageProps & BaseImageProps) { - const imageLoadedSuccessfully = useCallback( - (event: {nativeEvent: ImageLoadEventData}) => { - if (!onLoad) { - return; - } - - // We override `onLoad`, so both web and native have the same signature - const {width, height} = event.nativeEvent.source; - onLoad({nativeEvent: {width, height}}); - }, - [onLoad], - ); - - return ( - - ); -} - -BaseImage.displayName = 'BaseImage'; - -export default BaseImage; diff --git a/src/components/Image/index.js b/src/components/Image/index.js index 8cee1cf95e14..ef1a69e19c12 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -1,35 +1,51 @@ import lodashGet from 'lodash/get'; -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; +import {Image as RNImage} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import CONST from '@src/CONST'; +import _ from 'underscore'; import ONYXKEYS from '@src/ONYXKEYS'; -import BaseImage from './BaseImage'; import {defaultProps, imagePropTypes} from './imagePropTypes'; import RESIZE_MODES from './resizeModes'; -function Image({source: propsSource, isAuthTokenRequired, session, ...forwardedProps}) { - // Update the source to include the auth token if required +function Image(props) { + const {source: propsSource, isAuthTokenRequired, onLoad, session} = props; + /** + * Check if the image source is a URL - if so the `encryptedAuthToken` is appended + * to the source. + */ const source = useMemo(() => { - if (typeof lodashGet(propsSource, 'uri') === 'number') { - return propsSource.uri; + if (isAuthTokenRequired) { + // There is currently a `react-native-web` bug preventing the authToken being passed + // in the headers of the image request so the authToken is added as a query param. + // On native the authToken IS passed in the image request headers + const authToken = lodashGet(session, 'encryptedAuthToken', null); + return {uri: `${propsSource.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`}; } - if (typeof propsSource !== 'number' && isAuthTokenRequired) { - const authToken = lodashGet(session, 'encryptedAuthToken'); - return { - ...propsSource, - headers: { - [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken, - }, - }; - } - return propsSource; // The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034. // eslint-disable-next-line react-hooks/exhaustive-deps }, [propsSource, isAuthTokenRequired]); + /** + * The natural image dimensions are retrieved using the updated source + * and as a result the `onLoad` event needs to be manually invoked to return these dimensions + */ + useEffect(() => { + // If an onLoad callback was specified then manually call it and pass + // the natural image dimensions to match the native API + if (onLoad == null) { + return; + } + RNImage.getSize(source.uri, (width, height) => { + onLoad({nativeEvent: {width, height}}); + }); + }, [onLoad, source]); + + // Omit the props which the underlying RNImage won't use + const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']); + return ( - { + const {width, height, url} = evt.source; + dimensionsCache.set(url, {width, height}); + if (props.onLoad) { + props.onLoad({nativeEvent: {width, height}}); + } + }} + /> + ); +} + +Image.propTypes = imagePropTypes; +Image.defaultProps = defaultProps; +Image.displayName = 'Image'; +const ImageWithOnyx = withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(Image); +ImageWithOnyx.resizeMode = RESIZE_MODES; +ImageWithOnyx.resolveDimensions = resolveDimensions; + +export default ImageWithOnyx; diff --git a/src/components/Image/types.ts b/src/components/Image/types.ts deleted file mode 100644 index 5a4c94364a46..000000000000 --- a/src/components/Image/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -type BaseImageProps = { - /** Event called with image dimensions when image is loaded */ - onLoad?: (event: {nativeEvent: {width: number; height: number}}) => void; -}; - -export type {BaseImageProps}; - -export default BaseImageProps; diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 7313bb4aa7bb..48e9aa49d0de 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -45,7 +45,7 @@ type LocaleContextProps = { /** Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions */ - formatPhoneNumber: (phoneNumber: string) => string; + formatPhoneNumber: (phoneNumber: string | undefined) => string; /** Gets the locale digit corresponding to a standard digit */ toLocaleDigit: (digit: string) => string; diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 7b38dcd34109..13f75b9869a0 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -165,7 +165,7 @@ function MoneyRequestPreview(props) { const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(props.transaction); const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(props.transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(props.transaction); - const isExpensifyCardTransaction = TransactionUtils.isExpensifyCardTransaction(props.transaction); + const isCardTransaction = TransactionUtils.isCardTransaction(props.transaction); const isSettled = ReportUtils.isSettled(props.iouReport.reportID); const isDeleted = lodashGet(props.action, 'pendingAction', null) === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; @@ -184,7 +184,7 @@ function MoneyRequestPreview(props) { const receiptImages = hasReceipt ? [ReceiptUtils.getThumbnailAndImageURIs(props.transaction)] : []; const getSettledMessage = () => { - if (isExpensifyCardTransaction) { + if (isCardTransaction) { return translate('common.done'); } return translate('iou.settledExpensify'); @@ -207,7 +207,7 @@ function MoneyRequestPreview(props) { return translate('iou.split'); } - if (isExpensifyCardTransaction) { + if (isCardTransaction) { let message = translate('iou.card'); if (TransactionUtils.isPending(props.transaction)) { message += ` • ${translate('iou.pending')}`; diff --git a/src/libs/LocalePhoneNumber.ts b/src/libs/LocalePhoneNumber.ts index 933aa7937560..9aacc6968e1e 100644 --- a/src/libs/LocalePhoneNumber.ts +++ b/src/libs/LocalePhoneNumber.ts @@ -13,7 +13,7 @@ Onyx.connect({ * Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions */ -function formatPhoneNumber(number: string): string { +function formatPhoneNumber(number: string | undefined): string { if (!number) { return ''; } diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index cdf5dc5a0502..a4cafd59cb73 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -136,6 +136,7 @@ function DetailsPage(props) { diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index f671f14813d2..bfe27910c943 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -203,6 +203,9 @@ function ReportScreen({ isPinned: reportProp.isPinned, chatReportID: reportProp.chatReportID, visibility: reportProp.visibility, + oldPolicyName: reportProp.oldPolicyName, + policyName: reportProp.policyName, + isOptimisticReport: reportProp.isOptimisticReport, }), [ reportProp.lastReadTime, @@ -236,6 +239,9 @@ function ReportScreen({ reportProp.isPinned, reportProp.chatReportID, reportProp.visibility, + reportProp.oldPolicyName, + reportProp.policyName, + reportProp.isOptimisticReport, ], ); @@ -659,17 +665,8 @@ export default compose( _.isEqual(prevProps.policies, nextProps.policies) && prevProps.accountManagerReportID === nextProps.accountManagerReportID && prevProps.userLeavingStatus === nextProps.userLeavingStatus && - prevProps.report.reportID === nextProps.report.reportID && - prevProps.report.policyID === nextProps.report.policyID && - prevProps.report.managerID === nextProps.report.managerID && - prevProps.report.isOptimisticReport === nextProps.report.isOptimisticReport && - prevProps.report.statusNum === nextProps.report.statusNum && - _.isEqual(prevProps.report.pendingFields, nextProps.report.pendingFields) && prevProps.currentReportID === nextProps.currentReportID && - prevProps.report.notificationPreference === nextProps.report.notificationPreference && - prevProps.report.isPinned === nextProps.report.isPinned && - prevProps.report.chatReportID === nextProps.report.chatReportID && - prevProps.report.visibility === nextProps.report.visibility && - prevProps.viewportOffsetTop === nextProps.viewportOffsetTop, + prevProps.viewportOffsetTop === nextProps.viewportOffsetTop && + _.isEqual(prevProps.report, nextProps.report), ), ); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index e50dcbb99f6b..62ff9426b4ae 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -314,15 +314,7 @@ function arePropsEqual(oldProps, newProps) { return false; } - return ( - oldProps.report.lastReadTime === newProps.report.lastReadTime && - oldProps.report.reportID === newProps.report.reportID && - oldProps.report.policyID === newProps.report.policyID && - oldProps.report.managerID === newProps.report.managerID && - oldProps.report.lastVisibleActionCreated === newProps.report.lastVisibleActionCreated && - oldProps.report.isOptimisticReport === newProps.report.isOptimisticReport && - _.isEqual(oldProps.report.pendingFields, newProps.report.pendingFields) - ); + return _.isEqual(oldProps.report, newProps.report); } const MemoizedReportActionsView = React.memo(ReportActionsView, arePropsEqual); diff --git a/src/pages/settings/Security/CloseAccountPage.js b/src/pages/settings/Security/CloseAccountPage.tsx similarity index 63% rename from src/pages/settings/Security/CloseAccountPage.js rename to src/pages/settings/Security/CloseAccountPage.tsx index 073788867aca..b3a7e3cae3a0 100644 --- a/src/pages/settings/Security/CloseAccountPage.js +++ b/src/pages/settings/Security/CloseAccountPage.tsx @@ -1,45 +1,41 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; -import PropTypes from 'prop-types'; import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import ConfirmModal from '@components/ConfirmModal'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; +import type {OnyxFormValuesFields} from '@components/Form/types'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ValidationUtils from '@libs/ValidationUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; import * as CloseAccount from '@userActions/CloseAccount'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {Session} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; -const propTypes = { +type CloseAccountPageOnyxProps = { /** Session of currently logged in user */ - session: PropTypes.shape({ - /** Email address */ - email: PropTypes.string.isRequired, - }), - - ...windowDimensionsPropTypes, - ...withLocalizePropTypes, + session: OnyxEntry; }; -const defaultProps = { - session: { - email: null, - }, -}; +type CloseAccountPageProps = CloseAccountPageOnyxProps & StackScreenProps; -function CloseAccountPage(props) { +function CloseAccountPage({session}: CloseAccountPageProps) { const styles = useThemeStyles(); + const {translate, formatPhoneNumber} = useLocalize(); + const [isConfirmModalVisible, setConfirmModalVisibility] = useState(false); const [reasonForLeaving, setReasonForLeaving] = useState(''); @@ -58,21 +54,21 @@ function CloseAccountPage(props) { hideConfirmModal(); }; - const showConfirmModal = (values) => { + const showConfirmModal = (values: OnyxFormValuesFields) => { setConfirmModalVisibility(true); setReasonForLeaving(values.reasonForLeaving); }; /** * Removes spaces and transform the input string to lowercase. - * @param {String} phoneOrEmail - The input string to be sanitized. - * @returns {String} The sanitized string + * @param phoneOrEmail - The input string to be sanitized. + * @returns The sanitized string */ - const sanitizePhoneOrEmail = (phoneOrEmail) => phoneOrEmail.replace(/\s+/g, '').toLowerCase(); + const sanitizePhoneOrEmail = (phoneOrEmail: string): string => phoneOrEmail.replace(/\s+/g, '').toLowerCase(); - const validate = (values) => { + const validate = (values: OnyxFormValuesFields): Errors => { const requiredFields = ['phoneOrEmail']; - const userEmailOrPhone = props.formatPhoneNumber(props.session.email); + const userEmailOrPhone = formatPhoneNumber(session?.email); const errors = ValidationUtils.getFieldRequiredErrors(values, requiredFields); if (values.phoneOrEmail && sanitizePhoneOrEmail(userEmailOrPhone) !== sanitizePhoneOrEmail(values.phoneOrEmail)) { @@ -81,7 +77,7 @@ function CloseAccountPage(props) { return errors; }; - const userEmailOrPhone = props.formatPhoneNumber(props.session.email); + const userEmailOrPhone = formatPhoneNumber(session?.email); return ( Navigation.goBack()} /> - {props.translate('closeAccountPage.reasonForLeavingPrompt')} + {translate('closeAccountPage.reasonForLeavingPrompt')} - {props.translate('closeAccountPage.enterDefaultContactToConfirm')} {userEmailOrPhone} + {translate('closeAccountPage.enterDefaultContactToConfirm')} {userEmailOrPhone} @@ -143,16 +139,10 @@ function CloseAccountPage(props) { ); } -CloseAccountPage.propTypes = propTypes; -CloseAccountPage.defaultProps = defaultProps; CloseAccountPage.displayName = 'CloseAccountPage'; -export default compose( - withLocalize, - withWindowDimensions, - withOnyx({ - session: { - key: ONYXKEYS.SESSION, - }, - }), -)(CloseAccountPage); +export default withOnyx({ + session: { + key: ONYXKEYS.SESSION, + }, +})(CloseAccountPage); diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.js index 717f9a3e718c..8d261134baba 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.android.js @@ -11,6 +11,7 @@ function BackgroundImage(props) { source={AndroidBackgroundImage} pointerEvents={props.pointerEvents} style={[styles.signInBackground, {width: props.width}]} + transition={props.transitionDuration} /> ); } diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js index da6a6b9ee4fb..b426a888e8b9 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.ios.js @@ -21,11 +21,11 @@ function BackgroundImage(props) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const src = useMemo(() => (props.isSmallScreen ? MobileBackgroundImage : DesktopBackgroundImage), [props.isSmallScreen]); - return ( ); } diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/index.js b/src/pages/signin/SignInPageLayout/BackgroundImage/index.js index ac93ceeb4e2c..96406d6f3d31 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/index.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/index.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React from 'react'; +import * as Animatable from 'react-native-animatable'; import DesktopBackgroundImage from '@assets/images/home-background--desktop.svg'; import MobileBackgroundImage from '@assets/images/home-background--mobile.svg'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -17,16 +18,33 @@ const propTypes = { }; function BackgroundImage(props) { const styles = useThemeStyles(); - return props.isSmallScreen ? ( - - ) : ( - + animation={fadeIn} + duration={props.transitionDuration} + > + {props.isSmallScreen ? ( + + ) : ( + + )} + ); } diff --git a/src/pages/signin/SignInPageLayout/BackgroundImage/propTypes.js b/src/pages/signin/SignInPageLayout/BackgroundImage/propTypes.js index 533d22ad12c5..d966b196b725 100644 --- a/src/pages/signin/SignInPageLayout/BackgroundImage/propTypes.js +++ b/src/pages/signin/SignInPageLayout/BackgroundImage/propTypes.js @@ -6,6 +6,9 @@ const propTypes = { /** The width of the image. */ width: PropTypes.number.isRequired, + + /** Transition duration in milliseconds */ + transitionDuration: PropTypes.number, }; export default propTypes; diff --git a/src/pages/signin/SignInPageLayout/index.js b/src/pages/signin/SignInPageLayout/index.js index e2f9c28f9fcd..5465a3189818 100644 --- a/src/pages/signin/SignInPageLayout/index.js +++ b/src/pages/signin/SignInPageLayout/index.js @@ -13,6 +13,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import SignInPageHero from '@pages/signin/SignInPageHero'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import BackgroundImage from './BackgroundImage'; import Footer from './Footer'; import SignInPageContent from './SignInPageContent'; @@ -127,6 +128,7 @@ function SignInPageLayout(props) { isSmallScreen={false} pointerEvents="none" width={variables.signInHeroBackgroundWidth} + transitionDuration={CONST.BACKGROUND_IMAGE_TRANSITION_DURATION} /> @@ -166,6 +168,7 @@ function SignInPageLayout(props) { isSmallScreen pointerEvents="none" width={variables.signInHeroBackgroundWidthMobile} + transitionDuration={CONST.BACKGROUND_IMAGE_TRANSITION_DURATION} /> >; +type CloseAccountForm = Form<{ + reasonForLeaving: string; + phoneOrEmail: string; +}>; + export default Form; export type { @@ -78,4 +83,5 @@ export type { PersonalBankAccountForm, WorkspaceSettingsForm, ReportFieldEditForm, + CloseAccountForm, }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 939793b3b4a8..d0ac2ce395fa 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -11,6 +11,7 @@ import type CustomStatusDraft from './CustomStatusDraft'; import type Download from './Download'; import type { AddDebitCardForm, + CloseAccountForm, DateOfBirthForm, DisplayNameForm, IKnowATeacherForm, @@ -92,6 +93,7 @@ export type { Credentials, Currency, CustomStatusDraft, + CloseAccountForm, DateOfBirthForm, Download, Form, diff --git a/web/index.html b/web/index.html index 49ffd0d0a62f..f0cc320926a7 100644 --- a/web/index.html +++ b/web/index.html @@ -121,16 +121,16 @@ <% if (htmlWebpackPlugin.options.isWeb) { %> - <% if (htmlWebpackPlugin.options.isProduction) { %> + <% if (htmlWebpackPlugin.options.isStaging) { %> - + <% } %> <% } %> - <% if (htmlWebpackPlugin.options.isWeb && htmlWebpackPlugin.options.isProduction) { %> + <% if (htmlWebpackPlugin.options.isWeb && htmlWebpackPlugin.options.isStaging) { %>