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/ReportUtils.js b/src/libs/ReportUtils.js
index 0a3c98155940..e5994b4e0c94 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -1542,6 +1542,9 @@ 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});
@@ -3432,8 +3435,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 +3596,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;
}
/**
diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js
index 7a32db660021..314a1d63760e 100644
--- a/src/libs/SidebarUtils.js
+++ b/src/libs/SidebarUtils.js
@@ -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) {
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index 7746e73370ba..6a45bef5780b 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -87,7 +87,7 @@ function hasReceipt(transaction: Transaction | undefined | null): boolean {
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 === '';
@@ -97,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);
}
/**
@@ -472,6 +481,9 @@ export {
isPending,
isPosted,
getWaypoints,
+ isAmountMissing,
+ isMerchantMissing,
+ isCreatedMissing,
areRequiredFieldsEmpty,
hasMissingSmartscanFields,
getWaypointIndex,
diff --git a/src/libs/actions/DemoActions.js b/src/libs/actions/DemoActions.js
new file mode 100644
index 000000000000..29c983c35262
--- /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(Config ? 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..4e3fc91fc4d9 100644
--- a/src/libs/actions/IOU.js
+++ b/src/libs/actions/IOU.js
@@ -1303,7 +1303,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 +1430,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 +1699,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},
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/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/AdditionalDetailsStep.js b/src/pages/EnablePayments/AdditionalDetailsStep.js
index bd068ad9abcc..13091ab3f845 100644
--- a/src/pages/EnablePayments/AdditionalDetailsStep.js
+++ b/src/pages/EnablePayments/AdditionalDetailsStep.js
@@ -23,7 +23,6 @@ import DatePicker from '../../components/DatePicker';
import Form from '../../components/Form';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes, withCurrentUserPersonalDetailsDefaultProps} from '../../components/withCurrentUserPersonalDetails';
import * as PersonalDetails from '../../libs/actions/PersonalDetails';
-import OfflineIndicator from '../../components/OfflineIndicator';
const propTypes = {
...withLocalizePropTypes,
@@ -148,6 +147,7 @@ function AdditionalDetailsStep({walletAdditionalDetails, translate, currentUserP
if (!_.isEmpty(walletAdditionalDetails.questions)) {
return (
-