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/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/Modal/types.ts b/src/components/Modal/types.ts index ebe55d9b7469..172965b45fb2 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -12,7 +12,7 @@ type PopoverAnchorPosition = { }; type BaseModalProps = WindowDimensionsProps & - ModalProps & { + Partial & { /** Decides whether the modal should cover fullscreen. FullScreen modal has backdrop */ fullscreen?: boolean; diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 466a5a6eec51..1855152ef640 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,7 +282,7 @@ function MoneyRequestPreview(props) { - {getPreviewHeaderText() + (isSettled ? ` • ${getSettledMessage()}` : '')} + {getPreviewHeaderText() + (isSettled && !props.iouReport.isCancelledIOU ? ` • ${getSettledMessage()}` : '')} {hasFieldErrors && ( ; +}; +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/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..e3b6ec77380e 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -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/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/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(); });