diff --git a/.eslintrc.js b/.eslintrc.js index b76782af60f4..b71338d0c1a5 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -14,6 +14,11 @@ const restrictedImportPaths = [ importNames: ['TouchableOpacity', 'TouchableWithoutFeedback', 'TouchableNativeFeedback', 'TouchableHighlight'], message: "Please use 'PressableWithFeedback' and/or 'PressableWithoutFeedback' from 'src/components/Pressable' instead.", }, + { + name: 'react-native-safe-area-context', + importNames: ['useSafeAreaInsets', 'SafeAreaConsumer', 'SafeAreaInsetsContext'], + message: "Please use 'useSafeAreaInsets' from 'src/hooks/useSafeAreaInset' and/or 'SafeAreaConsumer' from 'src/components/SafeAreaConsumer' instead.", + }, ]; const restrictedImportPatterns = [ diff --git a/.github/workflows/platformDeploy.yml b/.github/workflows/platformDeploy.yml index bf27006e34a2..8ccc0a7f6056 100644 --- a/.github/workflows/platformDeploy.yml +++ b/.github/workflows/platformDeploy.yml @@ -347,6 +347,9 @@ jobs: if: ${{ failure() }} needs: [android, desktop, iOS, web] steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Post Slack message on failure uses: ./.github/actions/composite/announceFailedWorkflowInSlack with: diff --git a/android/app/build.gradle b/android/app/build.gradle index 28c983297c8b..2e735cbaf419 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -91,8 +91,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001040602 - versionName "1.4.6-2" + versionCode 1001040700 + versionName "1.4.7-0" } flavorDimensions "default" diff --git a/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md b/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md deleted file mode 100644 index 41a1fb96f56f..000000000000 --- a/docs/articles/expensify-classic/billing-and-subscriptions/Payment-Card.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -title: Payment Card -description: Payment Card ---- -## Resource Coming Soon! diff --git a/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md index 4c216faffc18..8bcc11fbf167 100644 --- a/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md +++ b/docs/articles/expensify-classic/expensify-card/Expensify-Card-Perks.md @@ -5,7 +5,30 @@ description: Get the most out of your Expensify Card with exclusive perks! # Overview -The Expensify Card is packed with perks, both native to our Card program and through exclusive discounts with partnering solutions. Below, we’ll cover all of our exclusive offers in more detail and how to claim discounts with our partners. +The Expensify Card is packed with perks, both native to our Card program and through exclusive discounts with partnering solutions. The Expensify Card’s primary perks include: +- Swipe to Win, where every swipe has a chance to win fun personalized gifts for you and your closest friends and family members +- Unbeatable cash back incentive with each swipe +Below, we’ll cover all of our exclusive offers in more detail and how to claim discounts with our partners. + +# Expensify Card Perks + +## Swipe to Win +Swipe to Win is a new [Expensify Card](https://use.expensify.com/company-credit-card) perk that gives cardholders the chance to send a gift to a friend, family member, or essential worker on the frontlines! + +Winners can choose to _Send a Smile_ or _Send a Laugh_. To start, we’re offering one gift per option: + +- **Send A Smile:** Champagne by Expensify +- **Send a Laugh:** Jenga Set + +**How to Participate** +It’s easy! Once you have an Expensify Card, you just need to start using it. With each swipe, you're automatically entered to win and have a 1 in 250 chance of getting a prize! + +**How will I know if I’ve won?** +Winners will be notified immediately via the Expensify app, and receive additional instructions on how to choose and send their desired gift. + +If you don't have Expensify notifications turned on yet, here are some helpful guides: +- [Apple Notification Preferences](https://support.apple.com/en-us/HT201925) +- [Android Notification Preferences](https://community.expensify.com/home/leaving?allowTrusted=1&target=https%3A%2F%2Fsupport.google.com%2Fandroid%2Fanswer%2F9079661%3Fhl%3Den) # Partner Specific Perks @@ -188,4 +211,3 @@ Stripe Atlas helps removes obstacles typically associated with starting a busine **Receive $100 off Stripe Atlas and get access to a startup toolkit and special offers on additional Strip Atlas services.** **How to redeem:** Sign up with your Expensify Card. - diff --git a/docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md b/docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md deleted file mode 100644 index 6b85bb0364b5..000000000000 --- a/docs/articles/expensify-classic/expensify-partner-program/Coming-Soon.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- diff --git a/docs/articles/new-expensify/workspace-and-domain-settings/Coming-Soon.md b/docs/articles/new-expensify/workspace-and-domain-settings/Coming-Soon.md deleted file mode 100644 index 6b85bb0364b5..000000000000 --- a/docs/articles/new-expensify/workspace-and-domain-settings/Coming-Soon.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Coming Soon -description: Coming Soon ---- diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index c05fe7fbfeba..a01a04f71b3c 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.6 + 1.4.7 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.6.2 + 1.4.7.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 4c875f6d7ba2..095122c37ce4 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.6 + 1.4.7 CFBundleSignature ???? CFBundleVersion - 1.4.6.2 + 1.4.7.0 diff --git a/package-lock.json b/package-lock.json index b3a8c47f853c..65173ccec496 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.6-2", + "version": "1.4.7-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.6-2", + "version": "1.4.7-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 6412fd588bde..1693afc40b04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.6-2", + "version": "1.4.7-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", diff --git a/src/CONST.ts b/src/CONST.ts index 5751c588ead8..13b79179f431 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -497,6 +497,7 @@ const CONST = { MODIFIEDEXPENSE: 'MODIFIEDEXPENSE', MOVED: 'MOVED', REIMBURSEMENTQUEUED: 'REIMBURSEMENTQUEUED', + REIMBURSEMENTDEQUEUED: 'REIMBURSEMENTDEQUEUED', RENAMED: 'RENAMED', REPORTPREVIEW: 'REPORTPREVIEW', SUBMITTED: 'SUBMITTED', @@ -1165,6 +1166,9 @@ const CONST = { SVG: 'svg', }, RECEIPT_ERROR: 'receiptError', + CANCEL_REASON: { + PAYMENT_EXPIRED: 'CANCEL_REASON_PAYMENT_EXPIRED', + }, }, GROWL: { diff --git a/src/components/AvatarWithIndicator.js b/src/components/AvatarWithIndicator.tsx similarity index 62% rename from src/components/AvatarWithIndicator.js rename to src/components/AvatarWithIndicator.tsx index f3607b69a73f..3ae9507350c8 100644 --- a/src/components/AvatarWithIndicator.js +++ b/src/components/AvatarWithIndicator.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import * as UserUtils from '@libs/UserUtils'; @@ -9,38 +8,33 @@ import * as Expensicons from './Icon/Expensicons'; import Indicator from './Indicator'; import Tooltip from './Tooltip'; -const propTypes = { +type AvatarWithIndicatorProps = { /** URL for the avatar */ - source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + source: UserUtils.AvatarSource; /** To show a tooltip on hover */ - tooltipText: PropTypes.string, + tooltipText?: string; /** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */ - fallbackIcon: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), + fallbackIcon?: UserUtils.AvatarSource; /** Indicates whether the avatar is loaded or not */ - isLoading: PropTypes.bool, + isLoading?: boolean; }; -const defaultProps = { - tooltipText: '', - fallbackIcon: Expensicons.FallbackAvatar, - isLoading: true, -}; - -function AvatarWithIndicator(props) { +function AvatarWithIndicator({source, tooltipText = '', fallbackIcon = Expensicons.FallbackAvatar, isLoading = true}: AvatarWithIndicatorProps) { const styles = useThemeStyles(); + return ( - + - {props.isLoading ? ( + {isLoading ? ( ) : ( <> @@ -50,8 +44,6 @@ function AvatarWithIndicator(props) { ); } -AvatarWithIndicator.defaultProps = defaultProps; -AvatarWithIndicator.propTypes = propTypes; AvatarWithIndicator.displayName = 'AvatarWithIndicator'; export default AvatarWithIndicator; diff --git a/src/components/CustomStatusBar/index.tsx b/src/components/CustomStatusBar/index.tsx index 2e4994378264..3b5022c60898 100644 --- a/src/components/CustomStatusBar/index.tsx +++ b/src/components/CustomStatusBar/index.tsx @@ -5,6 +5,7 @@ import {navigationRef} from '@libs/Navigation/Navigation'; import StatusBar from '@libs/StatusBar'; import useTheme from '@styles/themes/useTheme'; import CustomStatusBarContext from './CustomStatusBarContext'; +import updateStatusBarAppearance from './updateStatusBarAppearance'; type CustomStatusBarProps = { isNested: boolean; @@ -60,8 +61,7 @@ const CustomStatusBar: CustomStatusBarType = ({isNested = false}) => { statusBarStyle = screenTheme.statusBarStyle; } - StatusBar.setBackgroundColor(currentScreenBackgroundColor, true); - StatusBar.setBarStyle(statusBarStyle, true); + updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor, statusBarStyle}); }, [isDisabled, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle]); useEffect(() => { @@ -75,7 +75,7 @@ const CustomStatusBar: CustomStatusBarType = ({isNested = false}) => { return; } - StatusBar.setBarStyle(theme.statusBarStyle, true); + updateStatusBarAppearance({statusBarStyle: theme.statusBarStyle}); }, [isDisabled, theme.statusBarStyle]); if (isDisabled) { diff --git a/src/components/CustomStatusBar/updateStatusBarAppearance/index.android.ts b/src/components/CustomStatusBar/updateStatusBarAppearance/index.android.ts new file mode 100644 index 000000000000..b7651d4549de --- /dev/null +++ b/src/components/CustomStatusBar/updateStatusBarAppearance/index.android.ts @@ -0,0 +1,11 @@ +import StatusBar from '@libs/StatusBar'; +import UpdateStatusBarAppearanceProps from './types'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function updateStatusBarAppearance({statusBarStyle}: UpdateStatusBarAppearanceProps) { + StatusBar.setBackgroundColor('transparent'); + StatusBar.setTranslucent(true); + if (statusBarStyle) { + StatusBar.setBarStyle(statusBarStyle, true); + } +} diff --git a/src/components/CustomStatusBar/updateStatusBarAppearance/index.ios.ts b/src/components/CustomStatusBar/updateStatusBarAppearance/index.ios.ts new file mode 100644 index 000000000000..61fcb056bba5 --- /dev/null +++ b/src/components/CustomStatusBar/updateStatusBarAppearance/index.ios.ts @@ -0,0 +1,10 @@ +import StatusBar from '@libs/StatusBar'; +import UpdateStatusBarAppearanceProps from './types'; + +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function updateStatusBarAppearance({statusBarStyle}: UpdateStatusBarAppearanceProps) { + if (!statusBarStyle) { + return; + } + StatusBar.setBarStyle(statusBarStyle, true); +} diff --git a/src/components/CustomStatusBar/updateStatusBarAppearance/index.ts b/src/components/CustomStatusBar/updateStatusBarAppearance/index.ts new file mode 100644 index 000000000000..574efd90f0b5 --- /dev/null +++ b/src/components/CustomStatusBar/updateStatusBarAppearance/index.ts @@ -0,0 +1,11 @@ +import StatusBar from '@libs/StatusBar'; +import UpdateStatusBarAppearanceProps from './types'; + +export default function updateStatusBarAppearance({backgroundColor, statusBarStyle}: UpdateStatusBarAppearanceProps) { + if (backgroundColor) { + StatusBar.setBackgroundColor(backgroundColor, true); + } + if (statusBarStyle) { + StatusBar.setBarStyle(statusBarStyle, true); + } +} diff --git a/src/components/CustomStatusBar/updateStatusBarAppearance/types.ts b/src/components/CustomStatusBar/updateStatusBarAppearance/types.ts new file mode 100644 index 000000000000..3d16b5944a31 --- /dev/null +++ b/src/components/CustomStatusBar/updateStatusBarAppearance/types.ts @@ -0,0 +1,8 @@ +import {StatusBarStyle} from '@styles/styles'; + +type UpdateStatusBarAppearanceProps = { + backgroundColor?: string; + statusBarStyle?: StatusBarStyle; +}; + +export default UpdateStatusBarAppearanceProps; diff --git a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js index 4a2fa28082b6..1b59219f38be 100644 --- a/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js +++ b/src/components/GrowlNotification/GrowlNotificationContainer/index.native.js @@ -1,6 +1,6 @@ import React from 'react'; import {Animated} from 'react-native'; -import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import * as StyleUtils from '@styles/StyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import growlNotificationContainerPropTypes from './growlNotificationContainerPropTypes'; @@ -11,14 +11,12 @@ const propTypes = { function GrowlNotificationContainer(props) { const styles = useThemeStyles(); + const insets = useSafeAreaInsets; + return ( - - {(insets) => ( - - {props.children} - - )} - + + {props.children} + ); } diff --git a/src/components/LHNOptionsList/LHNOptionsList.js b/src/components/LHNOptionsList/LHNOptionsList.js index accf26cac6c1..86de188f9db8 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.js +++ b/src/components/LHNOptionsList/LHNOptionsList.js @@ -118,7 +118,9 @@ function LHNOptionsList({ const transactionID = lodashGet(itemParentReportAction, ['originalMessage', 'IOUTransactionID'], ''); const itemTransaction = transactionID ? transactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] : {}; const itemComment = draftComments[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] || ''; - const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(ReportUtils.getParticipantsIDs(itemFullReport), personalDetails); + const participants = [ReportUtils.getParticipantsIDs(itemFullReport), itemFullReport.ownerAccountID]; + + const participantsPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, personalDetails); return ( & { /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ fullscreen?: boolean; diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js index fd162c78fe2c..1c785d4b363f 100644 --- a/src/components/PopoverWithoutOverlay/index.js +++ b/src/components/PopoverWithoutOverlay/index.js @@ -1,9 +1,9 @@ -import React from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; -import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; import {defaultProps, propTypes} from '@components/Popover/popoverPropTypes'; import {PopoverContext} from '@components/PopoverProvider'; import withWindowDimensions from '@components/withWindowDimensions'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import getModalStyles from '@styles/getModalStyles'; import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; @@ -14,6 +14,7 @@ function Popover(props) { const theme = useTheme(); const styles = useThemeStyles(); const {onOpen, close} = React.useContext(PopoverContext); + const insets = useSafeAreaInsets(); const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = getModalStyles( 'popover', { @@ -28,6 +29,47 @@ function Popover(props) { props.outerStyle, ); + const { + paddingTop: safeAreaPaddingTop, + paddingBottom: safeAreaPaddingBottom, + paddingLeft: safeAreaPaddingLeft, + paddingRight: safeAreaPaddingRight, + } = useMemo(() => StyleUtils.getSafeAreaPadding(insets), [insets]); + + const modalPaddingStyles = useMemo( + () => + StyleUtils.getModalPaddingStyles({ + safeAreaPaddingTop, + safeAreaPaddingBottom, + safeAreaPaddingLeft, + safeAreaPaddingRight, + shouldAddBottomSafeAreaMargin, + shouldAddTopSafeAreaMargin, + shouldAddBottomSafeAreaPadding, + shouldAddTopSafeAreaPadding, + modalContainerStyleMarginTop: modalContainerStyle.marginTop, + modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, + modalContainerStylePaddingTop: modalContainerStyle.paddingTop, + modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, + insets, + }), + [ + insets, + modalContainerStyle.marginBottom, + modalContainerStyle.marginTop, + modalContainerStyle.paddingBottom, + modalContainerStyle.paddingTop, + safeAreaPaddingBottom, + safeAreaPaddingLeft, + safeAreaPaddingRight, + safeAreaPaddingTop, + shouldAddBottomSafeAreaMargin, + shouldAddBottomSafeAreaPadding, + shouldAddTopSafeAreaMargin, + shouldAddTopSafeAreaPadding, + ], + ); + React.useEffect(() => { let removeOnClose; if (props.isVisible) { @@ -64,44 +106,16 @@ function Popover(props) { style={[modalStyle, {zIndex: 1}]} ref={props.withoutOverlayRef} > - - {(insets) => { - const { - paddingTop: safeAreaPaddingTop, - paddingBottom: safeAreaPaddingBottom, - paddingLeft: safeAreaPaddingLeft, - paddingRight: safeAreaPaddingRight, - } = StyleUtils.getSafeAreaPadding(insets); - - const modalPaddingStyles = StyleUtils.getModalPaddingStyles({ - safeAreaPaddingTop, - safeAreaPaddingBottom, - safeAreaPaddingLeft, - safeAreaPaddingRight, - shouldAddBottomSafeAreaMargin, - shouldAddTopSafeAreaMargin, - shouldAddBottomSafeAreaPadding, - shouldAddTopSafeAreaPadding, - modalContainerStyleMarginTop: modalContainerStyle.marginTop, - modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, - modalContainerStylePaddingTop: modalContainerStyle.paddingTop, - modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, - insets, - }); - return ( - - {props.children} - - ); + + ref={props.forwardedRef} + > + {props.children} + ); } diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 466a5a6eec51..ce1c248962aa 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -220,6 +220,8 @@ function MoneyRequestPreview(props) { message += ` • ${props.translate('iou.approved')}`; } else if (props.iouReport.isWaitingOnBankAccount) { message += ` • ${props.translate('iou.pending')}`; + } else if (props.iouReport.isCancelledIOU) { + message += ` • ${props.translate('iou.canceled')}`; } return message; }; @@ -280,9 +282,9 @@ function MoneyRequestPreview(props) { - {getPreviewHeaderText() + (isSettled ? ` • ${getSettledMessage()}` : '')} + {getPreviewHeaderText() + (isSettled && !props.iouReport.isCancelledIOU ? ` • ${getSettledMessage()}` : '')} - {hasFieldErrors && ( + {!isSettled && hasFieldErrors && ( {getPreviewMessage()} - {hasErrors && ( + {!iouSettled && hasErrors && ( + {(insets) => { + const insetsWithDefault = insets ?? { + top: 0, + bottom: 0, + left: 0, + right: 0, + }; + + const androidInsets = { + ...insetsWithDefault, + top: StatusBar.currentHeight ?? insetsWithDefault.top, + }; + + const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(androidInsets ?? undefined); + return children({ + paddingTop, + paddingBottom, + insets: androidInsets ?? undefined, + safeAreaPaddingBottomStyle: {paddingBottom}, + }); + }} + + ); +} + +SafeAreaConsumer.displayName = 'SafeAreaConsumer'; + +export default SafeAreaConsumer; diff --git a/src/components/SafeAreaConsumer.tsx b/src/components/SafeAreaConsumer/index.tsx similarity index 69% rename from src/components/SafeAreaConsumer.tsx rename to src/components/SafeAreaConsumer/index.tsx index 7df73dbdb65f..f515d6697bb4 100644 --- a/src/components/SafeAreaConsumer.tsx +++ b/src/components/SafeAreaConsumer/index.tsx @@ -1,20 +1,8 @@ import React from 'react'; -import type {DimensionValue} from 'react-native'; -import {EdgeInsets, SafeAreaInsetsContext} from 'react-native-safe-area-context'; +// eslint-disable-next-line no-restricted-imports +import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; import * as StyleUtils from '@styles/StyleUtils'; - -type ChildrenProps = { - paddingTop?: DimensionValue; - paddingBottom?: DimensionValue; - insets?: EdgeInsets; - safeAreaPaddingBottomStyle: { - paddingBottom?: DimensionValue; - }; -}; - -type SafeAreaConsumerProps = { - children: React.FC; -}; +import SafeAreaConsumerProps from './types'; /** * This component is a light wrapper around the SafeAreaInsetsContext.Consumer. There are several places where we diff --git a/src/components/SafeAreaConsumer/types.ts b/src/components/SafeAreaConsumer/types.ts new file mode 100644 index 000000000000..bc81de96a082 --- /dev/null +++ b/src/components/SafeAreaConsumer/types.ts @@ -0,0 +1,17 @@ +import {DimensionValue} from 'react-native'; +import {EdgeInsets} from 'react-native-safe-area-context'; + +type ChildrenProps = { + paddingTop?: DimensionValue; + paddingBottom?: DimensionValue; + insets?: EdgeInsets; + safeAreaPaddingBottomStyle: { + paddingBottom?: DimensionValue; + }; +}; + +type SafeAreaConsumerProps = { + children: React.FC; +}; + +export default SafeAreaConsumerProps; diff --git a/src/components/TestToolMenu.js b/src/components/TestToolMenu.tsx similarity index 71% rename from src/components/TestToolMenu.js rename to src/components/TestToolMenu.tsx index fd71b6cee75b..53953b861d7d 100644 --- a/src/components/TestToolMenu.js +++ b/src/components/TestToolMenu.tsx @@ -1,7 +1,5 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React from 'react'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import * as ApiUtils from '@libs/ApiUtils'; import compose from '@libs/compose'; import useThemeStyles from '@styles/useThemeStyles'; @@ -10,34 +8,30 @@ import * as Session from '@userActions/Session'; import * as User from '@userActions/User'; import CONFIG from '@src/CONFIG'; import ONYXKEYS from '@src/ONYXKEYS'; +import NetworkOnyx from '@src/types/onyx/Network'; +import UserOnyx from '@src/types/onyx/User'; import Button from './Button'; -import networkPropTypes from './networkPropTypes'; import {withNetwork} from './OnyxProvider'; import Switch from './Switch'; import TestToolRow from './TestToolRow'; import Text from './Text'; -const propTypes = { +type TestToolMenuOnyxProps = { /** User object in Onyx */ - user: PropTypes.shape({ - /** Whether we should use the staging version of the secure API server */ - shouldUseStagingServer: PropTypes.bool, - }), + user: OnyxEntry; +}; +type TestToolMenuProps = TestToolMenuOnyxProps & { /** Network object in Onyx */ - network: networkPropTypes.isRequired, + network: OnyxEntry; }; -const defaultProps = { - user: { - // The default value is environment specific and can't be set with `defaultProps` (ENV is not resolved yet) - // When undefined (during render) STAGING defaults to `true`, other envs default to `false` - shouldUseStagingServer: undefined, - }, -}; +const USER_DEFAULT: UserOnyx = {shouldUseStagingServer: undefined, isSubscribedToNewsletter: false, validated: false, isFromPublicDomain: false, isUsingExpensifyCard: false}; -function TestToolMenu(props) { +function TestToolMenu({user = USER_DEFAULT, network}: TestToolMenuProps) { + const shouldUseStagingServer = user?.shouldUseStagingServer ?? ApiUtils.isUsingStagingApi(); const styles = useThemeStyles(); + return ( <> User.setShouldUseStagingServer(!lodashGet(props, 'user.shouldUseStagingServer', ApiUtils.isUsingStagingApi()))} + isOn={shouldUseStagingServer} + onToggle={() => User.setShouldUseStagingServer(!shouldUseStagingServer)} /> )} @@ -64,8 +58,8 @@ function TestToolMenu(props) { Network.setShouldForceOffline(!props.network.shouldForceOffline)} + isOn={!!network?.shouldForceOffline} + onToggle={() => Network.setShouldForceOffline(!network?.shouldForceOffline)} /> @@ -73,8 +67,8 @@ function TestToolMenu(props) { Network.setShouldFailAllRequests(!props.network.shouldFailAllRequests)} + isOn={!!network?.shouldFailAllRequests} + onToggle={() => Network.setShouldFailAllRequests(!network?.shouldFailAllRequests)} /> @@ -99,15 +93,13 @@ function TestToolMenu(props) { ); } -TestToolMenu.propTypes = propTypes; -TestToolMenu.defaultProps = defaultProps; TestToolMenu.displayName = 'TestToolMenu'; export default compose( - withNetwork(), - withOnyx({ + withOnyx({ user: { key: ONYXKEYS.USER, }, }), + withNetwork(), )(TestToolMenu); diff --git a/src/components/TestToolsModal.js b/src/components/TestToolsModal.tsx similarity index 56% rename from src/components/TestToolsModal.js rename to src/components/TestToolsModal.tsx index 334090d98334..f555c2b0b4fe 100644 --- a/src/components/TestToolsModal.js +++ b/src/components/TestToolsModal.tsx @@ -1,7 +1,6 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import useThemeStyles from '@styles/useThemeStyles'; import toggleTestToolsModal from '@userActions/TestTool'; import CONST from '@src/CONST'; @@ -9,27 +8,19 @@ import ONYXKEYS from '@src/ONYXKEYS'; import Modal from './Modal'; import TestToolMenu from './TestToolMenu'; -const propTypes = { - /** Details about modal */ - modal: PropTypes.shape({ - /** Indicates when an Alert modal is about to be visible */ - willAlertModalBecomeVisible: PropTypes.bool, - }), - +type TestToolsModalOnyxProps = { /** Whether the test tools modal is open */ - isTestToolsModalOpen: PropTypes.bool, + isTestToolsModalOpen: OnyxEntry; }; -const defaultProps = { - modal: {}, - isTestToolsModalOpen: false, -}; +type TestToolsModalProps = TestToolsModalOnyxProps; -function TestToolsModal(props) { +function TestToolsModal({isTestToolsModalOpen = false}: TestToolsModalProps) { const styles = useThemeStyles(); + return ( @@ -40,14 +31,9 @@ function TestToolsModal(props) { ); } -TestToolsModal.propTypes = propTypes; -TestToolsModal.defaultProps = defaultProps; TestToolsModal.displayName = 'TestToolsModal'; -export default withOnyx({ - modal: { - key: ONYXKEYS.MODAL, - }, +export default withOnyx({ isTestToolsModalOpen: { key: ONYXKEYS.IS_TEST_TOOLS_MODAL_OPEN, }, diff --git a/src/components/withWindowDimensions/index.native.tsx b/src/components/withWindowDimensions/index.native.tsx index 0c9f61a45c0b..8ba385f72e4b 100644 --- a/src/components/withWindowDimensions/index.native.tsx +++ b/src/components/withWindowDimensions/index.native.tsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import React, {ComponentType, createContext, ForwardedRef, RefAttributes, useEffect, useMemo, useState} from 'react'; import {Dimensions} from 'react-native'; -import {useSafeAreaInsets} from 'react-native-safe-area-context'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import getWindowHeightAdjustment from '@libs/getWindowHeightAdjustment'; import variables from '@styles/variables'; diff --git a/src/components/withWindowDimensions/index.tsx b/src/components/withWindowDimensions/index.tsx index 1479450deec4..aa9fc2181a36 100644 --- a/src/components/withWindowDimensions/index.tsx +++ b/src/components/withWindowDimensions/index.tsx @@ -2,7 +2,7 @@ import lodashDebounce from 'lodash/debounce'; import PropTypes from 'prop-types'; import React, {ComponentType, createContext, ForwardedRef, RefAttributes, useEffect, useMemo, useState} from 'react'; import {Dimensions} from 'react-native'; -import {useSafeAreaInsets} from 'react-native-safe-area-context'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import getWindowHeightAdjustment from '@libs/getWindowHeightAdjustment'; import variables from '@styles/variables'; diff --git a/src/hooks/useSafeAreaInsets/index.android.ts b/src/hooks/useSafeAreaInsets/index.android.ts new file mode 100644 index 000000000000..55a83d425543 --- /dev/null +++ b/src/hooks/useSafeAreaInsets/index.android.ts @@ -0,0 +1,14 @@ +// eslint-disable-next-line no-restricted-imports +import {EdgeInsets, useSafeAreaInsets as useSafeAreaInsetsInternal} from 'react-native-safe-area-context'; +import StatusBar from '@libs/StatusBar'; + +function useSafeAreaInsets(): EdgeInsets { + const insets = useSafeAreaInsetsInternal(); + + return { + ...insets, + top: StatusBar.currentHeight ?? insets.top, + }; +} + +export default useSafeAreaInsets; diff --git a/src/hooks/useSafeAreaInsets/index.ts b/src/hooks/useSafeAreaInsets/index.ts new file mode 100644 index 000000000000..6cc2f1818fe5 --- /dev/null +++ b/src/hooks/useSafeAreaInsets/index.ts @@ -0,0 +1,4 @@ +/* eslint-disable no-restricted-imports */ +import {useSafeAreaInsets} from 'react-native-safe-area-context'; + +export default useSafeAreaInsets; diff --git a/src/languages/en.ts b/src/languages/en.ts index 2d7af9236e56..8f772f1260bb 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9,6 +9,7 @@ import type { BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, BeginningOfChatHistoryDomainRoomPartOneParams, + CanceledRequestParams, CharacterLimitParams, ConfirmThatParams, DateShouldBeAfterParams, @@ -542,6 +543,7 @@ export default { pay: 'Pay', viewDetails: 'View details', pending: 'Pending', + canceled: 'Canceled', posted: 'Posted', deleteReceipt: 'Delete receipt', receiptScanning: 'Receipt scan in progress…', @@ -572,6 +574,8 @@ export default { managerApproved: ({manager}: ManagerApprovedParams) => `${manager} approved:`, payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`, waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, + canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => + `Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} added a bank account. The ${amount} payment has been made.`, paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer} paid ${amount} elsewhere`, diff --git a/src/languages/es.ts b/src/languages/es.ts index b84fa57b4470..3887891299df 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8,6 +8,7 @@ import type { BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, BeginningOfChatHistoryDomainRoomPartOneParams, + CanceledRequestParams, CharacterLimitParams, ConfirmThatParams, DateShouldBeAfterParams, @@ -534,6 +535,7 @@ export default { pay: 'Pagar', viewDetails: 'Ver detalles', pending: 'Pendiente', + canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', receiptScanning: 'Escaneo de recibo en curso…', @@ -564,6 +566,8 @@ export default { managerApproved: ({manager}: ManagerApprovedParams) => `${manager} aprobó:`, payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`, waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`, + canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => + `Canceló el pago ${amount}, porque ${submitterDisplayName} no habilitó su billetera Expensify en un plazo de 30 días.`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => `${submitterDisplayName} añadió una cuenta bancaria. El pago de ${amount} se ha realizado.`, paidElsewhereWithAmount: ({payer, amount}: PaidElsewhereWithAmountParams) => `${payer} pagó ${amount} de otra forma`, diff --git a/src/languages/types.ts b/src/languages/types.ts index a012ebdfb95b..96dd85dfb627 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -125,6 +125,8 @@ type PayerSettledParams = {amount: number | string}; type WaitingOnBankAccountParams = {submitterDisplayName: string}; +type CanceledRequestParams = {amount: string; submitterDisplayName: string}; + type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string}; type PaidElsewhereWithAmountParams = {payer: string; amount: string}; @@ -282,6 +284,7 @@ export type { ManagerApprovedParams, PayerSettledParams, WaitingOnBankAccountParams, + CanceledRequestParams, SettledAfterAddedBankAccountParams, PaidElsewhereWithAmountParams, PaidWithExpensifyWithAmountParams, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index c616587c3983..820339ebc6c4 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -350,11 +350,11 @@ function getAllReportErrors(report, reportActions) { if (parentReportAction.actorAccountID === currentUserAccountID && ReportActionUtils.isTransactionThread(parentReportAction)) { const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], ''); const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] || {}; - if (TransactionUtils.hasMissingSmartscanFields(transaction)) { + if (TransactionUtils.hasMissingSmartscanFields(transaction) && !ReportUtils.isSettled(transaction.reportID)) { _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); } } else if ((ReportUtils.isIOUReport(report) || ReportUtils.isExpenseReport(report)) && report.ownerAccountID === currentUserAccountID) { - if (ReportUtils.hasMissingSmartscanFields(report.reportID)) { + if (ReportUtils.hasMissingSmartscanFields(report.reportID) && !ReportUtils.isSettled(report.reportID)) { _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); } } @@ -397,6 +397,8 @@ function getLastMessageTextForReport(report) { lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(iouReport, lastIOUMoneyReport, true, ReportUtils.isChatReport(report)); } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); + } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) { + lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(report); } else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) { lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction); } else if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) { diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index bd475a57954e..f58021e17064 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -109,6 +109,10 @@ function isChannelLogMemberAction(reportAction: OnyxEntry) { ); } +function isReimbursementDeQueuedAction(reportAction: OnyxEntry): boolean { + return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED; +} + /** * Returns whether the comment is a thread parent message/the first message in a thread */ @@ -698,6 +702,7 @@ export { hasRequestFromCurrentAccount, getFirstVisibleReportActionID, isChannelLogMemberAction, + isReimbursementDeQueuedAction, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 599963b6a9aa..f6c3090143f4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1489,6 +1489,16 @@ function getReimbursementQueuedActionMessage(reportAction: OnyxEntry): string { + const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? ''; + const amount = CurrencyUtils.convertToDisplayString(report?.total ?? 0, report?.currency); + + return Localize.translateLocal('iou.canceledRequest', {submitterDisplayName, amount}); +} + /** * Returns the last visible message for a given report after considering the given optimistic actions * @@ -1692,6 +1702,10 @@ function getMoneyRequestReportName(report: OnyxEntry, policy: OnyxEntry< return `${payerPaidAmountMessage} • ${Localize.translateLocal('iou.pending')}`; } + if (report?.isCancelledIOU) { + return `${payerPaidAmountMessage} • ${Localize.translateLocal('iou.canceled')}`; + } + if (hasNonReimbursableTransactions(report?.reportID)) { return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName, amount: formattedAmount}); } @@ -4404,6 +4418,7 @@ export { shouldUseFullTitleToDisplay, parseReportRouteParams, getReimbursementQueuedActionMessage, + getReimbursementDeQueuedActionMessage, getPersonalDetailsForAccountID, getChannelLogMemberMessage, getRoom, diff --git a/src/libs/UserUtils.ts b/src/libs/UserUtils.ts index d091cca57df4..29a43bcf711d 100644 --- a/src/libs/UserUtils.ts +++ b/src/libs/UserUtils.ts @@ -105,22 +105,22 @@ function getDefaultAvatarURL(accountID: string | number = '', isNewDot = false): /** * Given a user's avatar path, returns true if user doesn't have an avatar or if URL points to a default avatar - * @param [avatarURL] - the avatar source from user's personalDetails + * @param avatarSource - the avatar source from user's personalDetails */ -function isDefaultAvatar(avatarURL?: string | React.FC): boolean { - if (typeof avatarURL === 'string') { - if (avatarURL.includes('images/avatars/avatar_') || avatarURL.includes('images/avatars/default-avatar_') || avatarURL.includes('images/avatars/user/default')) { +function isDefaultAvatar(avatarSource?: AvatarSource): boolean { + if (typeof avatarSource === 'string') { + if (avatarSource.includes('images/avatars/avatar_') || avatarSource.includes('images/avatars/default-avatar_') || avatarSource.includes('images/avatars/user/default')) { return true; } // We use a hardcoded "default" Concierge avatar - if (avatarURL === CONST.CONCIERGE_ICON_URL_2021 || avatarURL === CONST.CONCIERGE_ICON_URL) { + if (avatarSource === CONST.CONCIERGE_ICON_URL_2021 || avatarSource === CONST.CONCIERGE_ICON_URL) { return true; } } - if (!avatarURL) { - // If null URL, we should also use a default avatar + if (!avatarSource) { + // If null source, we should also use a default avatar return true; } @@ -128,14 +128,14 @@ function isDefaultAvatar(avatarURL?: string | React.FC): boolean { } /** - * Provided a source URL, if source is a default avatar, return the associated SVG. - * Otherwise, return the URL pointing to a user-uploaded avatar. + * Provided an avatar source, if source is a default avatar, return the associated SVG. + * Otherwise, return the URL or SVG pointing to the user-uploaded avatar. * - * @param avatarURL - the avatar source from user's personalDetails + * @param avatarSource - the avatar source from user's personalDetails * @param accountID - the accountID of the user */ -function getAvatar(avatarURL: AvatarSource, accountID: number): AvatarSource { - return isDefaultAvatar(avatarURL) ? getDefaultAvatar(accountID) : avatarURL; +function getAvatar(avatarSource: AvatarSource, accountID?: number): AvatarSource { + return isDefaultAvatar(avatarSource) ? getDefaultAvatar(accountID) : avatarSource; } /** @@ -153,8 +153,8 @@ function getAvatarUrl(avatarURL: string, accountID: number): string { * Avatars uploaded by users will have a _128 appended so that the asset server returns a small version. * This removes that part of the URL so the full version of the image can load. */ -function getFullSizeAvatar(avatarURL: string, accountID: number): AvatarSource { - const source = getAvatar(avatarURL, accountID); +function getFullSizeAvatar(avatarSource: AvatarSource, accountID: number): AvatarSource { + const source = getAvatar(avatarSource, accountID); if (typeof source !== 'string') { return source; } @@ -165,8 +165,8 @@ function getFullSizeAvatar(avatarURL: string, accountID: number): AvatarSource { * Small sized avatars end with _128.. This adds the _128 at the end of the * source URL (before the file type) if it doesn't exist there already. */ -function getSmallSizeAvatar(avatarURL: string, accountID: number): AvatarSource { - const source = getAvatar(avatarURL, accountID); +function getSmallSizeAvatar(avatarSource: AvatarSource, accountID?: number): AvatarSource { + const source = getAvatar(avatarSource, accountID); if (typeof source !== 'string') { return source; } diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 442276b19a0b..88583ab15529 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -1,5 +1,5 @@ import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; import {ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -9,6 +9,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItem from '@components/MenuItem'; import MultipleAvatars from '@components/MultipleAvatars'; +import {withNetwork} from '@components/OnyxProvider'; import ParentNavigationSubtitle from '@components/ParentNavigationSubtitle'; import participantPropTypes from '@components/participantPropTypes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; @@ -76,6 +77,17 @@ function ReportDetailsPage(props) { const isGroupDMChat = useMemo(() => ReportUtils.isDM(props.report) && participants.length > 1, [props.report, participants.length]); + const isPrivateNotesFetchTriggered = !_.isUndefined(props.report.isLoadingPrivateNotes); + + useEffect(() => { + // Do not fetch private notes if isLoadingPrivateNotes is already defined, or if network is offline. + if (isPrivateNotesFetchTriggered || props.network.isOffline) { + return; + } + + Report.getReportPrivateNote(props.report.reportID); + }, [props.report.reportID, props.network.isOffline, isPrivateNotesFetchTriggered]); + const menuItems = useMemo(() => { const items = []; @@ -249,6 +261,7 @@ ReportDetailsPage.defaultProps = defaultProps; export default compose( withLocalize, withReportOrNotFound(), + withNetwork(), withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, diff --git a/src/pages/ShareCodePage.js b/src/pages/ShareCodePage.js index d81593fb552a..c63db694159f 100644 --- a/src/pages/ShareCodePage.js +++ b/src/pages/ShareCodePage.js @@ -108,12 +108,6 @@ class ShareCodePage extends React.Component { onPress={() => Clipboard.setString(url)} /> - Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE))} - /> - {isNative && ( this.qrCodeRef.current?.download()} /> )} + + Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SHARE_CODE))} + /> diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index f3075cb6b698..ea48f9cc931e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -489,7 +489,7 @@ function ComposerWithSuggestions({ // We want to focus or refocus the input when a modal has been closed or the underlying screen is refocused. // We avoid doing this on native platforms since the software keyboard popping // open creates a jarring and broken UX. - if (!(willBlurTextInputOnTapOutside && !isNextModalWillOpenRef.current && !modal.isVisible && isFocused && (prevIsModalVisible || !prevIsFocused))) { + if (!((willBlurTextInputOnTapOutside || shouldAutoFocus) && !isNextModalWillOpenRef.current && !modal.isVisible && isFocused && (prevIsModalVisible || !prevIsFocused))) { return; } @@ -498,7 +498,8 @@ function ComposerWithSuggestions({ return; } focus(true); - }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal.isVisible, isNextModalWillOpenRef]); + }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal.isVisible, isNextModalWillOpenRef, shouldAutoFocus]); + useEffect(() => { // Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit updateMultilineInputRange(textInputRef.current, shouldAutoFocus); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 85f016b79f7f..2b073d7fee34 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -34,6 +34,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import usePrevious from '@hooks/usePrevious'; import compose from '@libs/compose'; import ControlSelection from '@libs/ControlSelection'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import focusTextInputAfterAnimation from '@libs/focusTextInputAfterAnimation'; import Navigation from '@libs/Navigation/Navigation'; @@ -415,6 +416,11 @@ function ReportActionItem(props) { ); + } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [props.report.ownerAccountID, 'displayName']); + const amount = CurrencyUtils.convertToDisplayString(props.report.total, props.report.currency); + + children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { children = ; } else { diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index 639d6a24f2b4..6676dc99b911 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -3,11 +3,11 @@ import PropTypes from 'prop-types'; import React, {useEffect, useRef, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import {useSafeAreaInsets} from 'react-native-safe-area-context'; import _ from 'underscore'; import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import CustomStatusBar from '@components/CustomStatusBar'; import useLocalize from '@hooks/useLocalize'; +import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as Localize from '@libs/Localize'; diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 0dc532ebeded..f76fbd5ffd7d 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -197,6 +197,11 @@ type OriginalMessageReimbursementQueued = { originalMessage: unknown; }; +type OriginalMessageReimbursementDequeued = { + actionName: typeof CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED; + originalMessage: unknown; +}; + type OriginalMessageMoved = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.MOVED; originalMessage: { @@ -222,6 +227,7 @@ type OriginalMessage = | OriginalMessagePolicyTask | OriginalMessageModifiedExpense | OriginalMessageReimbursementQueued + | OriginalMessageReimbursementDequeued | OriginalMessageMoved; export default OriginalMessage; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 2b84f3946bc3..75c79358e510 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -118,6 +118,9 @@ type Report = { /** Whether the report is waiting on a bank account */ isWaitingOnBankAccount?: boolean; + /** Whether the report is cancelled */ + isCancelledIOU?: boolean; + /** Whether the last message was deleted */ isLastMessageDeletedParentAction?: boolean; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 891a0ffcb7b8..64e1eb0b7c88 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,5 +1,5 @@ -import {SvgProps} from 'react-native-svg'; import {ValueOf} from 'type-fest'; +import {AvatarSource} from '@libs/UserUtils'; import CONST from '@src/CONST'; import * as OnyxCommon from './OnyxCommon'; import OriginalMessage, {Decision, Reaction} from './OriginalMessage'; @@ -85,7 +85,7 @@ type ReportActionBase = { /** accountIDs of the people to which the whisper was sent to (if any). Returns empty array if it is not a whisper */ whisperedToAccountIDs?: number[]; - avatar?: string | React.FC; + avatar?: AvatarSource; automatic?: boolean; @@ -145,4 +145,4 @@ type ReportAction = ReportActionBase & OriginalMessage; type ReportActions = Record; export default ReportAction; -export type {ReportActions, Message}; +export type {Message, ReportActions}; diff --git a/tests/unit/CalendarPickerTest.js b/tests/unit/CalendarPickerTest.js index 92092acc4cd7..4e564b970832 100644 --- a/tests/unit/CalendarPickerTest.js +++ b/tests/unit/CalendarPickerTest.js @@ -1,5 +1,5 @@ import {fireEvent, render, within} from '@testing-library/react-native'; -import {addYears, subYears} from 'date-fns'; +import {addMonths, addYears, subYears} from 'date-fns'; import CalendarPicker from '../../src/components/NewDatePicker/CalendarPicker'; import CONST from '../../src/CONST'; import DateUtils from '../../src/libs/DateUtils'; @@ -64,7 +64,7 @@ describe('CalendarPicker', () => { fireEvent.press(getByTestId('next-month-arrow')); - const nextMonth = new Date().getMonth() + 1; + const nextMonth = addMonths(new Date(), 1).getMonth(); expect(getByText(monthNames[nextMonth])).toBeTruthy(); });