diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml
index 4c5dfdb14627..6aeecb3b4e05 100644
--- a/.github/workflows/platformDeploy.yml
+++ b/.github/workflows/platformDeploy.yml
@@ -10,7 +10,7 @@ on:
env:
SHOULD_DEPLOY_PRODUCTION: ${{ github.event_name == 'release' }}
- DEVELOPER_DIR: /Applications/Xcode_14.1.app/Contents/Developer
+ DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer
concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}
diff --git a/.github/workflows/testBuild.yml b/.github/workflows/testBuild.yml
index 5d860b41b786..8b18b8aa5d53 100644
--- a/.github/workflows/testBuild.yml
+++ b/.github/workflows/testBuild.yml
@@ -11,7 +11,7 @@ on:
branches: ['*ci-test/**']
env:
- DEVELOPER_DIR: /Applications/Xcode_14.2.app/Contents/Developer
+ DEVELOPER_DIR: /Applications/Xcode_15.0.1.app/Contents/Developer
jobs:
validateActor:
@@ -159,7 +159,7 @@ jobs:
uses: ./.github/actions/composite/setupNode
- name: Setup XCode
- run: sudo xcode-select -switch /Applications/Xcode_14.2.app
+ run: sudo xcode-select -switch /Applications/Xcode_15.0.1.app
- name: Setup Ruby
uses: ruby/setup-ruby@a05e47355e80e57b9a67566a813648fa67d92011
diff --git a/.github/workflows/testGithubActionsWorkflows.yml b/.github/workflows/testGithubActionsWorkflows.yml
index 58de3ba2d9f3..d052b343cf0c 100644
--- a/.github/workflows/testGithubActionsWorkflows.yml
+++ b/.github/workflows/testGithubActionsWorkflows.yml
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
workflow_call:
pull_request:
- types: [opened, reopened, edited, synchronize]
+ types: [opened, reopened, synchronize]
branches-ignore: [staging, production]
paths: ['.github/**']
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 4ff7984a2419..e93150b48fca 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -96,8 +96,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
- versionCode 1001042001
- versionName "1.4.20-1"
+ versionCode 1001042101
+ versionName "1.4.21-1"
}
flavorDimensions "default"
diff --git a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md b/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md
index 65361ba1af9a..0856e2694340 100644
--- a/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md
+++ b/docs/articles/expensify-classic/integrations/accounting-integrations/Certinia.md
@@ -3,7 +3,7 @@ title: Certinia
description: Guide to connecting Expensify and Certinia FFA and PSA/SRP (formerly known as FinancialForce)
---
# Overview
-[Cetinia](https://use.expensify.com/financialforce) (Formerly known as FinancialForce)is a cloud-based software solution that provides a range of financial management and accounting applications built on the Salesforce platform. There are two versions: PSA/SRP and FFA and we support both.
+[Cetinia](https://use.expensify.com/financialforce) (formerly known as FinancialForce) is a cloud-based software solution that provides a range of financial management and accounting applications built on the Salesforce platform. There are two versions: PSA/SRP and FFA and we support both.
# Before connecting to Certinia
Install the Expensify bundle in Certinia using the relevant installer:
diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist
index 40788ffa3f80..f267261a49c0 100644
--- a/ios/NewExpensify/Info.plist
+++ b/ios/NewExpensify/Info.plist
@@ -19,7 +19,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.20
+ 1.4.21
CFBundleSignature
????
CFBundleURLTypes
@@ -40,7 +40,7 @@
CFBundleVersion
- 1.4.20.1
+ 1.4.21.1
ITSAppUsesNonExemptEncryption
LSApplicationQueriesSchemes
diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist
index e2f9b3b1629b..f95a3f871d4c 100644
--- a/ios/NewExpensifyTests/Info.plist
+++ b/ios/NewExpensifyTests/Info.plist
@@ -15,10 +15,10 @@
CFBundlePackageType
BNDL
CFBundleShortVersionString
- 1.4.20
+ 1.4.21
CFBundleSignature
????
CFBundleVersion
- 1.4.20.1
+ 1.4.21.1
diff --git a/package-lock.json b/package-lock.json
index a055624e54b6..c8b4b2ba2082 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "new.expensify",
- "version": "1.4.20-1",
+ "version": "1.4.21-1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "new.expensify",
- "version": "1.4.20-1",
+ "version": "1.4.21-1",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
diff --git a/package.json b/package.json
index a35844df3428..bd4b8ffacedf 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
- "version": "1.4.20-1",
+ "version": "1.4.21-1",
"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/src/CONST.ts b/src/CONST.ts
index abba27b0c33b..865e97cc0133 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -719,6 +719,7 @@ const CONST = {
TRIE_INITIALIZATION: 'trie_initialization',
COMMENT_LENGTH_DEBOUNCE_TIME: 500,
SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300,
+ RESIZE_DEBOUNCE_TIME: 100,
},
PRIORITY_MODE: {
GSD: 'gsd',
@@ -853,7 +854,7 @@ const CONST = {
// It's copied here so that the same regex pattern can be used in form validations to be consistent with the server.
VALIDATE_FOR_HTML_TAG_REGEX: /<([^>\s]+)(?:[^>]*?)>/g,
- VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX: /<([\s]+[\s\w~!@#$%^&*(){}[\];':"`|?.,/\\+\-=<]+.*[\s]*)>/g,
+ VALIDATE_FOR_LEADINGSPACES_HTML_TAG_REGEX: /<([\s]+.+[\s]*)>/g,
WHITELISTED_TAGS: [/<>/, /< >/, /<->/, /<-->/, /
/, /
/],
@@ -979,6 +980,7 @@ const CONST = {
CHAT_FOOTER_SECONDARY_ROW_HEIGHT: 15,
CHAT_FOOTER_SECONDARY_ROW_PADDING: 5,
CHAT_FOOTER_MIN_HEIGHT: 65,
+ CHAT_FOOTER_HORIZONTAL_PADDING: 40,
CHAT_SKELETON_VIEW: {
AVERAGE_ROW_HEIGHT: 80,
HEIGHT_FOR_ROW_COUNT: {
@@ -1174,6 +1176,7 @@ const CONST = {
EXPENSIFY: 'Expensify',
VBBA: 'ACH',
},
+ DEFAULT_AMOUNT: 0,
TYPE: {
SEND: 'send',
SPLIT: 'split',
diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js
index d9e4ef2c0f6e..31b04a3d954f 100644
--- a/src/components/AddressSearch/index.js
+++ b/src/components/AddressSearch/index.js
@@ -111,6 +111,9 @@ const propTypes = {
/** Information about the network */
network: networkPropTypes.isRequired,
+ /** Location bias for querying search results. */
+ locationBias: PropTypes.string,
+
...withLocalizePropTypes,
};
@@ -138,6 +141,7 @@ const defaultProps = {
maxInputLength: undefined,
predefinedPlaces: [],
resultTypes: 'address',
+ locationBias: undefined,
};
function AddressSearch({
@@ -162,6 +166,7 @@ function AddressSearch({
shouldSaveDraft,
translate,
value,
+ locationBias,
}) {
const theme = useTheme();
const styles = useThemeStyles();
@@ -179,11 +184,11 @@ function AddressSearch({
language: preferredLocale,
types: resultTypes,
components: isLimitedToUSA ? 'country:us' : undefined,
+ ...(locationBias && {locationbias: locationBias}),
}),
- [preferredLocale, resultTypes, isLimitedToUSA],
+ [preferredLocale, resultTypes, isLimitedToUSA, locationBias],
);
const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused;
-
const saveLocationDetails = (autocompleteData, details) => {
const addressComponents = details.address_components;
if (!addressComponents) {
@@ -192,7 +197,7 @@ function AddressSearch({
// amount of data massaging needs to happen for what the parent expects to get from this function.
if (_.size(details)) {
onPress({
- address: lodashGet(details, 'description'),
+ address: autocompleteData.description || lodashGet(details, 'description', ''),
lat: lodashGet(details, 'geometry.location.lat', 0),
lng: lodashGet(details, 'geometry.location.lng', 0),
name: lodashGet(details, 'name'),
@@ -261,7 +266,7 @@ function AddressSearch({
lat: lodashGet(details, 'geometry.location.lat', 0),
lng: lodashGet(details, 'geometry.location.lng', 0),
- address: lodashGet(details, 'formatted_address', ''),
+ address: autocompleteData.description || lodashGet(details, 'formatted_address', ''),
};
// If the address is not in the US, use the full length state name since we're displaying the address's
diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx
index 8604d20130c7..2dae84106971 100644
--- a/src/components/ArchivedReportFooter.tsx
+++ b/src/components/ArchivedReportFooter.tsx
@@ -30,14 +30,14 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}}
const originalMessage = reportClosedAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? reportClosedAction.originalMessage : null;
const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT;
- let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[report?.ownerAccountID ?? 0]?.displayName);
+ let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[report?.ownerAccountID ?? 0]);
let oldDisplayName: string | undefined;
if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) {
const newAccountID = originalMessage?.newAccountID;
const oldAccountID = originalMessage?.oldAccountID;
- displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[newAccountID ?? 0]?.displayName);
- oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[oldAccountID ?? 0]?.displayName);
+ displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[newAccountID ?? 0]);
+ oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[oldAccountID ?? 0]);
}
const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT;
diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js
index 863e59aa4474..51912c04eb31 100755
--- a/src/components/AttachmentModal.js
+++ b/src/components/AttachmentModal.js
@@ -358,6 +358,14 @@ function AttachmentModal(props) {
setIsModalOpen(true);
}, []);
+ useEffect(() => {
+ setSource(props.source);
+ }, [props.source]);
+
+ useEffect(() => {
+ setIsAuthTokenRequired(props.isAuthTokenRequired);
+ }, [props.isAuthTokenRequired]);
+
const sourceForAttachmentView = props.source || source;
const threeDotsMenuItems = useMemo(() => {
@@ -368,7 +376,7 @@ function AttachmentModal(props) {
const parentReportAction = props.parentReportActions[props.report.parentReportActionID];
const canEdit =
- ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT) &&
+ ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT, props.transaction) &&
!TransactionUtils.isDistanceRequest(props.transaction);
if (canEdit) {
menuItems.push({
@@ -396,7 +404,7 @@ function AttachmentModal(props) {
}
return menuItems;
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [props.isReceiptAttachment, props.parentReport, props.parentReportActions, props.policy, props.transaction, file]);
+ }, [props.isReceiptAttachment, props.parentReport, props.parentReportActions, props.policy, props.transaction, file, source]);
// There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment.
// props.isReceiptAttachment will be null until its certain what the file is, in which case it will then be true|false.
diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
index c2320f7c0202..b72662f989dc 100644
--- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
+++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx
@@ -1,5 +1,5 @@
import {FlashList} from '@shopify/flash-list';
-import React, {ForwardedRef, forwardRef, ReactElement, useCallback, useEffect, useRef} from 'react';
+import React, {ForwardedRef, forwardRef, ReactElement, useCallback, useEffect, useMemo, useRef} from 'react';
import {View} from 'react-native';
// We take ScrollView from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another
import {ScrollView} from 'react-native-gesture-handler';
@@ -8,6 +8,8 @@ import ColorSchemeWrapper from '@components/ColorSchemeWrapper';
import PressableWithFeedback from '@components/Pressable/PressableWithFeedback';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
+import useWindowDimensions from '@hooks/useWindowDimensions';
+import variables from '@styles/variables';
import CONST from '@src/CONST';
import viewForwardedRef from '@src/types/utils/viewForwardedRef';
import type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps} from './types';
@@ -39,6 +41,7 @@ function BaseAutoCompleteSuggestions(
}: AutoCompleteSuggestionsProps,
ref: ForwardedRef,
) {
+ const {windowWidth, isLargeScreenWidth} = useWindowDimensions();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const rowHeight = useSharedValue(0);
@@ -64,7 +67,13 @@ function BaseAutoCompleteSuggestions(
const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length;
const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value));
-
+ const estimatedListSize = useMemo(
+ () => ({
+ height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length,
+ width: (isLargeScreenWidth ? windowWidth - variables.sideBarWidth : windowWidth) - CONST.CHAT_FOOTER_HORIZONTAL_PADDING,
+ }),
+ [isLargeScreenWidth, suggestions.length, windowWidth],
+ );
useEffect(() => {
rowHeight.value = withTiming(measureHeightOfSuggestionRows(suggestions.length, isSuggestionPickerLarge), {
duration: 100,
@@ -88,6 +97,7 @@ function BaseAutoCompleteSuggestions(
| ImageSourcePropType;
/** Color for the icon (should be from theme) */
- iconColor: PropTypes.string,
+ iconColor?: string;
/** Title message below the icon */
- title: PropTypes.string.isRequired,
+ title: string;
/** Subtitle message below the title */
- subtitle: PropTypes.string,
+ subtitle?: string;
/** Link message below the subtitle */
- linkKey: PropTypes.string,
+ linkKey?: TranslationPaths;
/** Whether we should show a link to navigate elsewhere */
- shouldShowLink: PropTypes.bool,
+ shouldShowLink?: boolean;
/** The custom icon width */
- iconWidth: PropTypes.number,
+ iconWidth?: number;
/** The custom icon height */
- iconHeight: PropTypes.number,
+ iconHeight?: number;
/** Function to call when pressing the navigation link */
- onLinkPress: PropTypes.func,
+ onLinkPress?: () => void;
/** Whether we should embed the link with subtitle */
- shouldEmbedLinkWithSubtitle: PropTypes.bool,
+ shouldEmbedLinkWithSubtitle?: boolean;
};
-const defaultProps = {
- iconColor: null,
- subtitle: '',
- shouldShowLink: false,
- linkKey: 'notFound.goBackHome',
- iconWidth: variables.iconSizeSuperLarge,
- iconHeight: variables.iconSizeSuperLarge,
- onLinkPress: () => Navigation.dismissModal(),
- shouldEmbedLinkWithSubtitle: false,
-};
-
-function BlockingView(props) {
+function BlockingView({
+ icon,
+ iconColor,
+ title,
+ subtitle = '',
+ linkKey = 'notFound.goBackHome',
+ shouldShowLink = false,
+ iconWidth = variables.iconSizeSuperLarge,
+ iconHeight = variables.iconSizeSuperLarge,
+ onLinkPress = () => Navigation.dismissModal(),
+ shouldEmbedLinkWithSubtitle = false,
+}: BlockingViewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
function renderContent() {
@@ -62,14 +62,14 @@ function BlockingView(props) {
<>
- {props.shouldShowLink ? (
+ {shouldShowLink ? (
- {translate(props.linkKey)}
+ {translate(linkKey)}
) : null}
>
@@ -79,14 +79,14 @@ function BlockingView(props) {
return (
- {props.title}
+ {title}
- {props.shouldEmbedLinkWithSubtitle ? (
+ {shouldEmbedLinkWithSubtitle ? (
{renderContent()}
) : (
{renderContent()}
@@ -95,8 +95,6 @@ function BlockingView(props) {
);
}
-BlockingView.propTypes = propTypes;
-BlockingView.defaultProps = defaultProps;
BlockingView.displayName = 'BlockingView';
export default BlockingView;
diff --git a/src/components/BlockingViews/FullPageNotFoundView.js b/src/components/BlockingViews/FullPageNotFoundView.tsx
similarity index 69%
rename from src/components/BlockingViews/FullPageNotFoundView.js
rename to src/components/BlockingViews/FullPageNotFoundView.tsx
index ce76b96c0eb0..6d7f838bf6c2 100644
--- a/src/components/BlockingViews/FullPageNotFoundView.js
+++ b/src/components/BlockingViews/FullPageNotFoundView.tsx
@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
import React from 'react';
import {View} from 'react-native';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
@@ -7,54 +6,54 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
+import {TranslationPaths} from '@src/languages/types';
import ROUTES from '@src/ROUTES';
import BlockingView from './BlockingView';
-const propTypes = {
+type FullPageNotFoundViewProps = {
/** Child elements */
- children: PropTypes.node,
+ children?: React.ReactNode;
/** If true, child components are replaced with a blocking "not found" view */
- shouldShow: PropTypes.bool,
+ shouldShow?: boolean;
/** The key in the translations file to use for the title */
- titleKey: PropTypes.string,
+ titleKey?: TranslationPaths;
/** The key in the translations file to use for the subtitle */
- subtitleKey: PropTypes.string,
+ subtitleKey?: TranslationPaths;
/** Whether we should show a link to navigate elsewhere */
- shouldShowLink: PropTypes.bool,
+ shouldShowLink?: boolean;
/** Whether we should show the back button on the header */
- shouldShowBackButton: PropTypes.bool,
+ shouldShowBackButton?: boolean;
/** The key in the translations file to use for the go back link */
- linkKey: PropTypes.string,
+ linkKey?: TranslationPaths;
/** Method to trigger when pressing the back button of the header */
- onBackButtonPress: PropTypes.func,
+ onBackButtonPress: () => void;
/** Function to call when pressing the navigation link */
- onLinkPress: PropTypes.func,
-};
-
-const defaultProps = {
- children: null,
- shouldShow: false,
- titleKey: 'notFound.notHere',
- subtitleKey: 'notFound.pageNotFound',
- linkKey: 'notFound.goBackHome',
- onBackButtonPress: () => Navigation.goBack(ROUTES.HOME),
- shouldShowLink: true,
- shouldShowBackButton: true,
- onLinkPress: () => Navigation.dismissModal(),
+ onLinkPress: () => void;
};
// eslint-disable-next-line rulesdir/no-negated-variables
-function FullPageNotFoundView({children, shouldShow, titleKey, subtitleKey, linkKey, onBackButtonPress, shouldShowLink, shouldShowBackButton, onLinkPress}) {
+function FullPageNotFoundView({
+ children = null,
+ shouldShow = false,
+ titleKey = 'notFound.notHere',
+ subtitleKey = 'notFound.pageNotFound',
+ linkKey = 'notFound.goBackHome',
+ onBackButtonPress = () => Navigation.goBack(ROUTES.HOME),
+ shouldShowLink = true,
+ shouldShowBackButton = true,
+ onLinkPress = () => Navigation.dismissModal(),
+}: FullPageNotFoundViewProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
+
if (shouldShow) {
return (
<>
@@ -81,8 +80,6 @@ function FullPageNotFoundView({children, shouldShow, titleKey, subtitleKey, link
return children;
}
-FullPageNotFoundView.propTypes = propTypes;
-FullPageNotFoundView.defaultProps = defaultProps;
FullPageNotFoundView.displayName = 'FullPageNotFoundView';
export default FullPageNotFoundView;
diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.js b/src/components/BlockingViews/FullPageOfflineBlockingView.js
deleted file mode 100644
index adbda21456dc..000000000000
--- a/src/components/BlockingViews/FullPageOfflineBlockingView.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import * as Expensicons from '@components/Icon/Expensicons';
-import networkPropTypes from '@components/networkPropTypes';
-import {withNetwork} from '@components/OnyxProvider';
-import withLocalize, {withLocalizePropTypes} from '@components/withLocalize';
-import useTheme from '@hooks/useTheme';
-import compose from '@libs/compose';
-import BlockingView from './BlockingView';
-
-const propTypes = {
- /** Child elements */
- children: PropTypes.node.isRequired,
-
- /** Props to fetch translation features */
- ...withLocalizePropTypes,
-
- /** Props to detect online status */
- network: networkPropTypes.isRequired,
-};
-
-function FullPageOfflineBlockingView(props) {
- const theme = useTheme();
-
- if (props.network.isOffline) {
- return (
-
- );
- }
-
- return props.children;
-}
-
-FullPageOfflineBlockingView.propTypes = propTypes;
-FullPageOfflineBlockingView.displayName = 'FullPageOfflineBlockingView';
-
-export default compose(withLocalize, withNetwork())(FullPageOfflineBlockingView);
diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx
new file mode 100644
index 000000000000..a9ebcf969ae5
--- /dev/null
+++ b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx
@@ -0,0 +1,31 @@
+import React from 'react';
+import * as Expensicons from '@components/Icon/Expensicons';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useTheme from '@hooks/useTheme';
+import ChildrenProps from '@src/types/utils/ChildrenProps';
+import BlockingView from './BlockingView';
+
+function FullPageOfflineBlockingView({children}: ChildrenProps) {
+ const {translate} = useLocalize();
+ const {isOffline} = useNetwork();
+
+ const theme = useTheme();
+
+ if (isOffline) {
+ return (
+
+ );
+ }
+
+ return children;
+}
+
+FullPageOfflineBlockingView.displayName = 'FullPageOfflineBlockingView';
+
+export default FullPageOfflineBlockingView;
diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx
index 715603ea362e..ac18b550501d 100644
--- a/src/components/Checkbox.tsx
+++ b/src/components/Checkbox.tsx
@@ -1,4 +1,4 @@
-import React, {ForwardedRef, forwardRef, KeyboardEvent as ReactKeyboardEvent} from 'react';
+import React, {type ForwardedRef, forwardRef, type MouseEventHandler, type KeyboardEvent as ReactKeyboardEvent} from 'react';
import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
@@ -29,7 +29,7 @@ type CheckboxProps = Partial & {
containerStyle?: StyleProp;
/** Callback that is called when mousedown is triggered. */
- onMouseDown?: () => void;
+ onMouseDown?: MouseEventHandler;
/** The size of the checkbox container */
containerSize?: number;
diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx
index b84f9c6a6630..f66a0204ac5e 100644
--- a/src/components/CustomStatusBarAndBackground/index.tsx
+++ b/src/components/CustomStatusBarAndBackground/index.tsx
@@ -1,4 +1,5 @@
import React, {useCallback, useContext, useEffect, useRef, useState} from 'react';
+import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
import useTheme from '@hooks/useTheme';
import {navigationRef} from '@libs/Navigation/Navigation';
import StatusBar from '@libs/StatusBar';
@@ -33,7 +34,27 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
};
}, [disableRootStatusBar, isNested]);
+ const prevStatusBarBackgroundColor = useRef(theme.appBG);
+ const statusBarBackgroundColor = useRef(theme.appBG);
+ const statusBarAnimation = useSharedValue(0);
+
+ useAnimatedReaction(
+ () => statusBarAnimation.value,
+ (current, previous) => {
+ // Do not run if either of the animated value is null
+ // or previous animated value is greater than or equal to the current one
+ if (previous === null || current === null || current <= previous) {
+ return;
+ }
+ const backgroundColor = interpolateColor(statusBarAnimation.value, [0, 1], [prevStatusBarBackgroundColor.current, statusBarBackgroundColor.current]);
+ runOnJS(updateStatusBarAppearance)({backgroundColor});
+ },
+ );
+
const listenerCount = useRef(0);
+
+ // Updates the status bar style and background color depending on the current route and theme
+ // This callback is triggered everytime the route changes or the theme changes
const updateStatusBarStyle = useCallback(
(listenerId?: number) => {
// Check if this function is either called through the current navigation listener or the general useEffect which listens for theme changes.
@@ -49,27 +70,40 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
currentRoute = navigationRef.getCurrentRoute();
}
- let currentScreenBackgroundColor = theme.appBG;
let newStatusBarStyle = theme.statusBarStyle;
+ let currentScreenBackgroundColor = theme.appBG;
if (currentRoute && 'name' in currentRoute && currentRoute.name in theme.PAGE_THEMES) {
- const screenTheme = theme.PAGE_THEMES[currentRoute.name];
- currentScreenBackgroundColor = screenTheme.backgroundColor;
- newStatusBarStyle = screenTheme.statusBarStyle;
+ const pageTheme = theme.PAGE_THEMES[currentRoute.name];
+
+ newStatusBarStyle = pageTheme.statusBarStyle;
+
+ const backgroundColorFromRoute =
+ currentRoute?.params && 'backgroundColor' in currentRoute.params && typeof currentRoute.params.backgroundColor === 'string' && currentRoute.params.backgroundColor;
+
+ // It's possible for backgroundColorFromRoute to be empty string, so we must use "||" to fallback to backgroundColorFallback.
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ currentScreenBackgroundColor = backgroundColorFromRoute || pageTheme.backgroundColor;
+ }
+
+ prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current;
+ statusBarBackgroundColor.current = currentScreenBackgroundColor;
+
+ if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.current !== theme.appBG) {
+ statusBarAnimation.value = 0;
+ statusBarAnimation.value = withDelay(300, withTiming(1));
}
// Don't update the status bar style if it's the same as the current one, to prevent flashing.
- if (newStatusBarStyle === statusBarStyle) {
- updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor});
- } else {
- updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor, statusBarStyle: newStatusBarStyle});
+ if (newStatusBarStyle !== statusBarStyle) {
+ updateStatusBarAppearance({statusBarStyle: newStatusBarStyle});
setStatusBarStyle(newStatusBarStyle);
}
},
- [statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle],
+ [statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle],
);
// Add navigation state listeners to update the status bar every time the route changes
- // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properyl
+ // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properly
useEffect(() => {
if (isDisabled) {
return;
@@ -82,15 +116,6 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack
return () => navigationRef.removeListener('state', listener);
}, [isDisabled, theme.appBG, updateStatusBarStyle]);
- // Update the status bar style everytime the theme changes
- useEffect(() => {
- if (isDisabled) {
- return;
- }
-
- updateStatusBarStyle();
- }, [isDisabled, theme, updateStatusBarStyle]);
-
// Update the global background (on web) everytime the theme changes.
// The background of the html element needs to be updated, otherwise you will see a big contrast when resizing the window or when the keyboard is open on iOS web.
useEffect(() => {
diff --git a/src/components/DistanceMapView/distanceMapViewPropTypes.js b/src/components/DistanceMapView/distanceMapViewPropTypes.js
deleted file mode 100644
index f7a3bab1879e..000000000000
--- a/src/components/DistanceMapView/distanceMapViewPropTypes.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import PropTypes from 'prop-types';
-import sourcePropTypes from '@components/Image/sourcePropTypes';
-
-const propTypes = {
- // Public access token to be used to fetch map data from Mapbox.
- accessToken: PropTypes.string.isRequired,
-
- // Style applied to MapView component. Note some of the View Style props are not available on ViewMap
- style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-
- // Link to the style JSON document.
- styleURL: PropTypes.string,
-
- // Whether map can tilt in the vertical direction.
- pitchEnabled: PropTypes.bool,
-
- // Padding to apply when the map is adjusted to fit waypoints and directions
- mapPadding: PropTypes.number,
-
- // Initial coordinate and zoom level
- initialState: PropTypes.shape({
- location: PropTypes.arrayOf(PropTypes.number).isRequired,
- zoom: PropTypes.number.isRequired,
- }),
-
- // Locations on which to put markers
- waypoints: PropTypes.arrayOf(
- PropTypes.shape({
- id: PropTypes.string,
- coordinate: PropTypes.arrayOf(PropTypes.number),
- markerComponent: sourcePropTypes,
- }),
- ),
-
- // List of coordinates which together forms a direction.
- directionCoordinates: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)),
-
- // Callback to call when the map is idle / ready
- onMapReady: PropTypes.func,
-
- // Optional additional styles to be applied to the overlay
- overlayStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]),
-};
-
-const defaultProps = {
- styleURL: undefined,
- pitchEnabled: false,
- mapPadding: 0,
- initialState: undefined,
- waypoints: undefined,
- directionCoordinates: undefined,
- onMapReady: () => {},
- overlayStyle: undefined,
-};
-
-export {propTypes, defaultProps};
diff --git a/src/components/DistanceMapView/index.android.js b/src/components/DistanceMapView/index.android.tsx
similarity index 73%
rename from src/components/DistanceMapView/index.android.js
rename to src/components/DistanceMapView/index.android.tsx
index fa40bd50673e..38e92163c3eb 100644
--- a/src/components/DistanceMapView/index.android.js
+++ b/src/components/DistanceMapView/index.android.tsx
@@ -1,18 +1,15 @@
import React, {useState} from 'react';
import {View} from 'react-native';
-import _ from 'underscore';
import BlockingView from '@components/BlockingViews/BlockingView';
import * as Expensicons from '@components/Icon/Expensicons';
import MapView from '@components/MapView';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
-import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
-import * as distanceMapViewPropTypes from './distanceMapViewPropTypes';
+import DistanceMapViewProps from './types';
-function DistanceMapView(props) {
+function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) {
const styles = useThemeStyles();
- const StyleUtils = useStyleUtils();
const [isMapReady, setIsMapReady] = useState(false);
const {isOffline} = useNetwork();
const {translate} = useLocalize();
@@ -21,7 +18,7 @@ function DistanceMapView(props) {
<>
{
if (isMapReady) {
return;
@@ -30,7 +27,7 @@ function DistanceMapView(props) {
}}
/>
{!isMapReady && (
-
+
;
-}
-
-DistanceMapView.propTypes = distanceMapViewPropTypes.propTypes;
-DistanceMapView.defaultProps = distanceMapViewPropTypes.defaultProps;
-DistanceMapView.displayName = 'DistanceMapView';
-
-export default DistanceMapView;
diff --git a/src/components/DistanceMapView/index.tsx b/src/components/DistanceMapView/index.tsx
new file mode 100644
index 000000000000..2abdc29865b0
--- /dev/null
+++ b/src/components/DistanceMapView/index.tsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import MapView from '@components/MapView';
+import DistanceMapViewProps from './types';
+
+function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) {
+ // eslint-disable-next-line react/jsx-props-no-spreading
+ return ;
+}
+
+DistanceMapView.displayName = 'DistanceMapView';
+
+export default DistanceMapView;
diff --git a/src/components/DistanceMapView/types.ts b/src/components/DistanceMapView/types.ts
new file mode 100644
index 000000000000..5ab3dbd8238e
--- /dev/null
+++ b/src/components/DistanceMapView/types.ts
@@ -0,0 +1,8 @@
+import {StyleProp, ViewStyle} from 'react-native';
+import type {MapViewProps} from '@components/MapView/MapViewTypes';
+
+type DistanceMapViewProps = MapViewProps & {
+ overlayStyle?: StyleProp;
+};
+
+export default DistanceMapViewProps;
diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts
index 939b2530fa3d..99e93e8d18d2 100644
--- a/src/components/HeaderWithBackButton/types.ts
+++ b/src/components/HeaderWithBackButton/types.ts
@@ -18,7 +18,7 @@ type ThreeDotsMenuItems = {
onSelected: () => void;
};
-type HeaderWithBackButtonProps = ChildrenProps & {
+type HeaderWithBackButtonProps = Partial & {
/** Title of the Header */
title?: string;
diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js
index f75e3390136a..fc4f05eefd22 100644
--- a/src/components/LHNOptionsList/OptionRowLHN.js
+++ b/src/components/LHNOptionsList/OptionRowLHN.js
@@ -135,7 +135,7 @@ function OptionRowLHN(props) {
props.reportID,
'0',
props.reportID,
- '',
+ undefined,
() => {},
() => setIsContextMenuActive(false),
false,
diff --git a/src/components/MapView/index.tsx b/src/components/MapView/index.tsx
index f273845fe4c0..d9da7f1dfea3 100644
--- a/src/components/MapView/index.tsx
+++ b/src/components/MapView/index.tsx
@@ -1,8 +1,8 @@
import React from 'react';
import MapView from './MapView';
-import {ComponentProps} from './types';
+import type {MapViewProps} from './MapViewTypes';
-function MapViewComponent(props: ComponentProps) {
+function MapViewComponent(props: MapViewProps) {
// eslint-disable-next-line react/jsx-props-no-spreading
return ;
}
diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js
index a559e876af18..8ed6d0746438 100644
--- a/src/components/MoneyReportHeader.js
+++ b/src/components/MoneyReportHeader.js
@@ -44,6 +44,9 @@ const propTypes = {
/** The role of the current user in the policy */
role: PropTypes.string,
+
+ /** Whether Scheduled Submit is turned on for this policy */
+ isHarvestingEnabled: PropTypes.bool,
}),
/** The chat report this report is linked to */
@@ -70,7 +73,9 @@ const defaultProps = {
session: {
email: null,
},
- policy: {},
+ policy: {
+ isHarvestingEnabled: false,
+ },
};
function MoneyReportHeader({session, personalDetails, policy, chatReport, nextStep, report: moneyRequestReport, isSmallScreenWidth}) {
@@ -108,6 +113,12 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableTotal, moneyRequestReport.currency);
const isMoreContentShown = shouldShowNextStep || (shouldShowAnyButton && isSmallScreenWidth);
+ // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on
+ const isWaitingForSubmissionFromCurrentUser = useMemo(
+ () => chatReport.isOwnPolicyExpenseChat && !policy.isHarvestingEnabled,
+ [chatReport.isOwnPolicyExpenseChat, policy.isHarvestingEnabled],
+ );
+
const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)];
if (!ReportUtils.isArchivedRoom(chatReport)) {
threeDotsMenuItems.push({
@@ -164,7 +175,7 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt