diff --git a/.github/workflows/README.md b/.github/workflows/README.md index e1b1696411b1..e432d9291f45 100644 --- a/.github/workflows/README.md +++ b/.github/workflows/README.md @@ -85,7 +85,7 @@ The GitHub workflows require a large list of secrets to deploy, notify and test 1. `LARGE_SECRET_PASSPHRASE` - decrypts secrets stored in various encrypted files stored in GitHub repository. To create updated versions of these encrypted files, refer to steps 1-4 of [this encrypted secrets help page](https://docs.github.com/en/actions/reference/encrypted-secrets#limits-for-secrets) using the `LARGE_SECRET_PASSPHRASE`. 1. `android/app/my-upload-key.keystore.gpg` 1. `android/app/android-fastlane-json-key.json.gpg` - 1. `ios/chat_expensify_adhoc.mobileprovision.gpg` + 1. `ios/expensify_chat_adhoc.mobileprovision.gpg` 1. `ios/chat_expensify_appstore.mobileprovision.gpg` 1. `ios/Certificates.p12.gpg` 1. `SLACK_WEBHOOK` - Sends Slack notifications via Slack WebHook https://expensify.slack.com/services/B01AX48D7MM diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 795271cab60a..1983e406c77b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -15,7 +15,7 @@ jobs: - uses: Expensify/App/.github/actions/composite/setupNode@main - - name: Lint JavaScript with ESLint + - name: Lint JavaScript and Typescript with ESLint run: npm run lint env: CI: true diff --git a/android/app/build.gradle b/android/app/build.gradle index 6e35c6267435..7cba91e7b0a9 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 1001036501 - versionName "1.3.65-1" + versionCode 1001036603 + versionName "1.3.66-3" } flavorDimensions "default" diff --git a/android/app/src/main/res/values-large/orientation.xml b/android/app/src/main/res/values-large/orientation.xml index c06e0147ee73..9f60d109a2fc 100644 --- a/android/app/src/main/res/values-large/orientation.xml +++ b/android/app/src/main/res/values-large/orientation.xml @@ -1,4 +1,4 @@ - false + true diff --git a/android/app/src/main/res/values-sw600dp/orientation.xml b/android/app/src/main/res/values-sw600dp/orientation.xml index c06e0147ee73..9f60d109a2fc 100644 --- a/android/app/src/main/res/values-sw600dp/orientation.xml +++ b/android/app/src/main/res/values-sw600dp/orientation.xml @@ -1,4 +1,4 @@ - false + true diff --git a/contributingGuides/FORMS.md b/contributingGuides/FORMS.md index 01f145dafbc6..661c700130c7 100644 --- a/contributingGuides/FORMS.md +++ b/contributingGuides/FORMS.md @@ -274,6 +274,7 @@ Form.js will automatically provide the following props to any input with the inp - onBlur: An onBlur handler that calls validate. - onTouched: An onTouched handler that marks the input as touched. - onInputChange: An onChange handler that saves draft values and calls validate for that input (inputA). Passing an inputID as a second param allows inputA to manipulate the input value of the provided inputID (inputB). +- onFocus: An onFocus handler that marks the input as focused. ## Dynamic Form Inputs diff --git a/docs/assets/images/insights-chart.png b/docs/assets/images/insights-chart.png index 7b10c8c92d8d..4b21b8d70a09 100644 Binary files a/docs/assets/images/insights-chart.png and b/docs/assets/images/insights-chart.png differ diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 92c61cb81b2c..ecec05f1cec1 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -224,11 +224,11 @@ platform :ios do contact_phone: ENV["APPLE_CONTACT_PHONE"], demo_account_name: ENV["APPLE_DEMO_EMAIL"], demo_account_password: ENV["APPLE_DEMO_PASSWORD"], - notes: "1. Log into the Expensify app using the provided email - 2. Now, you have to log in to this gmail account on https://mail.google.com/ so you can retrieve a One-Time-Password - 3. To log in to the gmail account, use the password above (That's NOT a password for the Expensify app but for the Gmail account) - 4. At the Gmail inbox, you should have received a one-time 6 digit magic code - 5. Use that to sign in" + notes: "1. In the Expensify app, enter the email 'appletest.expensify@proton.me'. This will trigger a sign-in link to be sent to 'appletest.expensify@proton.me' + 2. Navigate to https://account.proton.me/login, log into Proton Mail using 'appletest.expensify@proton.me' as email and the password associated with 'appletest.expensify@proton.me', provided above + 3. Once logged into Proton Mail, navigate to your inbox and locate the email triggered in step 1. The email subject should be 'Your magic sign-in link for Expensify' + 4. Open the email and copy the 6-digit sign-in code provided within + 5. Return to the Expensify app and enter the copied 6-digit code in the designated login field" } ) rescue Exception => e diff --git a/ios/Certificates.p12.gpg b/ios/Certificates.p12.gpg index c4a68891f6e4..f63d6861f888 100644 Binary files a/ios/Certificates.p12.gpg and b/ios/Certificates.p12.gpg differ diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index e5e912df6c48..e9e9394fcaae 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.3.65 + 1.3.66 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.3.65.1 + 1.3.66.3 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes @@ -108,6 +108,8 @@ armv7 + UIRequiresFullScreen + UISupportedInterfaceOrientations UIInterfaceOrientationPortrait @@ -117,8 +119,6 @@ UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationLandscapeLeft UIUserInterfaceStyle Dark diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 586c36d1e454..7286b383a0c7 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.3.65 + 1.3.66 CFBundleSignature ???? CFBundleVersion - 1.3.65.1 + 1.3.66.3 diff --git a/ios/chat_expensify_adhoc.mobileprovision.gpg b/ios/chat_expensify_adhoc.mobileprovision.gpg deleted file mode 100644 index 97179c8a65ac..000000000000 Binary files a/ios/chat_expensify_adhoc.mobileprovision.gpg and /dev/null differ diff --git a/ios/expensify_chat_adhoc.mobileprovision.gpg b/ios/expensify_chat_adhoc.mobileprovision.gpg index 1464356e423e..8160fba0cfa9 100644 Binary files a/ios/expensify_chat_adhoc.mobileprovision.gpg and b/ios/expensify_chat_adhoc.mobileprovision.gpg differ diff --git a/ios/expensify_chat_dev.mobileprovision.gpg b/ios/expensify_chat_dev.mobileprovision.gpg deleted file mode 100644 index 3b8b96b2c142..000000000000 Binary files a/ios/expensify_chat_dev.mobileprovision.gpg and /dev/null differ diff --git a/package-lock.json b/package-lock.json index 06868b2f7aca..4128a740b360 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.3.65-1", + "version": "1.3.66-3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.3.65-1", + "version": "1.3.66-3", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -85,7 +85,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.70", + "react-native-onyx": "1.0.72", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^4.0.0", @@ -40884,9 +40884,9 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.70", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.70.tgz", - "integrity": "sha512-bc/u4kkcwbrN6kLxXprZbwYqApYJ7G07IKteJhRuIjXi1hMPxOznRxxqMaOTELgET9y5LezUOB2QOwfEZ59FLg==", + "version": "1.0.72", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.72.tgz", + "integrity": "sha512-roJuA92qZH2PLYSqBhSPCse+Ra2EJu4FBpVqguwJRp6oaLNHR1CtPTgU1xMh/kj2nWmdpcqKoOc3nS35asb80g==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -76679,9 +76679,9 @@ } }, "react-native-onyx": { - "version": "1.0.70", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.70.tgz", - "integrity": "sha512-bc/u4kkcwbrN6kLxXprZbwYqApYJ7G07IKteJhRuIjXi1hMPxOznRxxqMaOTELgET9y5LezUOB2QOwfEZ59FLg==", + "version": "1.0.72", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.72.tgz", + "integrity": "sha512-roJuA92qZH2PLYSqBhSPCse+Ra2EJu4FBpVqguwJRp6oaLNHR1CtPTgU1xMh/kj2nWmdpcqKoOc3nS35asb80g==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index 40b9e6ebe92a..6574bcc4b77c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.3.65-1", + "version": "1.3.66-3", "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.", @@ -125,7 +125,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.70", + "react-native-onyx": "1.0.72", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^4.0.0", diff --git a/src/components/DragAndDrop/Provider/dragAndDropProviderPropTypes.js b/src/components/DragAndDrop/Provider/dragAndDropProviderPropTypes.js index d9cc806e9012..82e503456f7d 100644 --- a/src/components/DragAndDrop/Provider/dragAndDropProviderPropTypes.js +++ b/src/components/DragAndDrop/Provider/dragAndDropProviderPropTypes.js @@ -6,4 +6,7 @@ export default { /** Should this dropZone be disabled? */ isDisabled: PropTypes.bool, + + /** Indicate that users are dragging file or not */ + setIsDraggingOver: PropTypes.func, }; diff --git a/src/components/DragAndDrop/Provider/index.js b/src/components/DragAndDrop/Provider/index.js index 89b0f47a830d..6408f6dbfbfa 100644 --- a/src/components/DragAndDrop/Provider/index.js +++ b/src/components/DragAndDrop/Provider/index.js @@ -1,5 +1,5 @@ import _ from 'underscore'; -import React, {useRef, useCallback} from 'react'; +import React, {useRef, useCallback, useEffect} from 'react'; import {View} from 'react-native'; import {PortalHost} from '@gorhom/portal'; import Str from 'expensify-common/lib/str'; @@ -17,7 +17,7 @@ function shouldAcceptDrop(event) { return _.some(event.dataTransfer.types, (type) => type === 'Files'); } -function DragAndDropProvider({children, isDisabled = false}) { +function DragAndDropProvider({children, isDisabled = false, setIsDraggingOver = () => {}}) { const dropZone = useRef(null); const dropZoneID = useRef(Str.guid('drag-n-drop')); @@ -33,6 +33,10 @@ function DragAndDropProvider({children, isDisabled = false}) { isDisabled, }); + useEffect(() => { + setIsDraggingOver(isDraggingOver); + }, [isDraggingOver, setIsDraggingOver]); + return ( { + focusedInput.current = inputID; + if (_.isFunction(child.props.onFocus)) { + child.props.onFocus(event); + } + }, onBlur: (event) => { // Only run validation when user proactively blurs the input. if (Visibility.isVisible() && Visibility.hasFocus()) { @@ -328,6 +335,11 @@ function Form(props) { }, onInputChange: (value, key) => { const inputKey = key || inputID; + + if (focusedInput.current && focusedInput.current !== inputKey) { + setTouchedInput(focusedInput.current); + } + setInputValues((prevState) => { const newState = { ...prevState, diff --git a/src/components/HeaderGap/index.desktop.js b/src/components/HeaderGap/index.desktop.js index 10974aa9f5ee..6b47f56516de 100644 --- a/src/components/HeaderGap/index.desktop.js +++ b/src/components/HeaderGap/index.desktop.js @@ -1,9 +1,22 @@ import React, {PureComponent} from 'react'; import {View} from 'react-native'; +import PropTypes from 'prop-types'; import styles from '../../styles/styles'; -export default class HeaderGap extends PureComponent { +const propTypes = { + /** Styles to apply to the HeaderGap */ + // eslint-disable-next-line react/forbid-prop-types + styles: PropTypes.arrayOf(PropTypes.object), +}; + +class HeaderGap extends PureComponent { render() { - return ; + return ; } } + +HeaderGap.propTypes = propTypes; +HeaderGap.defaultProps = { + styles: [], +}; +export default HeaderGap; diff --git a/src/components/ReportActionItem/ReportActionItemImages.js b/src/components/ReportActionItem/ReportActionItemImages.js index 82082b18ce1c..e8e3aa8e8c40 100644 --- a/src/components/ReportActionItem/ReportActionItemImages.js +++ b/src/components/ReportActionItem/ReportActionItemImages.js @@ -11,7 +11,7 @@ const propTypes = { images: PropTypes.arrayOf( PropTypes.shape({ thumbnail: PropTypes.string, - image: PropTypes.string, + image: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }), ).isRequired, diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index ebdd79f586e1..f760e5d5aeb4 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -124,7 +124,7 @@ class ScreenWrapper extends React.Component { style={styles.flex1} enabled={this.props.shouldEnablePickerAvoiding} > - + {this.props.environment === CONST.ENVIRONMENT.DEV && } {this.props.environment === CONST.ENVIRONMENT.DEV && } { diff --git a/src/components/ScreenWrapper/propTypes.js b/src/components/ScreenWrapper/propTypes.js index 7162ca074f43..83033d9e97b7 100644 --- a/src/components/ScreenWrapper/propTypes.js +++ b/src/components/ScreenWrapper/propTypes.js @@ -36,6 +36,9 @@ const propTypes = { /** Whether to use the maxHeight (true) or use the 100% of the height (false) */ shouldEnableMaxHeight: PropTypes.bool, + /** Array of additional styles for header gap */ + headerGapStyles: PropTypes.arrayOf(PropTypes.object), + ...windowDimensionsPropTypes, ...environmentPropTypes, @@ -59,6 +62,7 @@ const defaultProps = { shouldEnablePickerAvoiding: true, shouldShowOfflineIndicator: true, offlineIndicatorStyle: [], + headerGapStyles: [], }; export {propTypes, defaultProps}; diff --git a/src/components/avatarPropTypes.js b/src/components/avatarPropTypes.js index 7e978fc74963..12ee5c622b4f 100644 --- a/src/components/avatarPropTypes.js +++ b/src/components/avatarPropTypes.js @@ -5,5 +5,5 @@ export default PropTypes.shape({ source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), type: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_WORKSPACE]), name: PropTypes.string, - id: PropTypes.number, + id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), }); diff --git a/src/components/transactionPropTypes.js b/src/components/transactionPropTypes.js index 66ed18a1f0b7..bc0a10025ba8 100644 --- a/src/components/transactionPropTypes.js +++ b/src/components/transactionPropTypes.js @@ -68,7 +68,7 @@ export default PropTypes.shape({ /** The receipt object associated with the transaction */ receipt: PropTypes.shape({ receiptID: PropTypes.number, - source: PropTypes.string, + source: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), state: PropTypes.string, }), diff --git a/src/components/withWindowDimensions.js b/src/components/withWindowDimensions/index.js similarity index 95% rename from src/components/withWindowDimensions.js rename to src/components/withWindowDimensions/index.js index 9ec9c5d4acbd..a3836fa99e6b 100644 --- a/src/components/withWindowDimensions.js +++ b/src/components/withWindowDimensions/index.js @@ -2,9 +2,9 @@ import React, {forwardRef, createContext, useState, useEffect} from 'react'; import PropTypes from 'prop-types'; import {Dimensions} from 'react-native'; import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; -import getComponentDisplayName from '../libs/getComponentDisplayName'; -import variables from '../styles/variables'; -import getWindowHeightAdjustment from '../libs/getWindowHeightAdjustment'; +import getComponentDisplayName from '../../libs/getComponentDisplayName'; +import variables from '../../styles/variables'; +import getWindowHeightAdjustment from '../../libs/getWindowHeightAdjustment'; const WindowDimensionsContext = createContext(null); const windowDimensionsPropTypes = { diff --git a/src/components/withWindowDimensions/index.native.js b/src/components/withWindowDimensions/index.native.js new file mode 100644 index 000000000000..e147a20c9f4e --- /dev/null +++ b/src/components/withWindowDimensions/index.native.js @@ -0,0 +1,116 @@ +import React, {forwardRef, createContext, useState, useEffect} from 'react'; +import PropTypes from 'prop-types'; +import {Dimensions} from 'react-native'; +import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import getComponentDisplayName from '../../libs/getComponentDisplayName'; +import variables from '../../styles/variables'; +import getWindowHeightAdjustment from '../../libs/getWindowHeightAdjustment'; + +const WindowDimensionsContext = createContext(null); +const windowDimensionsPropTypes = { + // Width of the window + windowWidth: PropTypes.number.isRequired, + + // Height of the window + windowHeight: PropTypes.number.isRequired, + + // Is the window width extra narrow, like on a Fold mobile device? + isExtraSmallScreenWidth: PropTypes.bool.isRequired, + + // Is the window width narrow, like on a mobile device? + isSmallScreenWidth: PropTypes.bool.isRequired, + + // Is the window width medium sized, like on a tablet device? + isMediumScreenWidth: PropTypes.bool.isRequired, + + // Is the window width wide, like on a browser or desktop? + isLargeScreenWidth: PropTypes.bool.isRequired, +}; + +const windowDimensionsProviderPropTypes = { + /* Actual content wrapped by this component */ + children: PropTypes.node.isRequired, +}; + +function WindowDimensionsProvider(props) { + const [windowDimension, setWindowDimension] = useState(() => { + const initialDimensions = Dimensions.get('window'); + return { + windowHeight: initialDimensions.height, + windowWidth: initialDimensions.width, + }; + }); + + useEffect(() => { + const onDimensionChange = (newDimensions) => { + const {window} = newDimensions; + + setWindowDimension({ + windowHeight: window.height, + windowWidth: window.width, + }); + }; + + const dimensionsEventListener = Dimensions.addEventListener('change', onDimensionChange); + + return () => { + if (!dimensionsEventListener) { + return; + } + dimensionsEventListener.remove(); + }; + }, []); + + return ( + + {(insets) => { + const isExtraSmallScreenWidth = windowDimension.windowWidth <= variables.extraSmallMobileResponsiveWidthBreakpoint; + const isSmallScreenWidth = true; + const isMediumScreenWidth = false; + const isLargeScreenWidth = false; + return ( + + {props.children} + + ); + }} + + ); +} + +WindowDimensionsProvider.propTypes = windowDimensionsProviderPropTypes; +WindowDimensionsProvider.displayName = 'WindowDimensionsProvider'; + +/** + * @param {React.Component} WrappedComponent + * @returns {React.Component} + */ +export default function withWindowDimensions(WrappedComponent) { + const WithWindowDimensions = forwardRef((props, ref) => ( + + {(windowDimensionsProps) => ( + + )} + + )); + + WithWindowDimensions.displayName = `withWindowDimensions(${getComponentDisplayName(WrappedComponent)})`; + return WithWindowDimensions; +} + +export {WindowDimensionsProvider, windowDimensionsPropTypes}; diff --git a/src/hooks/useWindowDimensions.js b/src/hooks/useWindowDimensions/index.js similarity index 95% rename from src/hooks/useWindowDimensions.js rename to src/hooks/useWindowDimensions/index.js index 58e6b8758927..86ff7ce85d3d 100644 --- a/src/hooks/useWindowDimensions.js +++ b/src/hooks/useWindowDimensions/index.js @@ -1,6 +1,6 @@ // eslint-disable-next-line no-restricted-imports import {useWindowDimensions} from 'react-native'; -import variables from '../styles/variables'; +import variables from '../../styles/variables'; /** * A convenience wrapper around React Native's useWindowDimensions hook that also provides booleans for our breakpoints. diff --git a/src/hooks/useWindowDimensions/index.native.js b/src/hooks/useWindowDimensions/index.native.js new file mode 100644 index 000000000000..358e43f1b75d --- /dev/null +++ b/src/hooks/useWindowDimensions/index.native.js @@ -0,0 +1,23 @@ +// eslint-disable-next-line no-restricted-imports +import {useWindowDimensions} from 'react-native'; +import variables from '../../styles/variables'; + +/** + * A convenience wrapper around React Native's useWindowDimensions hook that also provides booleans for our breakpoints. + * @returns {Object} + */ +export default function () { + const {width: windowWidth, height: windowHeight} = useWindowDimensions(); + const isExtraSmallScreenHeight = windowHeight <= variables.extraSmallMobileResponsiveHeightBreakpoint; + const isSmallScreenWidth = true; + const isMediumScreenWidth = false; + const isLargeScreenWidth = false; + return { + windowWidth, + windowHeight, + isExtraSmallScreenHeight, + isSmallScreenWidth, + isMediumScreenWidth, + isLargeScreenWidth, + }; +} diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index 42d6627d6699..00c2d536e8ba 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -72,12 +72,12 @@ function NavigationRoot(props) { }, [isSmallScreenWidth]); useEffect(() => { - if (!navigationRef.isReady()) { + if (!navigationRef.isReady() || !props.authenticated) { return; } // We need to force state rehydration so the CustomRouter can add the CentralPaneNavigator route if necessary. navigationRef.resetRoot(navigationRef.getRootState()); - }, [isSmallScreenWidth]); + }, [isSmallScreenWidth, props.authenticated]); const prevStatusBarBackgroundColor = useRef(themeColors.appBG); const statusBarBackgroundColor = useRef(themeColors.appBG); diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 7390bac47dd1..ee8d55fdd777 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -815,7 +815,7 @@ function getRoomWelcomeMessage(report, isUserPolicyAdmin) { * @returns {Boolean} */ function chatIncludesConcierge(report) { - return report.participantAccountIDs && _.contains(report.participantAccountIDs, CONST.ACCOUNT_ID.CONCIERGE); + return !_.isEmpty(report.participantAccountIDs) && _.contains(report.participantAccountIDs, CONST.ACCOUNT_ID.CONCIERGE); } /** diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index afce28aab60c..c6f99b2687bf 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -143,7 +143,7 @@ function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseRep } // Always copy over the category for now until we have a way to edit it (Will be implemented in https://github.com/Expensify/App/issues/24464) - updatedTransaction.category = transaction.category + updatedTransaction.category = transaction.category; updatedTransaction.pendingFields = { ...(_.has(transactionChanges, 'comment') && {comment: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), diff --git a/src/libs/actions/Receipt.js b/src/libs/actions/Receipt.ts similarity index 72% rename from src/libs/actions/Receipt.js rename to src/libs/actions/Receipt.ts index fbe9c22faaa2..530db149d902 100644 --- a/src/libs/actions/Receipt.js +++ b/src/libs/actions/Receipt.ts @@ -3,12 +3,8 @@ import ONYXKEYS from '../../ONYXKEYS'; /** * Sets the upload receipt error modal content when an invalid receipt is uploaded - * - * @param {Boolean} isAttachmentInvalid - * @param {String} attachmentInvalidReasonTitle - * @param {String} attachmentInvalidReason */ -function setUploadReceiptError(isAttachmentInvalid, attachmentInvalidReasonTitle, attachmentInvalidReason) { +function setUploadReceiptError(isAttachmentInvalid: boolean, attachmentInvalidReasonTitle: string, attachmentInvalidReason: string) { Onyx.merge(ONYXKEYS.RECEIPT_MODAL, { isAttachmentInvalid, attachmentInvalidReasonTitle, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 8b898a6aaaea..881615948a38 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1298,10 +1298,6 @@ function updateWriteCapabilityAndNavigate(report, newValue) { */ function navigateToConciergeChat() { if (!conciergeChatReportID) { - // In order not to delay the report life cycle, we first navigate to the unknown report - if (!Navigation.getTopmostReportId()) { - Navigation.navigate(ROUTES.REPORT); - } // In order to avoid creating concierge repeatedly, // we need to ensure that the server data has been successfully pulled Welcome.serverDataIsReadyPromise().then(() => { diff --git a/src/libs/actions/Session/clearCache/index.js b/src/libs/actions/Session/clearCache/index.js deleted file mode 100644 index 9ccd0193cfbd..000000000000 --- a/src/libs/actions/Session/clearCache/index.js +++ /dev/null @@ -1,5 +0,0 @@ -function clearStorage() { - return new Promise((res) => res()); -} - -export default clearStorage; diff --git a/src/libs/actions/Session/clearCache/index.native.js b/src/libs/actions/Session/clearCache/index.native.js deleted file mode 100644 index 3bd647dbf8fb..000000000000 --- a/src/libs/actions/Session/clearCache/index.native.js +++ /dev/null @@ -1,8 +0,0 @@ -import {CachesDirectoryPath, unlink} from 'react-native-fs'; - -function clearStorage() { - // `unlink` is used to delete the caches directory - return unlink(CachesDirectoryPath); -} - -export default clearStorage; diff --git a/src/libs/actions/Session/clearCache/index.native.ts b/src/libs/actions/Session/clearCache/index.native.ts new file mode 100644 index 000000000000..ce2e6beafa9f --- /dev/null +++ b/src/libs/actions/Session/clearCache/index.native.ts @@ -0,0 +1,7 @@ +import {CachesDirectoryPath, unlink} from 'react-native-fs'; +import ClearCache from './types'; + +// `unlink` is used to delete the caches directory +const clearStorage: ClearCache = () => unlink(CachesDirectoryPath); + +export default clearStorage; diff --git a/src/libs/actions/Session/clearCache/index.ts b/src/libs/actions/Session/clearCache/index.ts new file mode 100644 index 000000000000..2722d8636a75 --- /dev/null +++ b/src/libs/actions/Session/clearCache/index.ts @@ -0,0 +1,5 @@ +import ClearCache from './types'; + +const clearStorage: ClearCache = () => new Promise((res) => res()); + +export default clearStorage; diff --git a/src/libs/actions/Session/clearCache/types.ts b/src/libs/actions/Session/clearCache/types.ts new file mode 100644 index 000000000000..8c04b73e09c1 --- /dev/null +++ b/src/libs/actions/Session/clearCache/types.ts @@ -0,0 +1,3 @@ +type ClearCache = () => Promise; + +export default ClearCache; diff --git a/src/libs/checkForUpdates.js b/src/libs/checkForUpdates.js deleted file mode 100644 index fbf7ee84a8a7..000000000000 --- a/src/libs/checkForUpdates.js +++ /dev/null @@ -1,23 +0,0 @@ -const _ = require('underscore'); - -const UPDATE_INTERVAL = 1000 * 60 * 60 * 8; - -/** - * Check for updates every 8 hours and perform and platform-specific update - * - * @param {Object} platformSpecificUpdater - * @param {Function} platformSpecificUpdater.update - * @param {Function} [platformSpecificUpdater.init] - */ -function checkForUpdates(platformSpecificUpdater) { - if (_.isFunction(platformSpecificUpdater.init)) { - platformSpecificUpdater.init(); - } - - // Check for updates every hour - setInterval(() => { - platformSpecificUpdater.update(); - }, UPDATE_INTERVAL); -} - -module.exports = checkForUpdates; diff --git a/src/libs/checkForUpdates.ts b/src/libs/checkForUpdates.ts new file mode 100644 index 000000000000..51ce12335e29 --- /dev/null +++ b/src/libs/checkForUpdates.ts @@ -0,0 +1,19 @@ +const UPDATE_INTERVAL = 1000 * 60 * 60 * 8; + +type PlatformSpecificUpdater = { + update: () => void; + init?: () => void; +}; + +function checkForUpdates(platformSpecificUpdater: PlatformSpecificUpdater) { + if (typeof platformSpecificUpdater.init === 'function') { + platformSpecificUpdater.init(); + } + + // Check for updates every hour + setInterval(() => { + platformSpecificUpdater.update(); + }, UPDATE_INTERVAL); +} + +module.exports = checkForUpdates; diff --git a/src/libs/onyxSubscribe.js b/src/libs/onyxSubscribe.js deleted file mode 100644 index 600d010ed27f..000000000000 --- a/src/libs/onyxSubscribe.js +++ /dev/null @@ -1,12 +0,0 @@ -import Onyx from 'react-native-onyx'; - -/** - * Connect to onyx data. Same params as Onyx.connect(), but returns a function to unsubscribe. - * - * @param {Object} mapping Same as for Onyx.connect() - * @return {function(): void} Unsubscribe callback - */ -export default (mapping) => { - const connectionId = Onyx.connect(mapping); - return () => Onyx.disconnect(connectionId); -}; diff --git a/src/libs/onyxSubscribe.ts b/src/libs/onyxSubscribe.ts new file mode 100644 index 000000000000..469a7b810b1f --- /dev/null +++ b/src/libs/onyxSubscribe.ts @@ -0,0 +1,15 @@ +import Onyx, {ConnectOptions} from 'react-native-onyx'; +import {OnyxKey} from '../ONYXKEYS'; + +/** + * Connect to onyx data. Same params as Onyx.connect(), but returns a function to unsubscribe. + * + * @param mapping Same as for Onyx.connect() + * @return Unsubscribe callback + */ +function onyxSubscribe(mapping: ConnectOptions) { + const connectionId = Onyx.connect(mapping); + return () => Onyx.disconnect(connectionId); +} + +export default onyxSubscribe; diff --git a/src/libs/tryResolveUrlFromApiRoot.js b/src/libs/tryResolveUrlFromApiRoot.js index dc5780bb25e3..cc46f034e45b 100644 --- a/src/libs/tryResolveUrlFromApiRoot.js +++ b/src/libs/tryResolveUrlFromApiRoot.js @@ -20,6 +20,11 @@ const ORIGIN_PATTERN = new RegExp(`^(${ORIGINS_TO_REPLACE.join('|')})`); * @returns {String} */ export default function tryResolveUrlFromApiRoot(url) { + // in native, when we import an image asset, it will have a number representation which can be used in `source` of Image + // in this case we can skip the url resolving + if (typeof url === 'number') { + return url; + } const apiRoot = ApiUtils.getApiRoot({shouldUseSecure: false}); return url.replace(ORIGIN_PATTERN, apiRoot); } diff --git a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js index 1a3f63ede6e6..a75a03f7a517 100644 --- a/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js +++ b/src/pages/home/sidebar/SidebarScreen/FloatingActionButtonAndPopover.js @@ -238,11 +238,6 @@ function FloatingActionButtonAndPopover(props) { text: props.translate('iou.requestMoney'), onSelected: () => interceptAnonymousUser(() => IOU.startMoneyRequest(CONST.IOU.MONEY_REQUEST_TYPE.REQUEST)), }, - { - icon: Expensicons.Heart, - text: props.translate('sidebarScreen.saveTheWorld'), - onSelected: () => interceptAnonymousUser(() => Navigation.navigate(ROUTES.SAVE_THE_WORLD)), - }, { icon: Expensicons.Receipt, text: props.translate('iou.splitBill'), diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 2a2f3674cdfd..32d646702fb2 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -1,6 +1,6 @@ import {withOnyx} from 'react-native-onyx'; import {View} from 'react-native'; -import React from 'react'; +import React, {useState} from 'react'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import ONYXKEYS from '../../ONYXKEYS'; @@ -21,6 +21,7 @@ import OnyxTabNavigator, {TopTab} from '../../libs/Navigation/OnyxTabNavigator'; import NewRequestAmountPage from './steps/NewRequestAmountPage'; import reportPropTypes from '../reportPropTypes'; import * as ReportUtils from '../../libs/ReportUtils'; +import themeColors from '../../styles/themes/default'; const propTypes = { /** React Navigation route */ @@ -43,11 +44,13 @@ const propTypes = { }; const defaultProps = { - selectedTab: CONST.TAB.MANUAL, + selectedTab: CONST.TAB.SCAN, report: {}, }; function MoneyRequestSelectorPage(props) { + const [isDraggingOver, setIsDraggingOver] = useState(false); + const iouType = lodashGet(props.route, 'params.iouType', ''); const reportID = lodashGet(props.route, 'params.reportID', ''); const {translate} = useLocalize(); @@ -70,10 +73,22 @@ function MoneyRequestSelectorPage(props) { {({safeAreaPaddingBottomStyle}) => ( - + (