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