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;
};