diff --git a/android/app/build.gradle b/android/app/build.gradle index 162147aeff0c..18ef2984f67a 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001042407 - versionName "1.4.24-7" + versionCode 1001042500 + versionName "1.4.25-0" } flavorDimensions "default" diff --git a/assets/images/new-expensify.svg b/assets/images/new-expensify.svg index 38276ecd9385..89102ecbc5e4 100644 --- a/assets/images/new-expensify.svg +++ b/assets/images/new-expensify.svg @@ -1 +1 @@ - \ No newline at end of file + diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 7081805db569..b4056989a1c4 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.24 + 1.4.25 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.24.7 + 1.4.25.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 20d4ea1a4820..4f75315fb1ec 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.24 + 1.4.25 CFBundleSignature ???? CFBundleVersion - 1.4.24.7 + 1.4.25.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index f941edc1100e..a0328855047b 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -3,9 +3,9 @@ CFBundleShortVersionString - 1.4.24 + 1.4.25 CFBundleVersion - 1.4.24.7 + 1.4.25.0 NSExtension NSExtensionPointIdentifier diff --git a/ios/Podfile.lock b/ios/Podfile.lock index acc8720dafce..379194a70fd9 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1176,7 +1176,7 @@ PODS: - React-Core - react-native-key-command (1.0.6): - React-Core - - react-native-netinfo (11.1.0): + - react-native-netinfo (11.2.1): - React-Core - react-native-pager-view (6.2.2): - React-Core @@ -1909,7 +1909,7 @@ SPEC CHECKSUMS: react-native-image-manipulator: c48f64221cfcd46e9eec53619c4c0374f3328a56 react-native-image-picker: c33d4e79f0a14a2b66e5065e14946ae63749660b react-native-key-command: 5af6ee30ff4932f78da6a2109017549042932aa5 - react-native-netinfo: 3aa5637c18834966e0c932de8ae1ae56fea20a97 + react-native-netinfo: 8a7fd3f7130ef4ad2fb4276d5c9f8d3f28d2df3d react-native-pager-view: 02a5c4962530f7efc10dd51ee9cdabeff5e6c631 react-native-pdf: 79aa75e39a80c1d45ffe58aa500f3cf08f267a2e react-native-performance: cef2b618d47b277fb5c3280b81a3aad1e72f2886 @@ -1967,7 +1967,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 Turf: 13d1a92d969ca0311bbc26e8356cca178ce95da2 VisionCamera: 7d13aae043ffb38b224a0f725d1e23ca9c190fe7 - Yoga: e64aa65de36c0832d04e8c7bd614396c77a80047 + Yoga: 13c8ef87792450193e117976337b8527b49e8c03 PODFILE CHECKSUM: 0ccbb4f2406893c6e9f266dc1e7470dcd72885d2 diff --git a/package-lock.json b/package-lock.json index ac012bea728f..0acbc00bf99d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.24-7", + "version": "1.4.25-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.24-7", + "version": "1.4.25-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -27,7 +27,7 @@ "@react-native-camera-roll/camera-roll": "5.4.0", "@react-native-clipboard/clipboard": "^1.12.1", "@react-native-community/geolocation": "^3.0.6", - "@react-native-community/netinfo": "11.1.0", + "@react-native-community/netinfo": "11.2.1", "@react-native-firebase/analytics": "^12.3.0", "@react-native-firebase/app": "^12.3.0", "@react-native-firebase/crashlytics": "^12.3.0", @@ -9608,9 +9608,9 @@ } }, "node_modules/@react-native-community/netinfo": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.1.0.tgz", - "integrity": "sha512-pIbCuqgrY7SkngAcjUs9fMzNh1h4soQMVw1IeGp1HN5//wox3fUVOuvyIubTscUbdLFKiltJAiuQek7Nhx1bqA==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.2.1.tgz", + "integrity": "sha512-n9kgmH7vLaU7Cdo8vGfJGGwhrlgppaOSq5zKj9I7H4k5iRM3aNtwURw83mgrc22Ip7nSye2afZV2xDiIyvHttQ==", "peerDependencies": { "react-native": ">=0.59" } @@ -62630,9 +62630,9 @@ "requires": {} }, "@react-native-community/netinfo": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.1.0.tgz", - "integrity": "sha512-pIbCuqgrY7SkngAcjUs9fMzNh1h4soQMVw1IeGp1HN5//wox3fUVOuvyIubTscUbdLFKiltJAiuQek7Nhx1bqA==", + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-11.2.1.tgz", + "integrity": "sha512-n9kgmH7vLaU7Cdo8vGfJGGwhrlgppaOSq5zKj9I7H4k5iRM3aNtwURw83mgrc22Ip7nSye2afZV2xDiIyvHttQ==", "requires": {} }, "@react-native-firebase/analytics": { diff --git a/package.json b/package.json index 4a28617f649d..8682d4ad7385 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.24-7", + "version": "1.4.25-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.", @@ -75,7 +75,7 @@ "@react-native-camera-roll/camera-roll": "5.4.0", "@react-native-clipboard/clipboard": "^1.12.1", "@react-native-community/geolocation": "^3.0.6", - "@react-native-community/netinfo": "11.1.0", + "@react-native-community/netinfo": "11.2.1", "@react-native-firebase/analytics": "^12.3.0", "@react-native-firebase/app": "^12.3.0", "@react-native-firebase/crashlytics": "^12.3.0", diff --git a/src/CONST.ts b/src/CONST.ts index 8b5c0f5a88ca..f0f7ab736b78 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -479,7 +479,9 @@ const CONST = { ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/', // Use Environment.getEnvironmentURL to get the complete URL with port number DEV_NEW_EXPENSIFY_URL: 'https://dev.new.expensify.com:', - EXPENSIFY_INBOX_URL: 'https://www.expensify.com/inbox', + OLDDOT_URLS: { + INBOX: 'inbox', + }, SIGN_IN_FORM_WIDTH: 300, @@ -601,9 +603,11 @@ const CONST = { }, THREAD_DISABLED: ['CREATED'], }, + CANCEL_PAYMENT_REASONS: { + ADMIN: 'CANCEL_REASON_ADMIN', + }, ACTIONABLE_MENTION_WHISPER_RESOLUTION: { INVITE: 'invited', - NOTHING: 'nothing', }, ARCHIVE_REASON: { DEFAULT: 'default', @@ -2730,7 +2734,7 @@ const CONST = { EXPECTED_OUTPUT: 'FCFA 123,457', }, - PATHS_TO_TREAT_AS_EXTERNAL: ['NewExpensify.dmg'], + PATHS_TO_TREAT_AS_EXTERNAL: ['NewExpensify.dmg', 'docs/index.html'], // Test tool menu parameters TEST_TOOL: { diff --git a/src/components/MoneyReportHeader.js b/src/components/MoneyReportHeader.js index 5b59fca6cdae..ce1c9611c733 100644 --- a/src/components/MoneyReportHeader.js +++ b/src/components/MoneyReportHeader.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -24,7 +24,9 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import Button from './Button'; +import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; +import * as Expensicons from './Icon/Expensicons'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; import participantPropTypes from './participantPropTypes'; import SettlementButton from './SettlementButton'; @@ -94,6 +96,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt isPolicyAdmin && (isApproved || isManager) : isPolicyAdmin || (ReportUtils.isMoneyRequestReport(moneyRequestReport) && isManager); const isDraft = ReportUtils.isDraftExpenseReport(moneyRequestReport); + const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + + const cancelPayment = useCallback(() => { + IOU.cancelPayment(moneyRequestReport, chatReport); + setIsConfirmModalVisible(false); + }, [moneyRequestReport, chatReport]); + const shouldShowPayButton = useMemo( () => isPayer && !isDraft && !isSettled && !moneyRequestReport.isWaitingOnBankAccount && reimbursableTotal !== 0 && !ReportUtils.isArchivedRoom(chatReport), [isPayer, isDraft, isSettled, moneyRequestReport, reimbursableTotal, chatReport], @@ -120,6 +129,13 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt ); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)]; + if (isPayer && isSettled && ReportUtils.isExpenseReport(moneyRequestReport)) { + threeDotsMenuItems.push({ + icon: Expensicons.Trashcan, + text: translate('iou.cancelPayment'), + onSelected: () => setIsConfirmModalVisible(true), + }); + } if (!ReportUtils.isArchivedRoom(chatReport)) { threeDotsMenuItems.push({ icon: ZoomIcon, @@ -217,6 +233,16 @@ function MoneyReportHeader({session, personalDetails, policy, chatReport, nextSt )} + setIsConfirmModalVisible(false)} + prompt={translate('iou.cancelPaymentConfirmation')} + confirmText={translate('iou.cancelPayment')} + cancelText={translate('common.dismiss')} + danger + /> ); } diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 036b64af1e4b..7c7998c24c95 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -432,7 +432,7 @@ export default compose( return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`; }, }, - transactionViolation: { + transactionViolations: { key: ({report}) => { const parentReportAction = ReportActionsUtils.getParentReportAction(report); const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0); diff --git a/src/languages/en.ts b/src/languages/en.ts index 90f98d9aec85..09fd295cb859 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2,6 +2,7 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; import CONST from '@src/CONST'; import type { AddressLineParams, + AdminCanceledRequestParams, AlreadySignedInParams, AmountEachParams, ApprovedAmountParams, @@ -111,6 +112,7 @@ type AllCountries = Record; export default { common: { cancel: 'Cancel', + dismiss: 'Dismiss', yes: 'Yes', no: 'No', ok: 'OK', @@ -573,6 +575,8 @@ export default { requestMoney: 'Request money', sendMoney: 'Send money', pay: 'Pay', + cancelPayment: 'Cancel payment', + cancelPaymentConfirmation: 'Are you sure that you want to cancel this payment?', viewDetails: 'View details', pending: 'Pending', canceled: 'Canceled', @@ -609,6 +613,7 @@ export default { payerSettled: ({amount}: PayerSettledParams) => `paid ${amount}`, approvedAmount: ({amount}: ApprovedAmountParams) => `approved ${amount}`, waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `started settling up, payment is held until ${submitterDisplayName} adds a bank account`, + adminCanceledRequest: ({amount}: AdminCanceledRequestParams) => `The ${amount} payment has been cancelled by the admin.`, canceledRequest: ({amount, submitterDisplayName}: CanceledRequestParams) => `Canceled the ${amount} payment, because ${submitterDisplayName} did not enable their Expensify Wallet within 30 days`, settledAfterAddedBankAccount: ({submitterDisplayName, amount}: SettledAfterAddedBankAccountParams) => diff --git a/src/languages/es.ts b/src/languages/es.ts index 331a6a7b92b1..b977a614ae7e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1,6 +1,7 @@ import CONST from '@src/CONST'; import type { AddressLineParams, + AdminCanceledRequestParams, AlreadySignedInParams, AmountEachParams, ApprovedAmountParams, @@ -101,6 +102,7 @@ import type { export default { common: { cancel: 'Cancelar', + dismiss: 'Descartar', yes: 'Sí', no: 'No', ok: 'OK', @@ -566,6 +568,8 @@ export default { requestMoney: 'Pedir dinero', sendMoney: 'Enviar dinero', pay: 'Pagar', + cancelPayment: 'Cancelar el pago', + cancelPaymentConfirmation: '¿Estás seguro de que quieres cancelar este pago?', viewDetails: 'Ver detalles', pending: 'Pendiente', canceled: 'Canceló', @@ -602,6 +606,7 @@ export default { payerSettled: ({amount}: PayerSettledParams) => `pagó ${amount}`, approvedAmount: ({amount}: ApprovedAmountParams) => `aprobó ${amount}`, waitingOnBankAccount: ({submitterDisplayName}: WaitingOnBankAccountParams) => `inicio el pago, pero no se procesará hasta que ${submitterDisplayName} añada una cuenta bancaria`, + adminCanceledRequest: ({amount}: AdminCanceledRequestParams) => `El pago de ${amount} ha sido cancelado por el administrador.`, 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) => diff --git a/src/languages/types.ts b/src/languages/types.ts index 3185b7a8f6f1..35a5110abf79 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -135,6 +135,8 @@ type WaitingOnBankAccountParams = {submitterDisplayName: string}; type CanceledRequestParams = {amount: string; submitterDisplayName: string}; +type AdminCanceledRequestParams = {amount: string}; + type SettledAfterAddedBankAccountParams = {submitterDisplayName: string; amount: string}; type PaidElsewhereWithAmountParams = {payer?: string; amount: string}; @@ -288,6 +290,7 @@ type TranslationFlatObject = { }; export type { + AdminCanceledRequestParams, ApprovedAmountParams, AddressLineParams, AlreadySignedInParams, diff --git a/src/libs/DoInteractionTask/index.desktop.ts b/src/libs/DoInteractionTask/index.desktop.ts new file mode 100644 index 000000000000..73b3cb19ec32 --- /dev/null +++ b/src/libs/DoInteractionTask/index.desktop.ts @@ -0,0 +1,10 @@ +import {InteractionManager} from 'react-native'; + +// For desktop, we should call the callback after all interactions to prevent freezing. See more detail in https://github.com/Expensify/App/issues/28916 +function doInteractionTask(callback: () => void) { + return InteractionManager.runAfterInteractions(() => { + callback(); + }); +} + +export default doInteractionTask; diff --git a/src/libs/DoInteractionTask/index.ts b/src/libs/DoInteractionTask/index.ts new file mode 100644 index 000000000000..dffbb0562b98 --- /dev/null +++ b/src/libs/DoInteractionTask/index.ts @@ -0,0 +1,6 @@ +function doInteractionTask(callback: () => void) { + callback(); + return null; +} + +export default doInteractionTask; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 988398009dd8..37b7a9424fee 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -420,7 +420,7 @@ function getLastMessageTextForReport(report) { } else if (ReportActionUtils.isReimbursementQueuedAction(lastReportAction)) { lastMessageTextFromReport = ReportUtils.getReimbursementQueuedActionMessage(lastReportAction, report); } else if (ReportActionUtils.isReimbursementDeQueuedAction(lastReportAction)) { - lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(report); + lastMessageTextFromReport = ReportUtils.getReimbursementDeQueuedActionMessage(lastReportAction, 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/ReportUtils.ts b/src/libs/ReportUtils.ts index 767cebc7a8c1..b632dbd1fd03 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -16,7 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type {Beta, Login, PersonalDetails, PersonalDetailsList, Policy, Report, ReportAction, ReportMetadata, Session, Transaction} from '@src/types/onyx'; import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; -import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated} from '@src/types/onyx/OriginalMessage'; +import type {IOUMessage, OriginalMessageActionName, OriginalMessageCreated, ReimbursementDeQueuedMessage} from '@src/types/onyx/OriginalMessage'; import type {Status} from '@src/types/onyx/PersonalDetails'; import type {NotificationPreference} from '@src/types/onyx/Report'; import type {Message, ReportActionBase, ReportActions} from '@src/types/onyx/ReportAction'; @@ -180,6 +180,11 @@ type OptimisticSubmittedReportAction = Pick< 'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'isAttachment' | 'originalMessage' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' >; +type OptimisticCancelPaymentReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'created' | 'pendingAction' +>; + type OptimisticEditedTaskReportAction = Pick< ReportAction, 'reportActionID' | 'actionName' | 'pendingAction' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'shouldShow' | 'message' | 'person' @@ -1594,9 +1599,13 @@ function getReimbursementQueuedActionMessage(reportAction: OnyxEntry): string { +function getReimbursementDeQueuedActionMessage(reportAction: OnyxEntry, report: OnyxEntry): string { + const amount = CurrencyUtils.convertToDisplayString(Math.abs(report?.total ?? 0), report?.currency); + const originalMessage = reportAction?.originalMessage as ReimbursementDeQueuedMessage | undefined; + if (originalMessage?.cancellationReason === CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN) { + return Localize.translateLocal('iou.adminCanceledRequest', {amount}); + } const submitterDisplayName = getDisplayNameForParticipant(report?.ownerAccountID, true) ?? ''; - const amount = CurrencyUtils.convertToDisplayString(report?.total ?? 0, report?.currency); return Localize.translateLocal('iou.canceledRequest', {submitterDisplayName, amount}); } @@ -2879,6 +2888,40 @@ function buildOptimisticSubmittedReportAction(amount: number, currency: string, }; } +/** + * Builds an optimistic REIMBURSEMENTDEQUEUED report action with a randomly generated reportActionID. + * + */ +function buildOptimisticCancelPaymentReportAction(expenseReportID: string): OptimisticCancelPaymentReportAction { + return { + actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED, + actorAccountID: currentUserAccountID, + message: [ + { + cancellationReason: CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN, + expenseReportID, + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + text: '', + }, + ], + originalMessage: { + cancellationReason: CONST.REPORT.CANCEL_PAYMENT_REASONS.ADMIN, + expenseReportID, + }, + person: [ + { + style: 'strong', + text: currentUserPersonalDetails?.displayName ?? currentUserEmail, + type: 'TEXT', + }, + ], + reportActionID: NumberUtils.rand64(), + shouldShow: true, + created: DateUtils.getDBTime(), + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }; +} + /** * Builds an optimistic report preview action with a randomly generated reportActionID. * @@ -4201,17 +4244,22 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency) ?? ''; const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport?.managerID, true); - switch (originalMessage.paymentType) { - case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: - translationKey = 'iou.paidElsewhereWithAmount'; - break; - case CONST.IOU.PAYMENT_TYPE.EXPENSIFY: - case CONST.IOU.PAYMENT_TYPE.VBBA: - translationKey = 'iou.paidWithExpensifyWithAmount'; - break; - default: - translationKey = 'iou.payerPaidAmount'; - break; + // If the payment was cancelled, show the "Owes" message + if (!isSettled(IOUReportID)) { + translationKey = 'iou.payerOwesAmount'; + } else { + switch (originalMessage.paymentType) { + case CONST.IOU.PAYMENT_TYPE.ELSEWHERE: + translationKey = 'iou.paidElsewhereWithAmount'; + break; + case CONST.IOU.PAYMENT_TYPE.EXPENSIFY: + case CONST.IOU.PAYMENT_TYPE.VBBA: + translationKey = 'iou.paidWithExpensifyWithAmount'; + break; + default: + translationKey = 'iou.payerPaidAmount'; + break; + } } return Localize.translateLocal(translationKey, {amount: formattedAmount, payer: payerName ?? ''}); } @@ -4438,6 +4486,7 @@ export { buildOptimisticIOUReportAction, buildOptimisticReportPreview, buildOptimisticModifiedExpenseReportAction, + buildOptimisticCancelPaymentReportAction, updateReportPreview, buildOptimisticTaskReportAction, buildOptimisticAddCommentReportAction, diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 9aa7c52b1ea0..ca572452cc82 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -3328,6 +3328,109 @@ function submitReport(expenseReport) { ); } +/** + * @param {Object} expenseReport + * @param {Object} chatReport + */ +function cancelPayment(expenseReport, chatReport) { + const optimisticReportAction = ReportUtils.buildOptimisticCancelPaymentReportAction(expenseReport.reportID); + const policy = ReportUtils.getPolicy(chatReport.policyID); + const isFree = policy && policy.type === CONST.POLICY.TYPE.FREE; + const optimisticData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticReportAction.reportActionID]: { + ...optimisticReportAction, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + value: { + ...expenseReport, + lastMessageText: lodashGet(optimisticReportAction, 'message.0.text', ''), + lastMessageHtml: lodashGet(optimisticReportAction, 'message.0.html', ''), + state: isFree ? CONST.REPORT.STATE.SUBMITTED : CONST.REPORT.STATE.OPEN, + stateNum: isFree ? CONST.REPORT.STATE_NUM.PROCESSING : CONST.REPORT.STATE.OPEN, + statusNum: isFree ? CONST.REPORT.STATUS.SUBMITTED : CONST.REPORT.STATE.OPEN, + }, + }, + ...(chatReport.reportID + ? [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + ...chatReport, + hasOutstandingIOU: true, + hasOutstandingChildRequest: true, + iouReportID: expenseReport.reportID, + }, + }, + ] + : []), + ]; + + const successData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [optimisticReportAction.reportActionID]: { + pendingAction: null, + }, + }, + }, + ]; + + const failureData = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseReport.reportID}`, + value: { + [expenseReport.reportActionID]: { + errors: ErrorUtils.getMicroSecondOnyxError('iou.error.other'), + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, + value: { + statusNum: CONST.REPORT.STATUS.REIMBURSED, + }, + }, + ...(chatReport.reportID + ? [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, + value: { + hasOutstandingIOU: false, + hasOutstandingChildRequest: false, + iouReportID: 0, + }, + }, + ] + : []), + ]; + + API.write( + 'CancelPayment', + { + iouReportID: expenseReport.reportID, + chatReportID: chatReport.reportID, + managerAccountID: expenseReport.managerID, + reportActionID: optimisticReportAction.reportActionID, + }, + {optimisticData, successData, failureData}, + ); +} + /** * @param {String} paymentType * @param {Object} chatReport @@ -3657,4 +3760,5 @@ export { detachReceipt, getIOUReportID, editMoneyRequest, + cancelPayment, }; diff --git a/src/libs/actions/Link.ts b/src/libs/actions/Link.ts index 2fb863467e32..186c9beed970 100644 --- a/src/libs/actions/Link.ts +++ b/src/libs/actions/Link.ts @@ -65,7 +65,7 @@ function openOldDotLink(url: string) { function getInternalNewExpensifyPath(href: string) { const attrPath = Url.getPathFromURL(href); return (Url.hasSameExpensifyOrigin(href, CONST.NEW_EXPENSIFY_URL) || Url.hasSameExpensifyOrigin(href, CONST.STAGING_NEW_EXPENSIFY_URL) || href.startsWith(CONST.DEV_NEW_EXPENSIFY_URL)) && - !CONST.PATHS_TO_TREAT_AS_EXTERNAL.find((path) => path === attrPath) + !CONST.PATHS_TO_TREAT_AS_EXTERNAL.find((path) => attrPath.startsWith(path)) ? attrPath : ''; } diff --git a/src/pages/NewChatPage.js b/src/pages/NewChatPage.js index 3a58727eddb7..b90ce6bbc247 100755 --- a/src/pages/NewChatPage.js +++ b/src/pages/NewChatPage.js @@ -1,6 +1,6 @@ import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import {InteractionManager, View} from 'react-native'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import KeyboardAvoidingView from '@components/KeyboardAvoidingView'; @@ -15,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import doInteractionTask from '@libs/DoInteractionTask'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; import variables from '@styles/variables'; @@ -209,11 +210,16 @@ function NewChatPage({betas, isGroupChat, personalDetails, reports, translate, i }, [reports, personalDetails, searchTerm]); useEffect(() => { - const interactionTask = InteractionManager.runAfterInteractions(() => { + const interactionTask = doInteractionTask(() => { setDidScreenTransitionEnd(true); }); - return interactionTask.cancel; + return () => { + if (!interactionTask) { + return; + } + interactionTask.cancel(); + }; }, []); useEffect(() => { diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index f22eda58ce7f..7db39c1ed856 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -285,6 +285,11 @@ export default [ } else if (ReportActionsUtils.isModifiedExpenseAction(reportAction)) { const modifyExpenseMessage = ModifiedExpenseMessage.getForReportAction(reportAction); Clipboard.setString(modifyExpenseMessage); + } else if (ReportActionsUtils.isReimbursementDeQueuedAction(reportAction)) { + const {expenseReportID} = reportAction.originalMessage; + const expenseReport = ReportUtils.getReport(expenseReportID); + const displayMessage = ReportUtils.getReimbursementDeQueuedActionMessage(reportAction, expenseReport); + Clipboard.setString(displayMessage); } else if (ReportActionsUtils.isMoneyRequestAction(reportAction)) { const displayMessage = ReportUtils.getIOUReportActionDisplayMessage(reportAction); Clipboard.setString(displayMessage); diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index e490c4601d10..1f6455ea6630 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -38,7 +38,6 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; 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 ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; @@ -440,10 +439,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); - const amount = CurrencyUtils.convertToDisplayString(props.report.total, props.report.currency); - - children = ; + children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE) { children = ; } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.MARKEDREIMBURSED) { diff --git a/src/pages/home/report/ReportActionItemMessage.tsx b/src/pages/home/report/ReportActionItemMessage.tsx index 3a71ee8356b3..025b0cbb8b0a 100644 --- a/src/pages/home/report/ReportActionItemMessage.tsx +++ b/src/pages/home/report/ReportActionItemMessage.tsx @@ -57,7 +57,7 @@ function ReportActionItemMessage({action, displayAsGroup, reportID, style, isHid const originalMessage = action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? action.originalMessage : null; const iouReportID = originalMessage?.IOUReportID; if (iouReportID) { - iouMessage = ReportUtils.getReportPreviewMessage(ReportUtils.getReport(iouReportID), action); + iouMessage = ReportUtils.getIOUReportActionDisplayMessage(action); } } diff --git a/src/pages/settings/InitialSettingsPage.js b/src/pages/settings/InitialSettingsPage.js index d2b91ed6b76b..6e310b9a62bd 100755 --- a/src/pages/settings/InitialSettingsPage.js +++ b/src/pages/settings/InitialSettingsPage.js @@ -267,11 +267,11 @@ function InitialSettingsPage(props) { translationKey: 'initialSettingsPage.goToExpensifyClassic', icon: Expensicons.NewExpensify, action: () => { - Link.openExternalLink(CONST.EXPENSIFY_INBOX_URL); + Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX); }, shouldShowRightIcon: true, iconRight: Expensicons.NewWindow, - link: CONST.EXPENSIFY_INBOX_URL, + link: Link.buildOldDotURL(CONST.OLDDOT_URLS.INBOX), }, { translationKey: 'initialSettingsPage.signOut', diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 3c44f806fdb8..856c0613cec7 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -208,7 +208,7 @@ function ExpensifyCardPage({ medium style={[styles.mh5, styles.mb5]} text={translate('cardPage.reviewTransaction')} - onPress={() => Link.openOldDotLink('inbox')} + onPress={() => Link.openOldDotLink(CONST.OLDDOT_URLS.INBOX)} /> ) : null} diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index bf547bc4bd10..8382014a01e5 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -556,6 +556,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod }} onItemSelected={(method) => addPaymentMethodTypePressed(method)} anchorRef={addPaymentMethodAnchorRef} + shouldShowPersonalBankAccountOption /> ); diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index c7a1da7b64ff..2150358a5134 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -57,6 +57,7 @@ const propTypes = { }).isRequired, isLoadingReportData: PropTypes.bool, + invitedEmailsToAccountIDsDraft: PropTypes.objectOf(PropTypes.number), ...policyPropTypes, }; @@ -64,6 +65,7 @@ const defaultProps = { personalDetails: {}, betas: [], isLoadingReportData: true, + invitedEmailsToAccountIDsDraft: {}, ...policyDefaultProps, }; @@ -81,7 +83,10 @@ function WorkspaceInvitePage(props) { useEffect(() => { setSearchTerm(SearchInputManager.searchInput); - }, []); + return () => { + Policy.setWorkspaceInviteMembersDraft(props.route.params.policyID, {}); + }; + }, [props.route.params.policyID]); useEffect(() => { Policy.clearErrors(props.route.params.policyID); @@ -105,6 +110,12 @@ function WorkspaceInvitePage(props) { _.each(inviteOptions.personalDetails, (detail) => (detailsMap[detail.login] = OptionsListUtils.formatMemberForList(detail))); const newSelectedOptions = []; + _.each(_.keys(props.invitedEmailsToAccountIDsDraft), (login) => { + if (!_.has(detailsMap, login)) { + return; + } + newSelectedOptions.push({...detailsMap[login], isSelected: true}); + }); _.each(selectedOptions, (option) => { newSelectedOptions.push(_.has(detailsMap, option.login) ? {...detailsMap[option.login], isSelected: true} : option); }); @@ -323,5 +334,8 @@ export default compose( isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, }, + invitedEmailsToAccountIDsDraft: { + key: ({route}) => `${ONYXKEYS.COLLECTION.WORKSPACE_INVITE_MEMBERS_DRAFT}${route.params.policyID.toString()}`, + }, }), )(WorkspaceInvitePage); diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index f10696ced00f..09be2d9e04dd 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -37,6 +37,7 @@ type IOUMessage = { /** The ID of the iou transaction */ IOUTransactionID?: string; IOUReportID?: string; + expenseReportID?: string; amount: number; comment?: string; currency: string; @@ -44,10 +45,15 @@ type IOUMessage = { participantAccountIDs?: number[]; type: ValueOf; paymentType?: DeepValueOf; + cancellationReason?: string; /** Only exists when we are sending money */ IOUDetails?: IOUDetails; }; +type ReimbursementDeQueuedMessage = { + cancellationReason: string; +}; + type OriginalMessageIOU = { actionName: typeof CONST.REPORT.ACTIONS.TYPE.IOU; originalMessage: IOUMessage; @@ -274,6 +280,7 @@ export type { Reaction, ActionName, IOUMessage, + ReimbursementDeQueuedMessage, Closed, OriginalMessageActionName, ChangeLog, diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 509e3a286ea4..b2dc340af606 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -54,6 +54,12 @@ type Message = { /** ID of a task report */ taskReportID?: string; + /** Reason of payment cancellation */ + cancellationReason?: string; + + /** ID of an expense report */ + expenseReportID?: string; + /** resolution for actionable mention whisper */ resolution?: ValueOf | null; };