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}) => (
-
+
(