{
return new Promise((resolve, reject) => {
- const request = {
+ const request: Request = {
command,
data,
type,
@@ -35,8 +30,8 @@ function post(command, data = {}, type = CONST.NETWORK.METHOD.POST, shouldUseSec
// (e.g. any requests currently happening when the user logs out are cancelled)
request.data = {
...data,
- shouldRetry: lodashGet(data, 'shouldRetry', true),
- canCancel: lodashGet(data, 'canCancel', true),
+ shouldRetry: data?.shouldRetry ?? true,
+ canCancel: data?.canCancel ?? true,
appversion: pkg.version,
};
@@ -50,7 +45,7 @@ function post(command, data = {}, type = CONST.NETWORK.METHOD.POST, shouldUseSec
// This check is mainly used to prevent API commands from triggering calls to MainQueue.process() from inside the context of a previous
// call to MainQueue.process() e.g. calling a Log command without this would cause the requests in mainQueue to double process
// since we call Log inside MainQueue.process().
- const shouldProcessImmediately = lodashGet(request, 'data.shouldProcessImmediately', true);
+ const shouldProcessImmediately = request?.data?.shouldProcessImmediately ?? true;
if (!shouldProcessImmediately) {
return;
}
diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js
index 051c19312f09..886f6862f4a9 100644
--- a/src/libs/OptionsListUtils.js
+++ b/src/libs/OptionsListUtils.js
@@ -364,7 +364,8 @@ function getLastMessageTextForReport(report) {
if (ReportUtils.isReportMessageAttachment({text: report.lastMessageText, html: report.lastMessageHtml, translationKey: report.lastMessageTranslationKey})) {
lastMessageTextFromReport = `[${Localize.translateLocal(report.lastMessageTranslationKey || 'common.attachment')}]`;
} else if (ReportActionUtils.isMoneyRequestAction(lastReportAction)) {
- lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(report, lastReportAction, true);
+ const properSchemaForMoneyRequestMessage = ReportUtils.getReportPreviewMessage(report, lastReportAction, true);
+ lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForMoneyRequestMessage);
} else if (ReportActionUtils.isReportPreviewAction(lastReportAction)) {
const iouReport = ReportUtils.getReport(ReportActionUtils.getIOUReportIDFromReportActionPreview(lastReportAction));
const lastIOUMoneyReport = _.find(
@@ -375,6 +376,8 @@ function getLastMessageTextForReport(report) {
ReportActionUtils.isMoneyRequestAction(reportAction),
);
lastMessageTextFromReport = ReportUtils.getReportPreviewMessage(iouReport, lastIOUMoneyReport, true);
+ } else if (ReportActionUtils.isDeletedParentAction(lastReportAction) && ReportUtils.isChatReport(report)) {
+ lastMessageTextFromReport = ReportUtils.getDeletedParentActionMessageForChatReport(lastReportAction);
} else if (ReportActionUtils.isModifiedExpenseAction(lastReportAction)) {
const properSchemaForModifiedExpenseMessage = ReportUtils.getModifiedExpenseMessage(lastReportAction);
lastMessageTextFromReport = ReportUtils.formatReportLastMessageText(properSchemaForModifiedExpenseMessage, true);
@@ -521,7 +524,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, {
}
result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result);
- result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result);
+ result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result);
if (!hasMultipleParticipants) {
result.login = personalDetail.login;
diff --git a/src/libs/Performance.js b/src/libs/Performance.tsx
similarity index 52%
rename from src/libs/Performance.js
rename to src/libs/Performance.tsx
index 0207fd20c564..cfb5e258c9f8 100644
--- a/src/libs/Performance.js
+++ b/src/libs/Performance.tsx
@@ -1,39 +1,73 @@
-import _ from 'underscore';
-import lodashTransform from 'lodash/transform';
import React, {Profiler, forwardRef} from 'react';
import {Alert, InteractionManager} from 'react-native';
+import lodashTransform from 'lodash/transform';
+import isObject from 'lodash/isObject';
+import isEqual from 'lodash/isEqual';
+import {Performance as RNPerformance, PerformanceEntry, PerformanceMark, PerformanceMeasure} from 'react-native-performance';
+import {PerformanceObserverEntryList} from 'react-native-performance/lib/typescript/performance-observer';
import * as Metrics from './Metrics';
import getComponentDisplayName from './getComponentDisplayName';
import CONST from '../CONST';
import isE2ETestSession from './E2E/isE2ETestSession';
-/** @type {import('react-native-performance').Performance} */
-let rnPerformance;
+type WrappedComponentConfig = {id: string};
+
+type PerformanceEntriesCallback = (entry: PerformanceEntry) => void;
+
+type Phase = 'mount' | 'update';
+
+type WithRenderTraceHOC = >(WrappedComponent: React.ComponentType
) => React.ComponentType
>;
+
+type BlankHOC =
>(Component: React.ComponentType
) => React.ComponentType
;
+
+type SetupPerformanceObserver = () => void;
+type DiffObject = (object: Record, base: Record) => Record;
+type GetPerformanceMetrics = () => PerformanceEntry[];
+type PrintPerformanceMetrics = () => void;
+type MarkStart = (name: string, detail?: Record) => PerformanceMark | void;
+type MarkEnd = (name: string, detail?: Record) => PerformanceMark | void;
+type MeasureFailSafe = (measureName: string, startOrMeasureOptions: string, endMark: string) => void;
+type MeasureTTI = (endMark: string) => void;
+type TraceRender = (id: string, phase: Phase, actualDuration: number, baseDuration: number, startTime: number, commitTime: number, interactions: Set) => PerformanceMeasure | void;
+type WithRenderTrace = ({id}: WrappedComponentConfig) => WithRenderTraceHOC | BlankHOC;
+type SubscribeToMeasurements = (callback: PerformanceEntriesCallback) => void;
+
+type PerformanceModule = {
+ diffObject: DiffObject;
+ setupPerformanceObserver: SetupPerformanceObserver;
+ getPerformanceMetrics: GetPerformanceMetrics;
+ printPerformanceMetrics: PrintPerformanceMetrics;
+ markStart: MarkStart;
+ markEnd: MarkEnd;
+ measureFailSafe: MeasureFailSafe;
+ measureTTI: MeasureTTI;
+ traceRender: TraceRender;
+ withRenderTrace: WithRenderTrace;
+ subscribeToMeasurements: SubscribeToMeasurements;
+};
+
+let rnPerformance: RNPerformance;
/**
* Deep diff between two objects. Useful for figuring out what changed about an object from one render to the next so
* that state and props updates can be optimized.
- *
- * @param {Object} object
- * @param {Object} base
- * @return {Object}
*/
-function diffObject(object, base) {
- function changes(obj, comparisonObject) {
+function diffObject(object: Record, base: Record): Record {
+ function changes(obj: Record, comparisonObject: Record): Record {
return lodashTransform(obj, (result, value, key) => {
- if (_.isEqual(value, comparisonObject[key])) {
+ if (isEqual(value, comparisonObject[key])) {
return;
}
// eslint-disable-next-line no-param-reassign
- result[key] = _.isObject(value) && _.isObject(comparisonObject[key]) ? changes(value, comparisonObject[key]) : value;
+ result[key] = isObject(value) && isObject(comparisonObject[key]) ? changes(value as Record, comparisonObject[key] as Record) : value;
});
}
return changes(object, base);
}
-const Performance = {
+const Performance: PerformanceModule = {
// When performance monitoring is disabled the implementations are blank
diffObject,
setupPerformanceObserver: () => {},
@@ -44,7 +78,11 @@ const Performance = {
measureFailSafe: () => {},
measureTTI: () => {},
traceRender: () => {},
- withRenderTrace: () => (Component) => Component,
+ withRenderTrace:
+ () =>
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ >(Component: React.ComponentType
): React.ComponentType
=>
+ Component,
subscribeToMeasurements: () => {},
};
@@ -53,20 +91,21 @@ if (Metrics.canCapturePerformanceMetrics()) {
perfModule.setResourceLoggingEnabled(true);
rnPerformance = perfModule.default;
- Performance.measureFailSafe = (measureName, startOrMeasureOptions, endMark) => {
+ Performance.measureFailSafe = (measureName: string, startOrMeasureOptions: string, endMark: string) => {
try {
rnPerformance.measure(measureName, startOrMeasureOptions, endMark);
} catch (error) {
// Sometimes there might be no start mark recorded and the measure will fail with an error
- console.debug(error.message);
+ if (error instanceof Error) {
+ console.debug(error.message);
+ }
}
};
/**
* Measures the TTI time. To be called when the app is considered to be interactive.
- * @param {String} [endMark] Optional end mark name
*/
- Performance.measureTTI = (endMark) => {
+ Performance.measureTTI = (endMark: string) => {
// Make sure TTI is captured when the app is really usable
InteractionManager.runAfterInteractions(() => {
requestAnimationFrame(() => {
@@ -88,8 +127,8 @@ if (Metrics.canCapturePerformanceMetrics()) {
performanceReported.setupDefaultFlipperReporter();
// Monitor some native marks that we want to put on the timeline
- new perfModule.PerformanceObserver((list, observer) => {
- list.getEntries().forEach((entry) => {
+ new perfModule.PerformanceObserver((list: PerformanceObserverEntryList, observer: PerformanceObserver) => {
+ list.getEntries().forEach((entry: PerformanceEntry) => {
if (entry.name === 'nativeLaunchEnd') {
Performance.measureFailSafe('nativeLaunch', 'nativeLaunchStart', 'nativeLaunchEnd');
}
@@ -108,8 +147,8 @@ if (Metrics.canCapturePerformanceMetrics()) {
}).observe({type: 'react-native-mark', buffered: true});
// Monitor for "_end" marks and capture "_start" to "_end" measures
- new perfModule.PerformanceObserver((list) => {
- list.getEntriesByType('mark').forEach((mark) => {
+ new perfModule.PerformanceObserver((list: PerformanceObserverEntryList) => {
+ list.getEntriesByType('mark').forEach((mark: PerformanceEntry) => {
if (mark.name.endsWith('_end')) {
const end = mark.name;
const name = end.replace(/_end$/, '');
@@ -125,65 +164,64 @@ if (Metrics.canCapturePerformanceMetrics()) {
}).observe({type: 'mark', buffered: true});
};
- Performance.getPerformanceMetrics = () =>
- _.chain([
+ Performance.getPerformanceMetrics = (): PerformanceEntry[] =>
+ [
...rnPerformance.getEntriesByName('nativeLaunch'),
...rnPerformance.getEntriesByName('runJsBundle'),
...rnPerformance.getEntriesByName('jsBundleDownload'),
...rnPerformance.getEntriesByName('TTI'),
...rnPerformance.getEntriesByName('regularAppStart'),
...rnPerformance.getEntriesByName('appStartedToReady'),
- ])
- .filter((entry) => entry.duration > 0)
- .value();
+ ].filter((entry) => entry.duration > 0);
/**
* Outputs performance stats. We alert these so that they are easy to access in release builds.
*/
Performance.printPerformanceMetrics = () => {
const stats = Performance.getPerformanceMetrics();
- const statsAsText = _.map(stats, (entry) => `\u2022 ${entry.name}: ${entry.duration.toFixed(1)}ms`).join('\n');
+ const statsAsText = stats.map((entry) => `\u2022 ${entry.name}: ${entry.duration.toFixed(1)}ms`).join('\n');
if (stats.length > 0) {
Alert.alert('Performance', statsAsText);
}
};
- Performance.subscribeToMeasurements = (callback) => {
- new perfModule.PerformanceObserver((list) => {
+ Performance.subscribeToMeasurements = (callback: PerformanceEntriesCallback) => {
+ new perfModule.PerformanceObserver((list: PerformanceObserverEntryList) => {
list.getEntriesByType('measure').forEach(callback);
}).observe({type: 'measure', buffered: true});
};
/**
* Add a start mark to the performance entries
- * @param {string} name
- * @param {Object} [detail]
- * @returns {PerformanceMark}
*/
- Performance.markStart = (name, detail) => rnPerformance.mark(`${name}_start`, {detail});
+ Performance.markStart = (name: string, detail?: Record): PerformanceMark => rnPerformance.mark(`${name}_start`, {detail});
/**
* Add an end mark to the performance entries
* A measure between start and end is captured automatically
- * @param {string} name
- * @param {Object} [detail]
- * @returns {PerformanceMark}
*/
- Performance.markEnd = (name, detail) => rnPerformance.mark(`${name}_end`, {detail});
+ Performance.markEnd = (name: string, detail?: Record): PerformanceMark => rnPerformance.mark(`${name}_end`, {detail});
/**
* Put data emitted by Profiler components on the timeline
- * @param {string} id the "id" prop of the Profiler tree that has just committed
- * @param {'mount'|'update'} phase either "mount" (if the tree just mounted) or "update" (if it re-rendered)
- * @param {number} actualDuration time spent rendering the committed update
- * @param {number} baseDuration estimated time to render the entire subtree without memoization
- * @param {number} startTime when React began rendering this update
- * @param {number} commitTime when React committed this update
- * @param {Set} interactions the Set of interactions belonging to this update
- * @returns {PerformanceMeasure}
+ * @param id the "id" prop of the Profiler tree that has just committed
+ * @param phase either "mount" (if the tree just mounted) or "update" (if it re-rendered)
+ * @param actualDuration time spent rendering the committed update
+ * @param baseDuration estimated time to render the entire subtree without memoization
+ * @param startTime when React began rendering this update
+ * @param commitTime when React committed this update
+ * @param interactions the Set of interactions belonging to this update
*/
- Performance.traceRender = (id, phase, actualDuration, baseDuration, startTime, commitTime, interactions) =>
+ Performance.traceRender = (
+ id: string,
+ phase: Phase,
+ actualDuration: number,
+ baseDuration: number,
+ startTime: number,
+ commitTime: number,
+ interactions: Set,
+ ): PerformanceMeasure =>
rnPerformance.measure(id, {
start: startTime,
duration: actualDuration,
@@ -197,14 +235,12 @@ if (Metrics.canCapturePerformanceMetrics()) {
/**
* A HOC that captures render timings of the Wrapped component
- * @param {object} config
- * @param {string} config.id
- * @returns {function(React.Component): React.FunctionComponent}
*/
Performance.withRenderTrace =
- ({id}) =>
- (WrappedComponent) => {
- const WithRenderTrace = forwardRef((props, ref) => (
+ ({id}: WrappedComponentConfig) =>
+ // eslint-disable-next-line @typescript-eslint/naming-convention
+ >(WrappedComponent: React.ComponentType
): React.ComponentType
> => {
+ const WithRenderTrace: React.ComponentType
> = forwardRef((props: P, ref) => (
));
- WithRenderTrace.displayName = `withRenderTrace(${getComponentDisplayName(WrappedComponent)})`;
+ WithRenderTrace.displayName = `withRenderTrace(${getComponentDisplayName(WrappedComponent as React.ComponentType)})`;
return WithRenderTrace;
};
}
diff --git a/src/libs/PolicyUtils.js b/src/libs/PolicyUtils.js
index 347a825f59cc..6bbae72f1d80 100644
--- a/src/libs/PolicyUtils.js
+++ b/src/libs/PolicyUtils.js
@@ -174,7 +174,7 @@ function getMemberAccountIDsForWorkspace(policyMembers, personalDetails) {
if (!personalDetail || !personalDetail.login) {
return;
}
- memberEmailsToAccountIDs[personalDetail.login] = accountID;
+ memberEmailsToAccountIDs[personalDetail.login] = Number(accountID);
});
return memberEmailsToAccountIDs;
}
diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts
index cdc45cb119d5..9fa7ebdc6559 100644
--- a/src/libs/ReceiptUtils.ts
+++ b/src/libs/ReceiptUtils.ts
@@ -6,10 +6,13 @@ import ReceiptHTML from '../../assets/images/receipt-html.png';
import ReceiptDoc from '../../assets/images/receipt-doc.png';
import ReceiptGeneric from '../../assets/images/receipt-generic.png';
import ReceiptSVG from '../../assets/images/receipt-svg.png';
+import {Transaction} from '../types/onyx';
+import ROUTES from '../ROUTES';
type ThumbnailAndImageURI = {
image: ImageSourcePropType | string;
thumbnail: string | null;
+ transaction?: Transaction;
};
type FileNameAndExtension = {
@@ -20,12 +23,21 @@ type FileNameAndExtension = {
/**
* Grab the appropriate receipt image and thumbnail URIs based on file type
*
- * @param path URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg
- * @param filename of uploaded image or last part of remote URI
+ * @param transaction
*/
-function getThumbnailAndImageURIs(path: string, filename: string): ThumbnailAndImageURI {
+function getThumbnailAndImageURIs(transaction: Transaction): ThumbnailAndImageURI {
+ // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg
+ const path = transaction?.receipt?.source ?? '';
+ // filename of uploaded image or last part of remote URI
+ const filename = transaction?.filename ?? '';
const isReceiptImage = Str.isImage(filename);
+ const hasEReceipt = transaction?.hasEReceipt;
+
+ if (hasEReceipt) {
+ return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction};
+ }
+
// For local files, we won't have a thumbnail yet
if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) {
return {thumbnail: null, image: path};
diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js
index fa1883dd9b98..d0f0b35d5f9a 100644
--- a/src/libs/ReportActionsUtils.js
+++ b/src/libs/ReportActionsUtils.js
@@ -380,10 +380,11 @@ function shouldReportActionBeVisibleAsLastAction(reportAction) {
}
// If a whisper action is the REPORTPREVIEW action, we are displaying it.
+ // If the action's message text is empty and it is not a deleted parent with visible child actions, hide it. Else, consider the action to be displayable.
return (
shouldReportActionBeVisible(reportAction, reportAction.reportActionID) &&
!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) &&
- !isDeletedAction(reportAction)
+ !(isDeletedAction(reportAction) && !isDeletedParentAction(reportAction))
);
}
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index 8c8f203f8f18..eb512b7927a9 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -1212,6 +1212,46 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR
});
}
+/**
+ * For a deleted parent report action within a chat report,
+ * let us return the appropriate display message
+ *
+ * @param {Object} reportAction - The deleted report action of a chat report for which we need to return message.
+ * @return {String}
+ */
+function getDeletedParentActionMessageForChatReport(reportAction) {
+ // By default, let us display [Deleted message]
+ let deletedMessageText = Localize.translateLocal('parentReportAction.deletedMessage');
+ if (ReportActionsUtils.isCreatedTaskReportAction(reportAction)) {
+ // For canceled task report, let us display [Deleted task]
+ deletedMessageText = Localize.translateLocal('parentReportAction.deletedTask');
+ }
+ return deletedMessageText;
+}
+
+/**
+ * Returns the last visible message for a given report after considering the given optimistic actions
+ *
+ * @param {String} reportID - the report for which last visible message has to be fetched
+ * @param {Object} [actionsToMerge] - the optimistic merge actions that needs to be considered while fetching last visible message
+ * @return {Object}
+ */
+function getLastVisibleMessage(reportID, actionsToMerge = {}) {
+ const report = getReport(reportID);
+ const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, actionsToMerge);
+
+ // For Chat Report with deleted parent actions, let us fetch the correct message
+ if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && isChatReport(report)) {
+ const lastMessageText = getDeletedParentActionMessageForChatReport(lastVisibleAction);
+ return {
+ lastMessageText,
+ };
+ }
+
+ // Fetch the last visible message for report represented by reportID and based on actions to merge.
+ return ReportActionsUtils.getLastVisibleMessage(reportID, actionsToMerge);
+}
+
/**
* Determines if a report has an IOU that is waiting for an action from the current user (either Pay or Add a credit bank account)
*
@@ -1283,7 +1323,7 @@ function hasNonReimbursableTransactions(iouReportID) {
* @param {Object} allReportsDict
* @returns {Number}
*/
-function getMoneyRequestTotal(report, allReportsDict = null) {
+function getMoneyRequestReimbursableTotal(report, allReportsDict = null) {
const allAvailableReports = allReportsDict || allReports;
let moneyRequestReport;
if (isMoneyRequestReport(report)) {
@@ -1294,7 +1334,6 @@ function getMoneyRequestTotal(report, allReportsDict = null) {
}
if (moneyRequestReport) {
const total = lodashGet(moneyRequestReport, 'total', 0);
-
if (total !== 0) {
// There is a possibility that if the Expense report has a negative total.
// This is because there are instances where you can get a credit back on your card,
@@ -1305,6 +1344,45 @@ function getMoneyRequestTotal(report, allReportsDict = null) {
return 0;
}
+/**
+ * @param {Object} report
+ * @param {Object} allReportsDict
+ * @returns {Object}
+ */
+function getMoneyRequestSpendBreakdown(report, allReportsDict = null) {
+ const allAvailableReports = allReportsDict || allReports;
+ let moneyRequestReport;
+ if (isMoneyRequestReport(report)) {
+ moneyRequestReport = report;
+ }
+ if (allAvailableReports && report.hasOutstandingIOU && report.iouReportID) {
+ moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`];
+ }
+ if (moneyRequestReport) {
+ let nonReimbursableSpend = lodashGet(moneyRequestReport, 'nonReimbursableTotal', 0);
+ let reimbursableSpend = lodashGet(moneyRequestReport, 'total', 0);
+
+ if (nonReimbursableSpend + reimbursableSpend !== 0) {
+ // There is a possibility that if the Expense report has a negative total.
+ // This is because there are instances where you can get a credit back on your card,
+ // or you enter a negative expense to “offset” future expenses
+ nonReimbursableSpend = isExpenseReport(moneyRequestReport) ? nonReimbursableSpend * -1 : Math.abs(nonReimbursableSpend);
+ reimbursableSpend = isExpenseReport(moneyRequestReport) ? reimbursableSpend * -1 : Math.abs(reimbursableSpend);
+ const totalDisplaySpend = nonReimbursableSpend + reimbursableSpend;
+ return {
+ nonReimbursableSpend,
+ reimbursableSpend,
+ totalDisplaySpend,
+ };
+ }
+ }
+ return {
+ nonReimbursableSpend: 0,
+ reimbursableSpend: 0,
+ totalDisplaySpend: 0,
+ };
+}
+
/**
* Get the title for a policy expense chat which depends on the role of the policy member seeing this report
*
@@ -1344,7 +1422,7 @@ function getPolicyExpenseChatName(report, policy = undefined) {
* @returns {String}
*/
function getMoneyRequestReportName(report, policy = undefined) {
- const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(report), report.currency);
+ const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(report), report.currency);
const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID);
const payerPaidAmountMesssage = Localize.translateLocal('iou.payerPaidAmount', {
payer: payerName,
@@ -1542,12 +1620,15 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip
if (_.isEmpty(linkedTransaction)) {
return reportActionMessage;
}
+ if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) {
+ return Localize.translateLocal('iou.receiptScanning');
+ }
const {amount, currency, comment} = getTransactionDetails(linkedTransaction);
const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency);
return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment});
}
- const totalAmount = getMoneyRequestTotal(report);
+ const totalAmount = getMoneyRequestReimbursableTotal(report);
const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerID, true);
const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency);
@@ -2216,7 +2297,7 @@ function buildOptimisticExpenseReport(chatReportID, policyID, payeeAccountID, to
function getIOUReportActionMessage(iouReportID, type, total, comment, currency, paymentType = '', isSettlingUp = false) {
const amount =
type === CONST.IOU.REPORT_ACTION_TYPE.PAY
- ? CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(getReport(iouReportID)), currency)
+ ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(getReport(iouReportID)), currency)
: CurrencyUtils.convertToDisplayString(total, currency);
let paymentMethodMessage;
@@ -3070,6 +3151,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas,
if (
!report ||
!report.reportID ||
+ !report.type ||
report.isHidden ||
(report.participantAccountIDs &&
report.participantAccountIDs.length === 0 &&
@@ -3432,8 +3514,12 @@ function getMoneyRequestOptions(report, reportParticipants) {
// User created policy rooms and default rooms like #admins or #announce will always have the Split Bill option
// unless there are no participants at all (e.g. #admins room for a policy with only 1 admin)
// DM chats will have the Split Bill option only when there are at least 3 people in the chat.
- // There is no Split Bill option for Workspace chats, IOU or Expense reports which are threads
- if ((isChatRoom(report) && participants.length > 0) || (hasMultipleParticipants && !isPolicyExpenseChat(report) && !isMoneyRequestReport(report)) || isControlPolicyExpenseChat(report)) {
+ // There is no Split Bill option for IOU or Expense reports which are threads
+ if (
+ (isChatRoom(report) && participants.length > 0) ||
+ (hasMultipleParticipants && !isPolicyExpenseChat(report) && !isMoneyRequestReport(report)) ||
+ (isControlPolicyExpenseChat(report) && report.isOwnPolicyExpenseChat)
+ ) {
return [CONST.IOU.MONEY_REQUEST_TYPE.SPLIT];
}
@@ -3589,7 +3675,8 @@ function shouldDisableWriteActions(report) {
* @returns {String}
*/
function getOriginalReportID(reportID, reportAction) {
- return isThreadFirstChat(reportAction, reportID) ? lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, 'parentReportID']) : reportID;
+ const currentReportAction = ReportActionsUtils.getReportAction(reportID, reportAction.reportActionID);
+ return isThreadFirstChat(reportAction, reportID) && _.isEmpty(currentReportAction) ? lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, 'parentReportID']) : reportID;
}
/**
@@ -3792,29 +3879,6 @@ function getParticipantsIDs(report) {
return participants;
}
-/**
- * Get the last 3 transactions with receipts of an IOU report that will be displayed on the report preview
- *
- * @param {Object} reportPreviewAction
- * @returns {Object}
- */
-function getReportPreviewDisplayTransactions(reportPreviewAction) {
- const transactionIDs = lodashGet(reportPreviewAction, ['childRecentReceiptTransactionIDs']);
- return _.reduce(
- _.keys(transactionIDs),
- (transactions, transactionID) => {
- if (transactionIDs[transactionID] !== null) {
- const transaction = TransactionUtils.getTransaction(transactionID);
- if (TransactionUtils.hasReceipt(transaction)) {
- transactions.push(transaction);
- }
- }
- return transactions;
- },
- [],
- );
-}
-
/**
* Return iou report action display message
*
@@ -3828,7 +3892,7 @@ function getIOUReportActionDisplayMessage(reportAction) {
const {amount, currency, IOUReportID} = originalMessage;
const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency);
const iouReport = getReport(IOUReportID);
- const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport.managerID);
+ const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport.managerID, true);
let translationKey;
switch (originalMessage.paymentType) {
case CONST.IOU.PAYMENT_TYPE.ELSEWHERE:
@@ -3863,6 +3927,14 @@ function isReportDraft(report) {
return isExpenseReport(report) && lodashGet(report, 'stateNum') === CONST.REPORT.STATE_NUM.OPEN && lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.OPEN;
}
+/**
+ * @param {Object} report
+ * @returns {Boolean}
+ */
+function shouldUseFullTitleToDisplay(report) {
+ return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report);
+}
+
export {
getReportParticipantsTitle,
isReportMessageAttachment,
@@ -3895,7 +3967,8 @@ export {
hasExpensifyGuidesEmails,
isWaitingForIOUActionFromCurrentUser,
isIOUOwnedByCurrentUser,
- getMoneyRequestTotal,
+ getMoneyRequestReimbursableTotal,
+ getMoneyRequestSpendBreakdown,
canShowReportRecipientLocalTime,
formatReportLastMessageText,
chatIncludesConcierge,
@@ -3910,6 +3983,8 @@ export {
getReport,
getReportIDFromLink,
getRouteFromLink,
+ getDeletedParentActionMessageForChatReport,
+ getLastVisibleMessage,
navigateToDetailsPage,
generateReportID,
hasReportNameError,
@@ -4002,11 +4077,11 @@ export {
canEditMoneyRequest,
buildTransactionThread,
areAllRequestsBeingSmartScanned,
- getReportPreviewDisplayTransactions,
getTransactionsWithReceipts,
hasNonReimbursableTransactions,
hasMissingSmartscanFields,
getIOUReportActionDisplayMessage,
isWaitingForTaskCompleteFromAssignee,
isReportDraft,
+ shouldUseFullTitleToDisplay,
};
diff --git a/src/libs/Request.ts b/src/libs/Request.ts
index 903e70358da9..9c4af4aa7e18 100644
--- a/src/libs/Request.ts
+++ b/src/libs/Request.ts
@@ -3,24 +3,24 @@ import enhanceParameters from './Network/enhanceParameters';
import * as NetworkStore from './Network/NetworkStore';
import Request from '../types/onyx/Request';
import Response from '../types/onyx/Response';
-
-type Middleware = (response: Promise, request: Request, isFromSequentialQueue: boolean) => Promise;
+import Middleware from './Middleware/types';
let middlewares: Middleware[] = [];
-function makeXHR(request: Request): Promise {
+function makeXHR(request: Request): Promise {
const finalParameters = enhanceParameters(request.command, request?.data ?? {});
- return NetworkStore.hasReadRequiredDataFromStorage().then(() => {
+ return NetworkStore.hasReadRequiredDataFromStorage().then((): Promise => {
// If we're using the Supportal token and this is not a Supportal request
// let's just return a promise that will resolve itself.
if (NetworkStore.getSupportAuthToken() && !NetworkStore.isSupportRequest(request.command)) {
return new Promise((resolve) => resolve());
}
- return HttpUtils.xhr(request.command, finalParameters, request.type, request.shouldUseSecure);
- }) as Promise;
+
+ return HttpUtils.xhr(request.command, finalParameters, request.type, request.shouldUseSecure) as Promise;
+ });
}
-function processWithMiddleware(request: Request, isFromSequentialQueue = false): Promise {
+function processWithMiddleware(request: Request, isFromSequentialQueue = false): Promise {
return middlewares.reduce((last, middleware) => middleware(last, request, isFromSequentialQueue), makeXHR(request));
}
diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js
index 7a32db660021..dd6db33902fb 100644
--- a/src/libs/SidebarUtils.js
+++ b/src/libs/SidebarUtils.js
@@ -158,7 +158,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p
report.displayName = ReportUtils.getReportName(report);
// eslint-disable-next-line no-param-reassign
- report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReportsDict);
+ report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReportsDict);
});
// The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order:
@@ -347,17 +347,17 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale,
if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport) && !result.isArchivedRoom) {
const lastAction = visibleReportActionItems[report.reportID];
- if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
+ if (lastAction && lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
const newName = lodashGet(lastAction, 'originalMessage.newName', '');
result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName});
- } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED) {
+ } else if (lastAction && lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED) {
result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.reopened')}`;
- } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED) {
+ } else if (lastAction && lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED) {
result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.completed')}`;
- } else if (lodashGet(lastAction, 'actionName', '') !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
+ } else if (lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
result.alternateText = `${lastActorDisplayName}: ${lastMessageText}`;
} else {
- result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet');
+ result.alternateText = lastAction && lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet');
}
} else {
if (!lastMessageText) {
@@ -384,7 +384,7 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale,
}
result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result);
- result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result);
+ result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result);
if (!hasMultipleParticipants) {
result.accountID = personalDetail.accountID;
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 393de4e0d6e3..6a45bef5780b 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -76,11 +76,18 @@ function buildOptimisticTransaction(
};
}
+/**
+ * Check if the transaction has an Ereceipt
+ */
+function hasEReceipt(transaction: Transaction | undefined | null): boolean {
+ return !!transaction?.hasEReceipt;
+}
+
function hasReceipt(transaction: Transaction | undefined | null): boolean {
- return !!transaction?.receipt?.state;
+ return !!transaction?.receipt?.state || hasEReceipt(transaction);
}
-function areRequiredFieldsEmpty(transaction: Transaction): boolean {
+function isMerchantMissing(transaction: Transaction) {
const isMerchantEmpty =
transaction.merchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction.merchant === '';
@@ -90,10 +97,19 @@ function areRequiredFieldsEmpty(transaction: Transaction): boolean {
transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ||
transaction.modifiedMerchant === '';
- const isModifiedAmountEmpty = !transaction.modifiedAmount || transaction.modifiedAmount === 0;
- const isModifiedCreatedEmpty = !transaction.modifiedCreated || transaction.modifiedCreated === '';
+ return isMerchantEmpty && isModifiedMerchantEmpty;
+}
+
+function isAmountMissing(transaction: Transaction) {
+ return transaction.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0);
+}
- return (isModifiedMerchantEmpty && isMerchantEmpty) || (isModifiedAmountEmpty && transaction.amount === 0) || (isModifiedCreatedEmpty && transaction.created === '');
+function isCreatedMissing(transaction: Transaction) {
+ return transaction.created === '' && (!transaction.created || transaction.modifiedCreated === '');
+}
+
+function areRequiredFieldsEmpty(transaction: Transaction): boolean {
+ return isMerchantMissing(transaction) || isAmountMissing(transaction) || isCreatedMissing(transaction);
}
/**
@@ -356,13 +372,6 @@ function hasRoute(transaction: Transaction): boolean {
return !!transaction?.routes?.route0?.geometry?.coordinates;
}
-/**
- * Check if the transaction has an Ereceipt
- */
-function hasEreceipt(transaction: Transaction): boolean {
- return !!transaction?.hasEReceipt;
-}
-
/**
* Get the transactions related to a report preview with receipts
* Get the details linked to the IOU reportAction
@@ -463,7 +472,7 @@ export {
getLinkedTransaction,
getAllReportTransactions,
hasReceipt,
- hasEreceipt,
+ hasEReceipt,
hasRoute,
isReceiptBeingScanned,
getValidWaypoints,
@@ -472,6 +481,9 @@ export {
isPending,
isPosted,
getWaypoints,
+ isAmountMissing,
+ isMerchantMissing,
+ isCreatedMissing,
areRequiredFieldsEmpty,
hasMissingSmartscanFields,
getWaypointIndex,
diff --git a/src/libs/UpdateMultilineInputRange/index.ios.js b/src/libs/UpdateMultilineInputRange/index.ios.js
index 85ed529a33bc..4c10f768a2a2 100644
--- a/src/libs/UpdateMultilineInputRange/index.ios.js
+++ b/src/libs/UpdateMultilineInputRange/index.ios.js
@@ -8,8 +8,9 @@
* See https://github.com/Expensify/App/issues/20836 for more details.
*
* @param {Object} input the input element
+ * @param {boolean} shouldAutoFocus
*/
-export default function updateMultilineInputRange(input) {
+export default function updateMultilineInputRange(input, shouldAutoFocus = true) {
if (!input) {
return;
}
@@ -19,5 +20,7 @@ export default function updateMultilineInputRange(input) {
* Issue: does not scroll multiline input when text exceeds the maximum number of lines
* For more details: https://github.com/Expensify/App/pull/27702#issuecomment-1728651132
*/
- input.focus();
+ if (shouldAutoFocus) {
+ input.focus();
+ }
}
diff --git a/src/libs/UpdateMultilineInputRange/index.js b/src/libs/UpdateMultilineInputRange/index.js
index 179d30dc611d..66fb1889be21 100644
--- a/src/libs/UpdateMultilineInputRange/index.js
+++ b/src/libs/UpdateMultilineInputRange/index.js
@@ -8,8 +8,10 @@
* See https://github.com/Expensify/App/issues/20836 for more details.
*
* @param {Object} input the input element
+ * @param {boolean} shouldAutoFocus
*/
-export default function updateMultilineInputRange(input) {
+// eslint-disable-next-line no-unused-vars
+export default function updateMultilineInputRange(input, shouldAutoFocus = true) {
if (!input) {
return;
}
diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js
index a1d64154906c..75520d483f98 100644
--- a/src/libs/actions/App.js
+++ b/src/libs/actions/App.js
@@ -350,6 +350,40 @@ function createWorkspaceAndNavigateToIt(policyOwnerEmail = '', makeMeAdmin = fal
.then(endSignOnTransition);
}
+/**
+ * Create a new draft workspace and navigate to it
+ *
+ * @param {String} [policyOwnerEmail] Optional, the email of the account to make the owner of the policy
+ * @param {String} [policyName] Optional, custom policy name we will use for created workspace
+ * @param {Boolean} [transitionFromOldDot] Optional, if the user is transitioning from old dot
+ */
+function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', policyName = '', transitionFromOldDot = false) {
+ const policyID = Policy.generatePolicyID();
+ Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID);
+
+ Navigation.isNavigationReady()
+ .then(() => {
+ if (transitionFromOldDot) {
+ // We must call goBack() to remove the /transition route from history
+ Navigation.goBack(ROUTES.HOME);
+ }
+ Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID));
+ })
+ .then(endSignOnTransition);
+}
+
+/**
+ * Create a new workspace and delete the draft
+ *
+ * @param {String} [policyID] the ID of the policy to use
+ * @param {String} [policyName] custom policy name we will use for created workspace
+ * @param {String} [policyOwnerEmail] Optional, the email of the account to make the owner of the policy
+ * @param {Boolean} [makeMeAdmin] Optional, leave the calling account as an admin on the policy
+ */
+function savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail = '', makeMeAdmin = false) {
+ Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID);
+}
+
/**
* This action runs when the Navigator is ready and the current route changes
*
@@ -389,9 +423,6 @@ function setUpPoliciesAndNavigate(session, shouldNavigateToAdminChat) {
// Sign out the current user if we're transitioning with a different user
const isTransitioning = Str.startsWith(url.pathname, Str.normalizeUrl(ROUTES.TRANSITION_BETWEEN_APPS));
- if (isLoggingInAsNewUser && isTransitioning) {
- Session.signOut();
- }
const shouldCreateFreePolicy = !isLoggingInAsNewUser && isTransitioning && exitTo === ROUTES.WORKSPACE_NEW;
if (shouldCreateFreePolicy) {
@@ -527,4 +558,6 @@ export {
createWorkspaceAndNavigateToIt,
getMissingOnyxUpdates,
finalReconnectAppAfterActivatingReliableUpdates,
+ savePolicyDraftByNewWorkspace,
+ createWorkspaceWithPolicyDraftAndNavigateToIt,
};
diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.ts
similarity index 55%
rename from src/libs/actions/BankAccounts.js
rename to src/libs/actions/BankAccounts.ts
index 4d3c880b5983..249d7de9293a 100644
--- a/src/libs/actions/BankAccounts.js
+++ b/src/libs/actions/BankAccounts.ts
@@ -7,6 +7,10 @@ import * as PlaidDataProps from '../../pages/ReimbursementAccount/plaidDataPropT
import Navigation from '../Navigation/Navigation';
import ROUTES from '../../ROUTES';
import * as ReimbursementAccount from './ReimbursementAccount';
+import type PlaidBankAccount from '../../types/onyx/PlaidBankAccount';
+import type {ACHContractStepProps, BankAccountStepProps, CompanyStepProps, OnfidoData, ReimbursementAccountProps, RequestorStepProps} from '../../types/onyx/ReimbursementAccountDraft';
+import type {OnyxData} from '../../types/onyx/Request';
+import type {BankAccountStep, BankAccountSubStep} from '../../types/onyx/ReimbursementAccount';
export {
goToWithdrawalAccountSetupStep,
@@ -23,7 +27,13 @@ export {
export {openPlaidBankAccountSelector, openPlaidBankLogin} from './Plaid';
export {openOnfidoFlow, answerQuestionsForWallet, verifyIdentity, acceptWalletTerms} from './Wallet';
-function clearPlaid() {
+type BankAccountCompanyInformation = BankAccountStepProps & CompanyStepProps & ReimbursementAccountProps;
+
+type ReimbursementAccountStep = BankAccountStep | '';
+
+type ReimbursementAccountSubStep = BankAccountSubStep | '';
+
+function clearPlaid(): Promise {
Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, '');
return Onyx.set(ONYXKEYS.PLAID_DATA, PlaidDataProps.plaidDataDefaultProps);
@@ -35,9 +45,8 @@ function openPlaidView() {
/**
* Open the personal bank account setup flow, with an optional exitReportID to redirect to once the flow is finished.
- * @param {String} exitReportID
*/
-function openPersonalBankAccountSetupView(exitReportID) {
+function openPersonalBankAccountSetupView(exitReportID: string) {
clearPlaid().then(() => {
if (exitReportID) {
Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {exitReportID});
@@ -57,10 +66,8 @@ function clearOnfidoToken() {
/**
* Helper method to build the Onyx data required during setup of a Verified Business Bank Account
- * @param {String | undefined} currentStep The name of the bank account setup step for which we will update the draft value when we receive the response from the API.
- * @returns {Object}
*/
-function getVBBADataForOnyx(currentStep = undefined) {
+function getVBBADataForOnyx(currentStep?: BankAccountStep): OnyxData {
return {
optimisticData: [
{
@@ -103,14 +110,20 @@ function getVBBADataForOnyx(currentStep = undefined) {
/**
* Submit Bank Account step with Plaid data so php can perform some checks.
- *
- * @param {Number} bankAccountID
- * @param {Object} selectedPlaidBankAccount
*/
-function connectBankAccountWithPlaid(bankAccountID, selectedPlaidBankAccount) {
+function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount) {
const commandName = 'ConnectBankAccountWithPlaid';
- const parameters = {
+ type ConnectBankAccountWithPlaidParams = {
+ bankAccountID: number;
+ routingNumber: string;
+ accountNumber: string;
+ bank?: string;
+ plaidAccountID: string;
+ plaidAccessToken: string;
+ };
+
+ const parameters: ConnectBankAccountWithPlaidParams = {
bankAccountID,
routingNumber: selectedPlaidBankAccount.routingNumber,
accountNumber: selectedPlaidBankAccount.accountNumber,
@@ -125,13 +138,23 @@ function connectBankAccountWithPlaid(bankAccountID, selectedPlaidBankAccount) {
/**
* Adds a bank account via Plaid
*
- * @param {Object} account
* @TODO offline pattern for this command will have to be added later once the pattern B design doc is complete
*/
-function addPersonalBankAccount(account) {
+function addPersonalBankAccount(account: PlaidBankAccount) {
const commandName = 'AddPersonalBankAccount';
- const parameters = {
+ type AddPersonalBankAccountParams = {
+ addressName: string;
+ routingNumber: string;
+ accountNumber: string;
+ isSavings: boolean;
+ setupType: string;
+ bank?: string;
+ plaidAccountID: string;
+ plaidAccessToken: string;
+ };
+
+ const parameters: AddPersonalBankAccountParams = {
addressName: account.addressName,
routingNumber: account.routingNumber,
accountNumber: account.accountNumber,
@@ -142,7 +165,7 @@ function addPersonalBankAccount(account) {
plaidAccessToken: account.plaidAccessToken,
};
- const onyxData = {
+ const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -180,107 +203,94 @@ function addPersonalBankAccount(account) {
API.write(commandName, parameters, onyxData);
}
-function deletePaymentBankAccount(bankAccountID) {
- API.write(
- 'DeletePaymentBankAccount',
- {
- bankAccountID,
- },
- {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.BANK_ACCOUNT_LIST}`,
- value: {[bankAccountID]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}},
- },
- ],
-
- // Sometimes pusher updates aren't received when we close the App while still offline,
- // so we are setting the bankAccount to null here to ensure that it gets cleared out once we come back online.
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.BANK_ACCOUNT_LIST}`,
- value: {[bankAccountID]: null},
- },
- ],
- },
- );
+function deletePaymentBankAccount(bankAccountID: number) {
+ type DeletePaymentBankAccountParams = {bankAccountID: number};
+
+ const parameters: DeletePaymentBankAccountParams = {bankAccountID};
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.BANK_ACCOUNT_LIST}`,
+ value: {[bankAccountID]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}},
+ },
+ ],
+
+ // Sometimes pusher updates aren't received when we close the App while still offline,
+ // so we are setting the bankAccount to null here to ensure that it gets cleared out once we come back online.
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.BANK_ACCOUNT_LIST}`,
+ value: {[bankAccountID]: null},
+ },
+ ],
+ };
+
+ API.write('DeletePaymentBankAccount', parameters, onyxData);
}
/**
* Update the user's personal information on the bank account in database.
*
* This action is called by the requestor step in the Verified Bank Account flow
- *
- * @param {Object} params
- *
- * @param {String} [params.dob]
- * @param {String} [params.firstName]
- * @param {String} [params.lastName]
- * @param {String} [params.requestorAddressStreet]
- * @param {String} [params.requestorAddressCity]
- * @param {String} [params.requestorAddressState]
- * @param {String} [params.requestorAddressZipCode]
- * @param {String} [params.ssnLast4]
- * @param {String} [params.isControllingOfficer]
- * @param {Object} [params.onfidoData]
- * @param {Boolean} [params.isOnfidoSetupComplete]
*/
-function updatePersonalInformationForBankAccount(params) {
+function updatePersonalInformationForBankAccount(params: RequestorStepProps) {
API.write('UpdatePersonalInformationForBankAccount', params, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR));
}
-/**
- * @param {Number} bankAccountID
- * @param {String} validateCode
- */
-function validateBankAccount(bankAccountID, validateCode) {
- API.write(
- 'ValidateBankAccountWithTransactions',
- {
- bankAccountID,
- validateCode,
- },
- {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: true,
- errors: null,
- },
+function validateBankAccount(bankAccountID: number, validateCode: string) {
+ type ValidateBankAccountWithTransactionsParams = {
+ bankAccountID: number;
+ validateCode: string;
+ };
+
+ const parameters: ValidateBankAccountWithTransactionsParams = {
+ bankAccountID,
+ validateCode,
+ };
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: true,
+ errors: null,
},
- ],
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: false,
- },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
},
- ],
- failureData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: false,
- },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
},
- ],
- },
- );
+ },
+ ],
+ };
+
+ API.write('ValidateBankAccountWithTransactions', parameters, onyxData);
}
function clearReimbursementAccount() {
Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, null);
}
-function openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep) {
- const onyxData = {
+function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subStep: ReimbursementAccountSubStep, localCurrentStep: ReimbursementAccountStep) {
+ const onyxData: OnyxData = {
optimisticData: [
{
onyxMethod: Onyx.METHOD.MERGE,
@@ -310,122 +320,104 @@ function openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep) {
],
};
- const param = {
+ type OpenReimbursementAccountPageParams = {
+ stepToOpen: ReimbursementAccountStep;
+ subStep: ReimbursementAccountSubStep;
+ localCurrentStep: ReimbursementAccountStep;
+ };
+
+ const parameters: OpenReimbursementAccountPageParams = {
stepToOpen,
subStep,
localCurrentStep,
};
- return API.read('OpenReimbursementAccountPage', param, onyxData);
+ return API.read('OpenReimbursementAccountPage', parameters, onyxData);
}
/**
* Updates the bank account in the database with the company step data
- *
- * @param {Object} bankAccount
- * @param {Number} [bankAccount.bankAccountID]
- *
- * Fields from BankAccount step
- * @param {String} [bankAccount.routingNumber]
- * @param {String} [bankAccount.accountNumber]
- * @param {String} [bankAccount.bankName]
- * @param {String} [bankAccount.plaidAccountID]
- * @param {String} [bankAccount.plaidAccessToken]
- * @param {Boolean} [bankAccount.isSavings]
- *
- * Fields from Company step
- * @param {String} [bankAccount.companyName]
- * @param {String} [bankAccount.addressStreet]
- * @param {String} [bankAccount.addressCity]
- * @param {String} [bankAccount.addressState]
- * @param {String} [bankAccount.addressZipCode]
- * @param {String} [bankAccount.companyPhone]
- * @param {String} [bankAccount.website]
- * @param {String} [bankAccount.companyTaxID]
- * @param {String} [bankAccount.incorporationType]
- * @param {String} [bankAccount.incorporationState]
- * @param {String} [bankAccount.incorporationDate]
- * @param {Boolean} [bankAccount.hasNoConnectionToCannabis]
- * @param {String} policyID
*/
-function updateCompanyInformationForBankAccount(bankAccount, policyID) {
- API.write('UpdateCompanyInformationForBankAccount', {...bankAccount, policyID}, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY));
+function updateCompanyInformationForBankAccount(bankAccount: BankAccountCompanyInformation, policyID: string) {
+ type UpdateCompanyInformationForBankAccountParams = BankAccountCompanyInformation & {policyID: string};
+
+ const parameters: UpdateCompanyInformationForBankAccountParams = {...bankAccount, policyID};
+
+ API.write('UpdateCompanyInformationForBankAccount', parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY));
}
/**
* Add beneficial owners for the bank account, accept the ACH terms and conditions and verify the accuracy of the information provided
- *
- * @param {Object} params
- *
- * // ACH Contract Step
- * @param {Boolean} [params.ownsMoreThan25Percent]
- * @param {Boolean} [params.hasOtherBeneficialOwners]
- * @param {Boolean} [params.acceptTermsAndConditions]
- * @param {Boolean} [params.certifyTrueInformation]
- * @param {String} [params.beneficialOwners]
*/
-function updateBeneficialOwnersForBankAccount(params) {
- API.write('UpdateBeneficialOwnersForBankAccount', {...params}, getVBBADataForOnyx());
+function updateBeneficialOwnersForBankAccount(params: ACHContractStepProps) {
+ API.write('UpdateBeneficialOwnersForBankAccount', params, getVBBADataForOnyx());
}
/**
* Create the bank account with manually entered data.
*
- * @param {number} [bankAccountID]
- * @param {String} [accountNumber]
- * @param {String} [routingNumber]
- * @param {String} [plaidMask]
- *
*/
-function connectBankAccountManually(bankAccountID, accountNumber, routingNumber, plaidMask) {
- API.write(
- 'ConnectBankAccountManually',
- {
- bankAccountID,
- accountNumber,
- routingNumber,
- plaidMask,
- },
- getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT),
- );
+function connectBankAccountManually(bankAccountID: number, accountNumber?: string, routingNumber?: string, plaidMask?: string) {
+ type ConnectBankAccountManuallyParams = {
+ bankAccountID: number;
+ accountNumber?: string;
+ routingNumber?: string;
+ plaidMask?: string;
+ };
+
+ const parameters: ConnectBankAccountManuallyParams = {
+ bankAccountID,
+ accountNumber,
+ routingNumber,
+ plaidMask,
+ };
+
+ API.write('ConnectBankAccountManually', parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT));
}
/**
* Verify the user's identity via Onfido
- *
- * @param {Number} bankAccountID
- * @param {Object} onfidoData
*/
-function verifyIdentityForBankAccount(bankAccountID, onfidoData) {
- API.write(
- 'VerifyIdentityForBankAccount',
- {
- bankAccountID,
- onfidoData: JSON.stringify(onfidoData),
- },
- getVBBADataForOnyx(),
- );
+function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoData) {
+ type VerifyIdentityForBankAccountParams = {
+ bankAccountID: number;
+ onfidoData: string;
+ };
+
+ const parameters: VerifyIdentityForBankAccountParams = {
+ bankAccountID,
+ onfidoData: JSON.stringify(onfidoData),
+ };
+
+ API.write('VerifyIdentityForBankAccount', parameters, getVBBADataForOnyx());
}
function openWorkspaceView() {
- API.read('OpenWorkspaceView');
+ API.read('OpenWorkspaceView', {}, {});
}
-function handlePlaidError(bankAccountID, error, error_description, plaidRequestID) {
- API.write('BankAccount_HandlePlaidError', {
+function handlePlaidError(bankAccountID: number, error: string, errorDescription: string, plaidRequestID: string) {
+ type BankAccountHandlePlaidErrorParams = {
+ bankAccountID: number;
+ error: string;
+ errorDescription: string;
+ plaidRequestID: string;
+ };
+
+ const parameters: BankAccountHandlePlaidErrorParams = {
bankAccountID,
error,
- error_description,
+ errorDescription,
plaidRequestID,
- });
+ };
+
+ API.write('BankAccount_HandlePlaidError', parameters);
}
/**
* Set the reimbursement account loading so that it happens right away, instead of when the API command is processed.
- *
- * @param {Boolean} isLoading
*/
-function setReimbursementAccountLoading(isLoading) {
+function setReimbursementAccountLoading(isLoading: boolean) {
Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading});
}
diff --git a/src/libs/actions/Chronos.ts b/src/libs/actions/Chronos.ts
index 1b46a68a1afe..ce821e524722 100644
--- a/src/libs/actions/Chronos.ts
+++ b/src/libs/actions/Chronos.ts
@@ -1,11 +1,11 @@
-import Onyx from 'react-native-onyx';
+import Onyx, {OnyxUpdate} from 'react-native-onyx';
import CONST from '../../CONST';
import ONYXKEYS from '../../ONYXKEYS';
import * as API from '../API';
import {ChronosOOOEvent} from '../../types/onyx/OriginalMessage';
const removeEvent = (reportID: string, reportActionID: string, eventID: string, events: ChronosOOOEvent[]) => {
- const optimisticData = [
+ const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
@@ -20,7 +20,7 @@ const removeEvent = (reportID: string, reportActionID: string, eventID: string,
},
];
- const successData = [
+ const successData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
@@ -32,7 +32,7 @@ const removeEvent = (reportID: string, reportActionID: string, eventID: string,
},
];
- const failureData = [
+ const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
diff --git a/src/libs/actions/DemoActions.js b/src/libs/actions/DemoActions.js
new file mode 100644
index 000000000000..bbc46716b267
--- /dev/null
+++ b/src/libs/actions/DemoActions.js
@@ -0,0 +1,70 @@
+import Config from 'react-native-config';
+import Onyx from 'react-native-onyx';
+import lodashGet from 'lodash/get';
+import * as API from '../API';
+import * as ReportUtils from '../ReportUtils';
+import Navigation from '../Navigation/Navigation';
+import ROUTES from '../../ROUTES';
+import ONYXKEYS from '../../ONYXKEYS';
+
+let currentUserEmail;
+Onyx.connect({
+ key: ONYXKEYS.SESSION,
+ callback: (val) => {
+ currentUserEmail = lodashGet(val, 'email', '');
+ },
+});
+
+function runMoney2020Demo() {
+ // Try to navigate to existing demo chat if it exists in Onyx
+ const money2020AccountID = Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_MONEY2020', 15864555));
+ const existingChatReport = ReportUtils.getChatByParticipants([money2020AccountID]);
+ if (existingChatReport) {
+ // We must call goBack() to remove the demo route from nav history
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(existingChatReport.reportID));
+ return;
+ }
+
+ // We use makeRequestWithSideEffects here because we need to get the chat report ID to navigate to it after it's created
+ // eslint-disable-next-line rulesdir/no-api-side-effects-method
+ API.makeRequestWithSideEffects('CreateChatReport', {
+ emailList: `${currentUserEmail},money2020@expensify.com`,
+ activationConference: 'money2020',
+ }).then((response) => {
+ // If there's no response or no reportID in the response, navigate the user home so user doesn't get stuck.
+ if (!response || !response.reportID) {
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.HOME);
+ return;
+ }
+
+ // Get reportID & navigate to it
+ // Note: We must call goBack() to remove the demo route from history
+ const chatReportID = response.reportID;
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chatReportID));
+ });
+}
+
+/**
+ * Runs code for specific demos, based on the provided URL
+ *
+ * @param {String} url - URL user is navigating to via deep link (or regular link in web)
+ */
+function runDemoByURL(url = '') {
+ const cleanUrl = (url || '').toLowerCase();
+
+ if (cleanUrl.endsWith(ROUTES.MONEY2020)) {
+ Onyx.set(ONYXKEYS.DEMO_INFO, {
+ money2020: {
+ isBeginningDemo: true,
+ },
+ });
+ } else {
+ // No demo is being run, so clear out demo info
+ Onyx.set(ONYXKEYS.DEMO_INFO, null);
+ }
+}
+
+export {runMoney2020Demo, runDemoByURL};
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index a95d69243ec8..b3fa78d07614 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -1,6 +1,7 @@
import Onyx from 'react-native-onyx';
import _ from 'underscore';
import lodashGet from 'lodash/get';
+import lodashHas from 'lodash/has';
import Str from 'expensify-common/lib/str';
import {format} from 'date-fns';
import CONST from '../../CONST';
@@ -1064,6 +1065,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
let oneOnOneChatReport;
let isNewOneOnOneChatReport = false;
let shouldCreateOptimisticPersonalDetails = false;
+ const personalDetailExists = lodashHas(allPersonalDetails, accountID);
// If this is a split between two people only and the function
// wasn't provided with an existing group chat report id
@@ -1072,11 +1074,11 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
// entering code that creates optimistic personal details
if ((!hasMultipleParticipants && !existingSplitChatReportID) || isOwnPolicyExpenseChat) {
oneOnOneChatReport = splitChatReport;
- shouldCreateOptimisticPersonalDetails = !existingSplitChatReport;
+ shouldCreateOptimisticPersonalDetails = !existingSplitChatReport && !personalDetailExists;
} else {
const existingChatReport = ReportUtils.getChatByParticipants([accountID]);
isNewOneOnOneChatReport = !existingChatReport;
- shouldCreateOptimisticPersonalDetails = isNewOneOnOneChatReport;
+ shouldCreateOptimisticPersonalDetails = isNewOneOnOneChatReport && !personalDetailExists;
oneOnOneChatReport = existingChatReport || ReportUtils.buildOptimisticChatReport([accountID]);
}
@@ -1303,7 +1305,18 @@ function startSplitBill(participants, currentUserLogin, currentUserAccountID, co
const receiptObject = {state, source};
// ReportID is -2 (aka "deleted") on the group transaction
- const splitTransaction = TransactionUtils.buildOptimisticTransaction(0, CONST.CURRENCY.USD, CONST.REPORT.SPLIT_REPORTID, comment, '', '', '', '', receiptObject, filename);
+ const splitTransaction = TransactionUtils.buildOptimisticTransaction(
+ 0,
+ CONST.CURRENCY.USD,
+ CONST.REPORT.SPLIT_REPORTID,
+ comment,
+ '',
+ '',
+ '',
+ CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT,
+ receiptObject,
+ filename,
+ );
// Note: The created action must be optimistically generated before the IOU action so there's no chance that the created action appears after the IOU action in the chat
const splitChatCreatedReportAction = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit);
@@ -1419,7 +1432,7 @@ function startSplitBill(participants, currentUserLogin, currentUserAccountID, co
errors: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
},
[splitIOUReportAction.reportActionID]: {
- errors: ErrorUtils.getMicroSecondOnyxError('report.genericCreateFailureMessage'),
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
},
},
},
@@ -1688,15 +1701,23 @@ function completeSplitBill(chatReportID, reportAction, updatedTransaction, sessi
failureData.push(...oneOnOneFailureData);
});
+ const {
+ amount: transactionAmount,
+ currency: transactionCurrency,
+ created: transactionCreated,
+ merchant: transactionMerchant,
+ comment: transactionComment,
+ } = ReportUtils.getTransactionDetails(updatedTransaction);
+
API.write(
'CompleteSplitBill',
{
transactionID,
- amount: updatedTransaction.modifiedAmount,
- currency: updatedTransaction.modifiedCurrency,
- created: updatedTransaction.modifiedCreated,
- merchant: updatedTransaction.modifiedMerchant,
- comment: updatedTransaction.comment.comment,
+ amount: transactionAmount,
+ currency: transactionCurrency,
+ created: transactionCreated,
+ merchant: transactionMerchant,
+ comment: transactionComment,
splits: JSON.stringify(splits),
},
{optimisticData, successData, failureData},
@@ -2626,7 +2647,7 @@ function submitReport(expenseReport) {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
value: {
- state: CONST.REPORT.STATE.OPEN,
+ statusNum: CONST.REPORT.STATUS.OPEN,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
},
},
diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js
index 1a73b148e100..89324dd35485 100644
--- a/src/libs/actions/Policy.js
+++ b/src/libs/actions/Policy.js
@@ -73,6 +73,13 @@ Onyx.connect({
callback: (val) => (allRecentlyUsedCategories = val),
});
+let networkStatus = {};
+Onyx.connect({
+ key: ONYXKEYS.NETWORK,
+ waitForCollectionCallback: true,
+ callback: (val) => (networkStatus = val),
+});
+
/**
* Stores in Onyx the policy ID of the last workspace that was accessed by the user
* @param {String|null} policyID
@@ -766,7 +773,7 @@ function updateWorkspaceCustomUnitAndRate(policyID, currentCustomUnit, newCustom
'UpdateWorkspaceCustomUnitAndRate',
{
policyID,
- lastModified,
+ ...(!networkStatus.isOffline && {lastModified}),
customUnit: JSON.stringify(newCustomUnitParam),
customUnitRate: JSON.stringify(newCustomUnitParam.rates),
},
@@ -909,6 +916,48 @@ function buildOptimisticCustomUnits() {
};
}
+/**
+ * Optimistically creates a Policy Draft for a new workspace
+ *
+ * @param {String} [policyOwnerEmail] Optional, the email of the account to make the owner of the policy
+ * @param {String} [policyName] Optional, custom policy name we will use for created workspace
+ * @param {String} [policyID] Optional, custom policy id we will use for created workspace
+ */
+function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID()) {
+ const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail);
+ const {customUnits} = buildOptimisticCustomUnits();
+
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`,
+ value: {
+ id: policyID,
+ type: CONST.POLICY.TYPE.FREE,
+ name: workspaceName,
+ role: CONST.POLICY.ROLE.ADMIN,
+ owner: sessionEmail,
+ isPolicyExpenseChatEnabled: true,
+ outputCurrency: lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD),
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ customUnits,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${policyID}`,
+ value: {
+ [sessionAccountID]: {
+ role: CONST.POLICY.ROLE.ADMIN,
+ errors: {},
+ },
+ },
+ },
+ ];
+
+ Onyx.update(optimisticData);
+}
+
/**
* Optimistically creates a new workspace and default workspace chats
*
@@ -1027,6 +1076,16 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`,
value: expenseReportActionData,
},
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${policyID}`,
+ value: null,
+ },
],
successData: [
{
@@ -1131,6 +1190,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName
],
},
);
+
return adminsChatReportID;
}
@@ -1259,4 +1319,5 @@ export {
clearErrors,
openDraftWorkspaceRequest,
buildOptimisticPolicyRecentlyUsedCategories,
+ createDraftInitialWorkspace,
};
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index c56e9c567745..c9f3ba6318db 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -1054,7 +1054,7 @@ function deleteReportComment(reportID, reportAction) {
isLastMessageDeletedParentAction: true,
};
} else {
- const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportActionsUtils.getLastVisibleMessage(originalReportID, optimisticReportActions);
+ const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportUtils.getLastVisibleMessage(originalReportID, optimisticReportActions);
if (lastMessageText || lastMessageTranslationKey) {
const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions);
const lastVisibleActionCreated = lastVisibleAction.created;
@@ -1889,6 +1889,7 @@ function toggleEmojiReaction(reportID, reportAction, reactionObject, existingRea
* @param {Boolean} isAuthenticated
*/
function openReportFromDeepLink(url, isAuthenticated) {
+ const route = ReportUtils.getRouteFromLink(url);
const reportID = ReportUtils.getReportIDFromLink(url);
if (reportID && !isAuthenticated) {
@@ -1907,16 +1908,11 @@ function openReportFromDeepLink(url, isAuthenticated) {
// Navigate to the report after sign-in/sign-up.
InteractionManager.runAfterInteractions(() => {
Session.waitForUserSignIn().then(() => {
- Navigation.waitForProtectedRoutes()
- .then(() => {
- const route = ReportUtils.getRouteFromLink(url);
- if (route === ROUTES.CONCIERGE) {
- navigateToConciergeChat(true);
- return;
- }
- Navigation.navigate(route, CONST.NAVIGATION.TYPE.PUSH);
- })
- .catch((error) => Log.warn(error.message));
+ if (route === ROUTES.CONCIERGE) {
+ navigateToConciergeChat(true);
+ return;
+ }
+ Navigation.navigate(route, CONST.NAVIGATION.TYPE.PUSH);
});
});
}
diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js
index 117a092c3875..3b623a42689d 100644
--- a/src/libs/actions/Session/index.js
+++ b/src/libs/actions/Session/index.js
@@ -316,7 +316,7 @@ function signInWithShortLivedAuthToken(email, authToken) {
// If the user is signing in with a different account from the current app, should not pass the auto-generated login as it may be tied to the old account.
// scene 1: the user is transitioning to newDot from a different account on oldDot.
// scene 2: the user is transitioning to desktop app from a different account on web app.
- const oldPartnerUserID = credentials.login === email ? credentials.autoGeneratedLogin : '';
+ const oldPartnerUserID = credentials.login === email && credentials.autoGeneratedLogin ? credentials.autoGeneratedLogin : '';
API.read('SignInWithShortLivedAuthToken', {authToken, oldPartnerUserID, skipReauthentication: true}, {optimisticData, successData, failureData});
}
@@ -541,6 +541,10 @@ function clearAccountMessages() {
});
}
+function setAccountError(error) {
+ Onyx.merge(ONYXKEYS.ACCOUNT, {errors: ErrorUtils.getMicroSecondOnyxError(error)});
+}
+
// It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to
// reconnect each time when we only need to reconnect once. This way, if an authToken is expired and we try to
// subscribe to a bunch of channels at once we will only reauthenticate and force reconnect Pusher once.
@@ -807,6 +811,7 @@ export {
unlinkLogin,
clearSignInData,
clearAccountMessages,
+ setAccountError,
authenticatePusher,
reauthenticatePusher,
invalidateCredentials,
diff --git a/src/libs/actions/Session/updateSessionAuthTokens.js b/src/libs/actions/Session/updateSessionAuthTokens.js
index 5be53c77a92c..e88b3b993c7a 100644
--- a/src/libs/actions/Session/updateSessionAuthTokens.js
+++ b/src/libs/actions/Session/updateSessionAuthTokens.js
@@ -2,8 +2,8 @@ import Onyx from 'react-native-onyx';
import ONYXKEYS from '../../../ONYXKEYS';
/**
- * @param {String} authToken
- * @param {String} encryptedAuthToken
+ * @param {String | undefined} authToken
+ * @param {String | undefined} encryptedAuthToken
*/
export default function updateSessionAuthTokens(authToken, encryptedAuthToken) {
Onyx.merge(ONYXKEYS.SESSION, {authToken, encryptedAuthToken});
diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.ts
similarity index 74%
rename from src/libs/actions/SignInRedirect.js
rename to src/libs/actions/SignInRedirect.ts
index a010621c4eea..67f5f2d8586f 100644
--- a/src/libs/actions/SignInRedirect.js
+++ b/src/libs/actions/SignInRedirect.ts
@@ -1,7 +1,5 @@
import Onyx from 'react-native-onyx';
-import lodashGet from 'lodash/get';
-import _ from 'underscore';
-import ONYXKEYS from '../../ONYXKEYS';
+import ONYXKEYS, {OnyxKey} from '../../ONYXKEYS';
import * as MainQueue from '../Network/MainQueue';
import * as PersistedRequests from './PersistedRequests';
import NetworkConnection from '../NetworkConnection';
@@ -12,27 +10,21 @@ import Navigation from '../Navigation/Navigation';
import * as ErrorUtils from '../ErrorUtils';
import * as SessionUtils from '../SessionUtils';
-let currentIsOffline;
-let currentShouldForceOffline;
+let currentIsOffline: boolean | undefined;
+let currentShouldForceOffline: boolean | undefined;
Onyx.connect({
key: ONYXKEYS.NETWORK,
callback: (network) => {
- if (!network) {
- return;
- }
- currentIsOffline = network.isOffline;
- currentShouldForceOffline = Boolean(network.shouldForceOffline);
+ currentIsOffline = network?.isOffline;
+ currentShouldForceOffline = network?.shouldForceOffline;
},
});
-/**
- * @param {String} errorMessage
- */
-function clearStorageAndRedirect(errorMessage) {
+function clearStorageAndRedirect(errorMessage?: string) {
// Under certain conditions, there are key-values we'd like to keep in storage even when a user is logged out.
// We pass these into the clear() method in order to avoid having to reset them on a delayed tick and getting
// flashes of unwanted default state.
- const keysToPreserve = [];
+ const keysToPreserve: OnyxKey[] = [];
keysToPreserve.push(ONYXKEYS.NVP_PREFERRED_LOCALE);
keysToPreserve.push(ONYXKEYS.ACTIVE_CLIENTS);
keysToPreserve.push(ONYXKEYS.DEVICE_ID);
@@ -58,15 +50,15 @@ function clearStorageAndRedirect(errorMessage) {
*/
function resetHomeRouteParams() {
Navigation.isNavigationReady().then(() => {
- const routes = navigationRef.current && lodashGet(navigationRef.current.getState(), 'routes');
- const homeRoute = _.find(routes, (route) => route.name === SCREENS.HOME);
+ const routes = navigationRef.current?.getState().routes;
+ const homeRoute = routes?.find((route) => route.name === SCREENS.HOME);
- const emptyParams = {};
- _.keys(lodashGet(homeRoute, 'params')).forEach((paramKey) => {
+ const emptyParams: Record = {};
+ Object.keys(homeRoute?.params ?? {}).forEach((paramKey) => {
emptyParams[paramKey] = undefined;
});
- Navigation.setParams(emptyParams, lodashGet(homeRoute, 'key', ''));
+ Navigation.setParams(emptyParams, homeRoute?.key ?? '');
Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false);
});
}
@@ -79,9 +71,9 @@ function resetHomeRouteParams() {
*
* Normally this method would live in Session.js, but that would cause a circular dependency with Network.js.
*
- * @param {String} [errorMessage] error message to be displayed on the sign in page
+ * @param [errorMessage] error message to be displayed on the sign in page
*/
-function redirectToSignIn(errorMessage) {
+function redirectToSignIn(errorMessage?: string) {
MainQueue.clear();
HttpUtils.cancelPendingRequests();
PersistedRequests.clear();
diff --git a/src/libs/actions/Timing.js b/src/libs/actions/Timing.ts
similarity index 76%
rename from src/libs/actions/Timing.js
rename to src/libs/actions/Timing.ts
index 2be2cdc6fa63..13f40bab87c9 100644
--- a/src/libs/actions/Timing.js
+++ b/src/libs/actions/Timing.ts
@@ -4,15 +4,20 @@ import Firebase from '../Firebase';
import * as API from '../API';
import Log from '../Log';
-let timestampData = {};
+type TimestampData = {
+ startTime: number;
+ shouldUseFirebase: boolean;
+};
+
+let timestampData: Record = {};
/**
* Start a performance timing measurement
*
- * @param {String} eventName
- * @param {Boolean} shouldUseFirebase - adds an additional trace in Firebase
+ * @param eventName
+ * @param shouldUseFirebase - adds an additional trace in Firebase
*/
-function start(eventName, shouldUseFirebase = false) {
+function start(eventName: string, shouldUseFirebase = false) {
timestampData[eventName] = {startTime: Date.now(), shouldUseFirebase};
if (!shouldUseFirebase) {
@@ -25,11 +30,11 @@ function start(eventName, shouldUseFirebase = false) {
/**
* End performance timing. Measure the time between event start/end in milliseconds, and push to Grafana
*
- * @param {String} eventName - event name used as timestamp key
- * @param {String} [secondaryName] - optional secondary event name, passed to grafana
- * @param {number} [maxExecutionTime] - optional amount of time (ms) to wait before logging a warn
+ * @param eventName - event name used as timestamp key
+ * @param [secondaryName] - optional secondary event name, passed to grafana
+ * @param [maxExecutionTime] - optional amount of time (ms) to wait before logging a warn
*/
-function end(eventName, secondaryName = '', maxExecutionTime = 0) {
+function end(eventName: string, secondaryName = '', maxExecutionTime = 0) {
if (!timestampData[eventName]) {
return;
}
diff --git a/src/libs/getComponentDisplayName.ts b/src/libs/getComponentDisplayName.ts
index fd1bbcaea521..0bf52d543a84 100644
--- a/src/libs/getComponentDisplayName.ts
+++ b/src/libs/getComponentDisplayName.ts
@@ -1,6 +1,6 @@
import {ComponentType} from 'react';
/** Returns the display name of a component */
-export default function getComponentDisplayName(component: ComponentType): string {
+export default function getComponentDisplayName(component: ComponentType): string {
return component.displayName ?? component.name ?? 'Component';
}
diff --git a/src/libs/localFileDownload/index.android.js b/src/libs/localFileDownload/index.android.ts
similarity index 88%
rename from src/libs/localFileDownload/index.android.js
rename to src/libs/localFileDownload/index.android.ts
index b3e39e7a7a53..ad13b5c5cfa7 100644
--- a/src/libs/localFileDownload/index.android.js
+++ b/src/libs/localFileDownload/index.android.ts
@@ -1,15 +1,13 @@
import RNFetchBlob from 'react-native-blob-util';
import * as FileUtils from '../fileDownload/FileUtils';
+import LocalFileDownload from './types';
/**
* Writes a local file to the app's internal directory with the given fileName
* and textContent, so we're able to copy it to the Android public download dir.
* After the file is copied, it is removed from the internal dir.
- *
- * @param {String} fileName
- * @param {String} textContent
*/
-export default function localFileDownload(fileName, textContent) {
+const localFileDownload: LocalFileDownload = (fileName, textContent) => {
const newFileName = FileUtils.appendTimeToFileName(fileName);
const dir = RNFetchBlob.fs.dirs.DocumentDir;
const path = `${dir}/${newFileName}.txt`;
@@ -34,4 +32,6 @@ export default function localFileDownload(fileName, textContent) {
RNFetchBlob.fs.unlink(path);
});
});
-}
+};
+
+export default localFileDownload;
diff --git a/src/libs/localFileDownload/index.ios.js b/src/libs/localFileDownload/index.ios.ts
similarity index 82%
rename from src/libs/localFileDownload/index.ios.js
rename to src/libs/localFileDownload/index.ios.ts
index 1241f5a535db..3597ea5f6d3c 100644
--- a/src/libs/localFileDownload/index.ios.js
+++ b/src/libs/localFileDownload/index.ios.ts
@@ -1,16 +1,14 @@
import {Share} from 'react-native';
import RNFetchBlob from 'react-native-blob-util';
import * as FileUtils from '../fileDownload/FileUtils';
+import LocalFileDownload from './types';
/**
* Writes a local file to the app's internal directory with the given fileName
* and textContent, so we're able to share it using iOS' share API.
* After the file is shared, it is removed from the internal dir.
- *
- * @param {String} fileName
- * @param {String} textContent
*/
-export default function localFileDownload(fileName, textContent) {
+const localFileDownload: LocalFileDownload = (fileName, textContent) => {
const newFileName = FileUtils.appendTimeToFileName(fileName);
const dir = RNFetchBlob.fs.dirs.DocumentDir;
const path = `${dir}/${newFileName}.txt`;
@@ -20,4 +18,6 @@ export default function localFileDownload(fileName, textContent) {
RNFetchBlob.fs.unlink(path);
});
});
-}
+};
+
+export default localFileDownload;
diff --git a/src/libs/localFileDownload/index.js b/src/libs/localFileDownload/index.ts
similarity index 77%
rename from src/libs/localFileDownload/index.js
rename to src/libs/localFileDownload/index.ts
index 427928834c9c..7b9b4973d5c1 100644
--- a/src/libs/localFileDownload/index.js
+++ b/src/libs/localFileDownload/index.ts
@@ -1,18 +1,18 @@
import * as FileUtils from '../fileDownload/FileUtils';
+import LocalFileDownload from './types';
/**
* Creates a Blob with the given fileName and textContent, then dynamically
* creates a temporary anchor, just to programmatically click it, so the file
* is downloaded by the browser.
- *
- * @param {String} fileName
- * @param {String} textContent
*/
-export default function localFileDownload(fileName, textContent) {
+const localFileDownload: LocalFileDownload = (fileName, textContent) => {
const blob = new Blob([textContent], {type: 'text/plain'});
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.download = FileUtils.appendTimeToFileName(`${fileName}.txt`);
link.href = url;
link.click();
-}
+};
+
+export default localFileDownload;
diff --git a/src/libs/localFileDownload/types.ts b/src/libs/localFileDownload/types.ts
new file mode 100644
index 000000000000..2086e2334d39
--- /dev/null
+++ b/src/libs/localFileDownload/types.ts
@@ -0,0 +1,3 @@
+type LocalFileDownload = (fileName: string, textContent: string) => void;
+
+export default LocalFileDownload;
diff --git a/src/pages/DemoSetupPage.js b/src/pages/DemoSetupPage.js
index 5d4b99a0daf9..5432bea0c806 100644
--- a/src/pages/DemoSetupPage.js
+++ b/src/pages/DemoSetupPage.js
@@ -1,9 +1,11 @@
-import React from 'react';
+import React, {useCallback} from 'react';
import PropTypes from 'prop-types';
import {useFocusEffect} from '@react-navigation/native';
import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';
import Navigation from '../libs/Navigation/Navigation';
import ROUTES from '../ROUTES';
+import CONST from '../CONST';
+import * as DemoActions from '../libs/actions/DemoActions';
const propTypes = {
/** Navigation route context info provided by react navigation */
@@ -18,12 +20,16 @@ const propTypes = {
* route that led the user here. Now, it's just used to route the user home so we
* don't show them a "Hmm... It's not here" message (which looks broken).
*/
-function DemoSetupPage() {
- useFocusEffect(() => {
- Navigation.isNavigationReady().then(() => {
- Navigation.goBack(ROUTES.HOME);
- });
- });
+function DemoSetupPage(props) {
+ useFocusEffect(
+ useCallback(() => {
+ if (props.route.name === CONST.DEMO_PAGES.MONEY2020) {
+ DemoActions.runMoney2020Demo();
+ } else {
+ Navigation.goBack(ROUTES.HOME);
+ }
+ }, [props.route.name]),
+ );
return ;
}
diff --git a/src/pages/EditRequestReceiptPage.js b/src/pages/EditRequestReceiptPage.js
index 6744f027b404..54ed5a8897a4 100644
--- a/src/pages/EditRequestReceiptPage.js
+++ b/src/pages/EditRequestReceiptPage.js
@@ -1,5 +1,6 @@
import React, {useState} from 'react';
import PropTypes from 'prop-types';
+import {View} from 'react-native';
import ScreenWrapper from '../components/ScreenWrapper';
import HeaderWithBackButton from '../components/HeaderWithBackButton';
import Navigation from '../libs/Navigation/Navigation';
@@ -40,17 +41,21 @@ function EditRequestReceiptPage({route, transactionID}) {
testID={EditRequestReceiptPage.displayName}
headerGapStyles={isDraggingOver ? [styles.receiptDropHeaderGap] : []}
>
-
-
-
-
+ {({safeAreaPaddingBottomStyle}) => (
+
+
+
+
+
+
+ )}
);
}
diff --git a/src/pages/EditSplitBillPage.js b/src/pages/EditSplitBillPage.js
index 217b1a100572..d10803cd40ea 100644
--- a/src/pages/EditSplitBillPage.js
+++ b/src/pages/EditSplitBillPage.js
@@ -37,11 +37,11 @@ const propTypes = {
transaction: transactionPropTypes.isRequired,
/** The draft transaction that holds data to be persisted on the current transaction */
- draftTransaction: PropTypes.shape(transactionPropTypes),
+ draftTransaction: transactionPropTypes,
};
const defaultProps = {
- draftTransaction: {},
+ draftTransaction: undefined,
};
function EditSplitBillPage({route, transaction, draftTransaction}) {
diff --git a/src/pages/EnablePayments/ActivateStep.js b/src/pages/EnablePayments/ActivateStep.js
index 268c2664e01d..2d23f39d25e5 100644
--- a/src/pages/EnablePayments/ActivateStep.js
+++ b/src/pages/EnablePayments/ActivateStep.js
@@ -1,3 +1,4 @@
+import _ from 'underscore';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
import * as LottieAnimations from '../../components/LottieAnimations';
@@ -29,8 +30,8 @@ const defaultProps = {
};
function ActivateStep(props) {
- const isGoldWallet = props.userWallet.tierName === CONST.WALLET.TIER_NAME.GOLD;
- const animation = isGoldWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo;
+ const isActivatedWallet = _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], props.userWallet.tierName);
+ const animation = isActivatedWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo;
const continueButtonText = props.walletTerms.chatReportID ? props.translate('activateStep.continueToPayment') : props.translate('activateStep.continueToTransfer');
return (
@@ -38,9 +39,9 @@ function ActivateStep(props) {
diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.js b/src/pages/LogInWithShortLivedAuthTokenPage.js
index 62eff262611d..875cdf7e8072 100644
--- a/src/pages/LogInWithShortLivedAuthTokenPage.js
+++ b/src/pages/LogInWithShortLivedAuthTokenPage.js
@@ -12,8 +12,7 @@ import themeColors from '../styles/themes/default';
import Icon from '../components/Icon';
import * as Expensicons from '../components/Icon/Expensicons';
import * as Illustrations from '../components/Icon/Illustrations';
-import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
-import compose from '../libs/compose';
+import useLocalize from '../hooks/useLocalize';
import TextLink from '../components/TextLink';
import ONYXKEYS from '../ONYXKEYS';
@@ -33,8 +32,6 @@ const propTypes = {
}),
}).isRequired,
- ...withLocalizePropTypes,
-
/** The details about the account that the user is signing in with */
account: PropTypes.shape({
/** Whether a sign is loading */
@@ -49,15 +46,26 @@ const defaultProps = {
};
function LogInWithShortLivedAuthTokenPage(props) {
+ const {translate} = useLocalize();
+
useEffect(() => {
const email = lodashGet(props, 'route.params.email', '');
// We have to check for both shortLivedAuthToken and shortLivedToken, as the old mobile app uses shortLivedToken, and is not being actively updated.
const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', '') || lodashGet(props, 'route.params.shortLivedToken', '');
- if (shortLivedAuthToken) {
+
+ // Try to authenticate using the shortLivedToken if we're not already trying to load the accounts
+ if (shortLivedAuthToken && !props.account.isLoading) {
Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken);
return;
}
+
+ // If an error is returned as part of the route, ensure we set it in the onyxData for the account
+ const error = lodashGet(props, 'route.params.error', '');
+ if (error) {
+ Session.setAccountError(error);
+ }
+
const exitTo = lodashGet(props, 'route.params.exitTo', '');
if (exitTo) {
Navigation.isNavigationReady().then(() => {
@@ -82,10 +90,18 @@ function LogInWithShortLivedAuthTokenPage(props) {
src={Illustrations.RocketBlue}
/>