route.name === SCREENS.NOT_FOUND)) {
+ const lastRouteIndex = rootState.routes.length - 1;
+ const centralRouteIndex = _.findLastIndex(rootState.routes, (route) => route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR);
+ navigationRef.current.dispatch({...StackActions.pop(lastRouteIndex - centralRouteIndex), target: rootState.key});
} else {
navigationRef.current.dispatch({...StackActions.pop(), target: rootState.key});
}
diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js
index 34a52adfeca9..c7a3b14e4fb0 100644
--- a/src/libs/Navigation/NavigationRoot.js
+++ b/src/libs/Navigation/NavigationRoot.js
@@ -101,7 +101,7 @@ function NavigationRoot(props) {
const animateStatusBarBackgroundColor = () => {
const currentRoute = navigationRef.getCurrentRoute();
- const currentScreenBackgroundColor = themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG;
+ const currentScreenBackgroundColor = (currentRoute.params && currentRoute.params.backgroundColor) || themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG;
prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current;
statusBarBackgroundColor.current = currentScreenBackgroundColor;
diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js
index fde5fe400c76..2311f4faa193 100644
--- a/src/libs/Navigation/linkingConfig.js
+++ b/src/libs/Navigation/linkingConfig.js
@@ -18,6 +18,9 @@ export default {
[SCREENS.DESKTOP_SIGN_IN_REDIRECT]: ROUTES.DESKTOP_SIGN_IN_REDIRECT,
[SCREENS.REPORT_ATTACHMENTS]: ROUTES.REPORT_ATTACHMENTS.route,
+ // Demo routes
+ [CONST.DEMO_PAGES.MONEY2020]: ROUTES.MONEY2020,
+
// Sidebar
[SCREENS.HOME]: {
path: ROUTES.HOME,
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/Pusher/EventType.js b/src/libs/Pusher/EventType.ts
similarity index 97%
rename from src/libs/Pusher/EventType.js
rename to src/libs/Pusher/EventType.ts
index 85ccc5e17242..89e8a0ca0260 100644
--- a/src/libs/Pusher/EventType.js
+++ b/src/libs/Pusher/EventType.ts
@@ -11,4 +11,4 @@ export default {
MULTIPLE_EVENT_TYPE: {
ONYX_API_UPDATE: 'onyxApiUpdate',
},
-};
+} as const;
diff --git a/src/libs/Pusher/library/index.js b/src/libs/Pusher/library/index.js
deleted file mode 100644
index 12cfae7df02f..000000000000
--- a/src/libs/Pusher/library/index.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * We use the standard pusher-js module to support pusher on web environments.
- * @see: https://github.com/pusher/pusher-js
- */
-import Pusher from 'pusher-js/with-encryption';
-
-export default Pusher;
diff --git a/src/libs/Pusher/library/index.native.js b/src/libs/Pusher/library/index.native.js
deleted file mode 100644
index 7b87d0c8bdfb..000000000000
--- a/src/libs/Pusher/library/index.native.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * We use the pusher-js/react-native module to support pusher on native environments.
- * @see: https://github.com/pusher/pusher-js
- */
-import Pusher from 'pusher-js/react-native';
-
-export default Pusher;
diff --git a/src/libs/Pusher/library/index.native.ts b/src/libs/Pusher/library/index.native.ts
new file mode 100644
index 000000000000..f50834366515
--- /dev/null
+++ b/src/libs/Pusher/library/index.native.ts
@@ -0,0 +1,10 @@
+/**
+ * We use the pusher-js/react-native module to support pusher on native environments.
+ * @see: https://github.com/pusher/pusher-js
+ */
+import PusherImplementation from 'pusher-js/react-native';
+import Pusher from './types';
+
+const PusherNative: Pusher = PusherImplementation;
+
+export default PusherNative;
diff --git a/src/libs/Pusher/library/index.ts b/src/libs/Pusher/library/index.ts
new file mode 100644
index 000000000000..6a7104a1d2a5
--- /dev/null
+++ b/src/libs/Pusher/library/index.ts
@@ -0,0 +1,10 @@
+/**
+ * We use the standard pusher-js module to support pusher on web environments.
+ * @see: https://github.com/pusher/pusher-js
+ */
+import PusherImplementation from 'pusher-js/with-encryption';
+import type Pusher from './types';
+
+const PusherWeb: Pusher = PusherImplementation;
+
+export default PusherWeb;
diff --git a/src/libs/Pusher/library/types.ts b/src/libs/Pusher/library/types.ts
new file mode 100644
index 000000000000..cc8c70fccdbb
--- /dev/null
+++ b/src/libs/Pusher/library/types.ts
@@ -0,0 +1,10 @@
+import PusherClass from 'pusher-js/with-encryption';
+import {LiteralUnion} from 'type-fest';
+
+type Pusher = typeof PusherClass;
+
+type SocketEventName = LiteralUnion<'error' | 'connected' | 'disconnected' | 'state_change', string>;
+
+export default Pusher;
+
+export type {SocketEventName};
diff --git a/src/libs/Pusher/pusher.js b/src/libs/Pusher/pusher.ts
similarity index 72%
rename from src/libs/Pusher/pusher.js
rename to src/libs/Pusher/pusher.ts
index 4f2b63d36c0c..dad963e933fe 100644
--- a/src/libs/Pusher/pusher.js
+++ b/src/libs/Pusher/pusher.ts
@@ -1,9 +1,48 @@
import Onyx from 'react-native-onyx';
-import _ from 'underscore';
+import {Channel, ChannelAuthorizerGenerator, Options} from 'pusher-js/with-encryption';
+import isObject from 'lodash/isObject';
+import {LiteralUnion, ValueOf} from 'type-fest';
import ONYXKEYS from '../../ONYXKEYS';
import Pusher from './library';
import TYPE from './EventType';
import Log from '../Log';
+import DeepValueOf from '../../types/utils/DeepValueOf';
+import {SocketEventName} from './library/types';
+import CONST from '../../CONST';
+import {OnyxUpdateEvent, OnyxUpdatesFromServer} from '../../types/onyx';
+
+type States = {
+ previous: string;
+ current: string;
+};
+
+type Args = {
+ appKey: string;
+ cluster: string;
+ authEndpoint: string;
+};
+
+type PushJSON = OnyxUpdateEvent[] | OnyxUpdatesFromServer;
+
+type EventCallbackError = {type: ValueOf; data: {code: number}};
+
+type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean};
+
+type EventData = {id?: string; chunk?: unknown; final?: boolean; index: number};
+
+type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void;
+
+type PusherWithAuthParams = InstanceType & {
+ config: {
+ auth?: {
+ params?: unknown;
+ };
+ };
+};
+
+type PusherEventName = LiteralUnion, string>;
+
+type PusherSubscribtionErrorData = {type?: string; error?: string; status?: string};
let shouldForceOffline = false;
Onyx.connect({
@@ -16,33 +55,23 @@ Onyx.connect({
},
});
-let socket;
+let socket: PusherWithAuthParams | null;
let pusherSocketID = '';
-const socketEventCallbacks = [];
-let customAuthorizer;
+const socketEventCallbacks: SocketEventCallback[] = [];
+let customAuthorizer: ChannelAuthorizerGenerator;
/**
* Trigger each of the socket event callbacks with the event information
- *
- * @param {String} eventName
- * @param {*} data
*/
-function callSocketEventCallbacks(eventName, data) {
- _.each(socketEventCallbacks, (cb) => cb(eventName, data));
+function callSocketEventCallbacks(eventName: SocketEventName, data?: EventCallbackError | States) {
+ socketEventCallbacks.forEach((cb) => cb(eventName, data));
}
/**
* Initialize our pusher lib
- *
- * @param {Object} args
- * @param {String} args.appKey
- * @param {String} args.cluster
- * @param {String} args.authEndpoint
- * @param {Object} [params]
- * @public
- * @returns {Promise} resolves when Pusher has connected
+ * @returns resolves when Pusher has connected
*/
-function init(args, params) {
+function init(args: Args, params?: unknown): Promise {
return new Promise((resolve) => {
if (socket) {
return resolve();
@@ -55,7 +84,7 @@ function init(args, params) {
// }
// };
- const options = {
+ const options: Options = {
cluster: args.cluster,
authEndpoint: args.authEndpoint,
};
@@ -65,7 +94,6 @@ function init(args, params) {
}
socket = new Pusher(args.appKey, options);
-
// If we want to pass params in our requests to api.php we'll need to add it to socket.config.auth.params
// as per the documentation
// (https://pusher.com/docs/channels/using_channels/connection#channels-options-parameter).
@@ -77,21 +105,21 @@ function init(args, params) {
}
// Listen for connection errors and log them
- socket.connection.bind('error', (error) => {
+ socket?.connection.bind('error', (error: EventCallbackError) => {
callSocketEventCallbacks('error', error);
});
- socket.connection.bind('connected', () => {
- pusherSocketID = socket.connection.socket_id;
+ socket?.connection.bind('connected', () => {
+ pusherSocketID = socket?.connection.socket_id ?? '';
callSocketEventCallbacks('connected');
resolve();
});
- socket.connection.bind('disconnected', () => {
+ socket?.connection.bind('disconnected', () => {
callSocketEventCallbacks('disconnected');
});
- socket.connection.bind('state_change', (states) => {
+ socket?.connection.bind('state_change', (states: States) => {
callSocketEventCallbacks('state_change', states);
});
});
@@ -99,12 +127,8 @@ function init(args, params) {
/**
* Returns a Pusher channel for a channel name
- *
- * @param {String} channelName
- *
- * @returns {Channel}
*/
-function getChannel(channelName) {
+function getChannel(channelName: string): Channel | undefined {
if (!socket) {
return;
}
@@ -114,19 +138,14 @@ function getChannel(channelName) {
/**
* Binds an event callback to a channel + eventName
- * @param {Pusher.Channel} channel
- * @param {String} eventName
- * @param {Function} [eventCallback]
- *
- * @private
*/
-function bindEventToChannel(channel, eventName, eventCallback = () => {}) {
+function bindEventToChannel(channel: Channel | undefined, eventName: PusherEventName, eventCallback: (data: PushJSON) => void = () => {}) {
if (!eventName) {
return;
}
- const chunkedDataEvents = {};
- const callback = (eventData) => {
+ const chunkedDataEvents: Record = {};
+ const callback = (eventData: string | Record | EventData) => {
if (shouldForceOffline) {
Log.info('[Pusher] Ignoring a Push event because shouldForceOffline = true');
return;
@@ -134,7 +153,7 @@ function bindEventToChannel(channel, eventName, eventCallback = () => {}) {
let data;
try {
- data = _.isObject(eventData) ? eventData : JSON.parse(eventData);
+ data = isObject(eventData) ? eventData : JSON.parse(eventData);
} catch (err) {
Log.alert('[Pusher] Unable to parse single JSON event data from Pusher', {error: err, eventData});
return;
@@ -164,7 +183,7 @@ function bindEventToChannel(channel, eventName, eventCallback = () => {}) {
// Only call the event callback if we've received the last packet and we don't have any holes in the complete
// packet.
- if (chunkedEvent.receivedFinal && chunkedEvent.chunks.length === _.keys(chunkedEvent.chunks).length) {
+ if (chunkedEvent.receivedFinal && chunkedEvent.chunks.length === Object.keys(chunkedEvent.chunks).length) {
try {
eventCallback(JSON.parse(chunkedEvent.chunks.join('')));
} catch (err) {
@@ -181,22 +200,14 @@ function bindEventToChannel(channel, eventName, eventCallback = () => {}) {
}
};
- channel.bind(eventName, callback);
+ channel?.bind(eventName, callback);
}
/**
* Subscribe to a channel and an event
- *
- * @param {String} channelName
- * @param {String} eventName
- * @param {Function} [eventCallback]
- * @param {Function} [onResubscribe] Callback to be called when reconnection happen
- *
- * @return {Promise}
- *
- * @public
+ * @param [onResubscribe] Callback to be called when reconnection happen
*/
-function subscribe(channelName, eventName, eventCallback = () => {}, onResubscribe = () => {}) {
+function subscribe(channelName: string, eventName: PusherEventName, eventCallback: (data: PushJSON) => void = () => {}, onResubscribe = () => {}): Promise {
return new Promise((resolve, reject) => {
// We cannot call subscribe() before init(). Prevent any attempt to do this on dev.
if (!socket) {
@@ -226,7 +237,7 @@ function subscribe(channelName, eventName, eventCallback = () => {}, onResubscri
onResubscribe();
});
- channel.bind('pusher:subscription_error', (data = {}) => {
+ channel.bind('pusher:subscription_error', (data: PusherSubscribtionErrorData = {}) => {
const {type, error, status} = data;
Log.hmmm('[Pusher] Issue authenticating with Pusher during subscribe attempt.', {
channelName,
@@ -245,12 +256,8 @@ function subscribe(channelName, eventName, eventCallback = () => {}, onResubscri
/**
* Unsubscribe from a channel and optionally a specific event
- *
- * @param {String} channelName
- * @param {String} [eventName]
- * @public
*/
-function unsubscribe(channelName, eventName = '') {
+function unsubscribe(channelName: string, eventName: PusherEventName = '') {
const channel = getChannel(channelName);
if (!channel) {
@@ -269,18 +276,14 @@ function unsubscribe(channelName, eventName = '') {
Log.info('[Pusher] Unsubscribing from channel', false, {channelName});
channel.unbind();
- socket.unsubscribe(channelName);
+ socket?.unsubscribe(channelName);
}
}
/**
* Are we already in the process of subscribing to this channel?
- *
- * @param {String} channelName
- *
- * @returns {Boolean}
*/
-function isAlreadySubscribing(channelName) {
+function isAlreadySubscribing(channelName: string): boolean {
if (!socket) {
return false;
}
@@ -291,12 +294,8 @@ function isAlreadySubscribing(channelName) {
/**
* Are we already subscribed to this channel?
- *
- * @param {String} channelName
- *
- * @returns {Boolean}
*/
-function isSubscribed(channelName) {
+function isSubscribed(channelName: string): boolean {
if (!socket) {
return false;
}
@@ -307,12 +306,8 @@ function isSubscribed(channelName) {
/**
* Sends an event over a specific event/channel in pusher.
- *
- * @param {String} channelName
- * @param {String} eventName
- * @param {Object} payload
*/
-function sendEvent(channelName, eventName, payload) {
+function sendEvent(channelName: string, eventName: PusherEventName, payload: Record) {
// Check to see if we are subscribed to this channel before sending the event. Sending client events over channels
// we are not subscribed too will throw errors and cause reconnection attempts. Subscriptions are not instant and
// can happen later than we expect.
@@ -325,15 +320,13 @@ function sendEvent(channelName, eventName, payload) {
return;
}
- socket.send_event(eventName, payload, channelName);
+ socket?.send_event(eventName, payload, channelName);
}
/**
* Register a method that will be triggered when a socket event happens (like disconnecting)
- *
- * @param {Function} cb
*/
-function registerSocketEventCallback(cb) {
+function registerSocketEventCallback(cb: SocketEventCallback) {
socketEventCallbacks.push(cb);
}
@@ -341,10 +334,8 @@ function registerSocketEventCallback(cb) {
* A custom authorizer allows us to take a more fine-grained approach to
* authenticating Pusher. e.g. we can handle failed attempts to authorize
* with an expired authToken and retry the attempt.
- *
- * @param {Function} authorizer
*/
-function registerCustomAuthorizer(authorizer) {
+function registerCustomAuthorizer(authorizer: ChannelAuthorizerGenerator) {
customAuthorizer = authorizer;
}
@@ -376,18 +367,13 @@ function reconnect() {
socket.connect();
}
-/**
- * @returns {String}
- */
-function getPusherSocketID() {
+function getPusherSocketID(): string {
return pusherSocketID;
}
if (window) {
/**
* Pusher socket for debugging purposes
- *
- * @returns {Function}
*/
window.getPusherInstance = () => socket;
}
@@ -407,3 +393,5 @@ export {
TYPE,
getPusherSocketID,
};
+
+export type {EventCallbackError, States, PushJSON};
diff --git a/src/libs/PusherConnectionManager.ts b/src/libs/PusherConnectionManager.ts
index 4ab08d6dc760..9b1f6ebe1b2a 100644
--- a/src/libs/PusherConnectionManager.ts
+++ b/src/libs/PusherConnectionManager.ts
@@ -1,11 +1,10 @@
-import {ValueOf} from 'type-fest';
+import {ChannelAuthorizationCallback} from 'pusher-js/with-encryption';
import * as Pusher from './Pusher/pusher';
import * as Session from './actions/Session';
import Log from './Log';
import CONST from '../CONST';
-
-type EventCallbackError = {type: ValueOf; data: {code: number}};
-type CustomAuthorizerChannel = {name: string};
+import {SocketEventName} from './Pusher/library/types';
+import {EventCallbackError, States} from './Pusher/pusher';
function init() {
/**
@@ -14,30 +13,32 @@ function init() {
* current valid token to generate the signed auth response
* needed to subscribe to Pusher channels.
*/
- Pusher.registerCustomAuthorizer((channel: CustomAuthorizerChannel) => ({
- authorize: (socketID: string, callback: () => void) => {
- Session.authenticatePusher(socketID, channel.name, callback);
+ Pusher.registerCustomAuthorizer((channel) => ({
+ authorize: (socketId: string, callback: ChannelAuthorizationCallback) => {
+ Session.authenticatePusher(socketId, channel.name, callback);
},
}));
- Pusher.registerSocketEventCallback((eventName: string, error: EventCallbackError) => {
+ Pusher.registerSocketEventCallback((eventName: SocketEventName, error?: EventCallbackError | States) => {
switch (eventName) {
case 'error': {
- const errorType = error?.type;
- const code = error?.data?.code;
- if (errorType === CONST.ERROR.PUSHER_ERROR && code === 1006) {
- // 1006 code happens when a websocket connection is closed. There may or may not be a reason attached indicating why the connection was closed.
- // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
- Log.hmmm('[PusherConnectionManager] Channels Error 1006', {error});
- } else if (errorType === CONST.ERROR.PUSHER_ERROR && code === 4201) {
- // This means the connection was closed because Pusher did not receive a reply from the client when it pinged them for a response
- // https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol/#4200-4299
- Log.hmmm('[PusherConnectionManager] Pong reply not received', {error});
- } else if (errorType === CONST.ERROR.WEB_SOCKET_ERROR) {
- // It's not clear why some errors are wrapped in a WebSocketError type - this error could mean different things depending on the contents.
- Log.hmmm('[PusherConnectionManager] WebSocketError', {error});
- } else {
- Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} [PusherConnectionManager] Unknown error event`, {error});
+ if (error && 'type' in error) {
+ const errorType = error?.type;
+ const code = error?.data?.code;
+ if (errorType === CONST.ERROR.PUSHER_ERROR && code === 1006) {
+ // 1006 code happens when a websocket connection is closed. There may or may not be a reason attached indicating why the connection was closed.
+ // https://datatracker.ietf.org/doc/html/rfc6455#section-7.1.5
+ Log.hmmm('[PusherConnectionManager] Channels Error 1006', {error});
+ } else if (errorType === CONST.ERROR.PUSHER_ERROR && code === 4201) {
+ // This means the connection was closed because Pusher did not receive a reply from the client when it pinged them for a response
+ // https://pusher.com/docs/channels/library_auth_reference/pusher-websockets-protocol/#4200-4299
+ Log.hmmm('[PusherConnectionManager] Pong reply not received', {error});
+ } else if (errorType === CONST.ERROR.WEB_SOCKET_ERROR) {
+ // It's not clear why some errors are wrapped in a WebSocketError type - this error could mean different things depending on the contents.
+ Log.hmmm('[PusherConnectionManager] WebSocketError', {error});
+ } else {
+ Log.alert(`${CONST.ERROR.ENSURE_BUGBOT} [PusherConnectionManager] Unknown error event`, {error});
+ }
}
break;
}
diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts
index 5baa4b68d5f8..d47283f21bbf 100644
--- a/src/libs/PusherUtils.ts
+++ b/src/libs/PusherUtils.ts
@@ -4,9 +4,7 @@ import Log from './Log';
import NetworkConnection from './NetworkConnection';
import * as Pusher from './Pusher/pusher';
import CONST from '../CONST';
-import {OnyxUpdateEvent, OnyxUpdatesFromServer} from '../types/onyx';
-
-type PushJSON = OnyxUpdateEvent[] | OnyxUpdatesFromServer;
+import {PushJSON} from './Pusher/pusher';
type Callback = (data: OnyxUpdate[]) => Promise;
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index fdfc630cfd33..0b7bbfd61461 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -1267,6 +1267,17 @@ function isWaitingForTaskCompleteFromAssignee(report, parentReportAction = {}) {
return isTaskReport(report) && isReportManager(report) && isOpenTaskReport(report, parentReportAction);
}
+/**
+ * Returns number of transactions that are nonReimbursable
+ *
+ * @param {Object|null} iouReportID
+ * @returns {Number}
+ */
+function hasNonReimbursableTransactions(iouReportID) {
+ const allTransactions = TransactionUtils.getAllReportTransactions(iouReportID);
+ return _.filter(allTransactions, (transaction) => transaction.reimbursable === false).length > 0;
+}
+
/**
* @param {Object} report
* @param {Object} allReportsDict
@@ -1344,6 +1355,10 @@ function getMoneyRequestReportName(report, policy = undefined) {
return `${payerPaidAmountMesssage} • ${Localize.translateLocal('iou.pending')}`;
}
+ if (hasNonReimbursableTransactions(report.reportID)) {
+ return Localize.translateLocal('iou.payerSpentAmount', {payer: payerName, amount: formattedAmount});
+ }
+
if (report.hasOutstandingIOU) {
return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount});
}
@@ -1373,6 +1388,8 @@ function getTransactionDetails(transaction, createdDateFormat = CONST.DATE.FNS_F
tag: TransactionUtils.getTag(transaction),
mccGroup: TransactionUtils.getMCCGroup(transaction),
cardID: TransactionUtils.getCardID(transaction),
+ originalAmount: TransactionUtils.getOriginalAmount(transaction),
+ originalCurrency: TransactionUtils.getOriginalCurrency(transaction),
};
}
@@ -1525,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});
@@ -1563,7 +1583,8 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip
return Localize.translateLocal('iou.waitingOnBankAccount', {submitterDisplayName});
}
- return Localize.translateLocal('iou.payerOwesAmount', {payer: payerName, amount: formattedAmount});
+ const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID);
+ return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName, amount: formattedAmount});
}
/**
@@ -2592,6 +2613,7 @@ function buildOptimisticTaskReportAction(taskReportID, actionName, message = '')
* @param {String} notificationPreference
* @param {String} parentReportActionID
* @param {String} parentReportID
+ * @param {String} welcomeMessage
* @returns {Object}
*/
function buildOptimisticChatReport(
@@ -2607,6 +2629,7 @@ function buildOptimisticChatReport(
notificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
parentReportActionID = '',
parentReportID = '',
+ welcomeMessage = '',
) {
const currentTime = DateUtils.getDBTime();
return {
@@ -2633,7 +2656,7 @@ function buildOptimisticChatReport(
stateNum: 0,
statusNum: 0,
visibility,
- welcomeMessage: '',
+ welcomeMessage,
writeCapability,
};
}
@@ -3412,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];
}
@@ -3569,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;
}
/**
@@ -3984,6 +4012,7 @@ export {
areAllRequestsBeingSmartScanned,
getReportPreviewDisplayTransactions,
getTransactionsWithReceipts,
+ hasNonReimbursableTransactions,
hasMissingSmartscanFields,
getIOUReportActionDisplayMessage,
isWaitingForTaskCompleteFromAssignee,
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 43b1c2f39902..77fc4f04f99d 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -80,7 +80,7 @@ function hasReceipt(transaction: Transaction | undefined | null): boolean {
return !!transaction?.receipt?.state;
}
-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 +90,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);
}
/**
@@ -225,6 +234,21 @@ function getCurrency(transaction: Transaction): string {
return transaction?.currency ?? CONST.CURRENCY.USD;
}
+/**
+ * Return the original currency field from the transaction.
+ */
+function getOriginalCurrency(transaction: Transaction): string {
+ return transaction?.originalCurrency ?? '';
+}
+
+/**
+ * Return the absolute value of the original amount field from the transaction.
+ */
+function getOriginalAmount(transaction: Transaction): number {
+ const amount = transaction?.originalAmount ?? 0;
+ return Math.abs(amount);
+}
+
/**
* Return the merchant field from the transaction, return the modifiedMerchant if present.
*/
@@ -293,6 +317,9 @@ function isDistanceRequest(transaction: Transaction): boolean {
return type === CONST.TRANSACTION.TYPE.CUSTOM_UNIT && customUnitName === CONST.CUSTOM_UNITS.NAME_DISTANCE;
}
+/**
+ * Determine whether a transaction is made with an Expensify card.
+ */
function isExpensifyCardTransaction(transaction: Transaction): boolean {
if (!transaction.cardID) {
return false;
@@ -300,6 +327,9 @@ function isExpensifyCardTransaction(transaction: Transaction): boolean {
return isExpensifyCard(transaction.cardID);
}
+/**
+ * Check if the transaction status is set to Pending.
+ */
function isPending(transaction: Transaction): boolean {
if (!transaction.status) {
return false;
@@ -307,6 +337,9 @@ function isPending(transaction: Transaction): boolean {
return transaction.status === CONST.TRANSACTION.STATUS.PENDING;
}
+/**
+ * Check if the transaction status is set to Posted.
+ */
function isPosted(transaction: Transaction): boolean {
if (!transaction.status) {
return false;
@@ -428,6 +461,8 @@ export {
getAmount,
getCurrency,
getCardID,
+ getOriginalCurrency,
+ getOriginalAmount,
getMerchant,
getMCCGroup,
getCreated,
@@ -446,6 +481,9 @@ export {
isPending,
isPosted,
getWaypoints,
+ isAmountMissing,
+ isMerchantMissing,
+ isCreatedMissing,
areRequiredFieldsEmpty,
hasMissingSmartscanFields,
getWaypointIndex,
diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js
index b1cb09a8a5e2..4d3c880b5983 100644
--- a/src/libs/actions/BankAccounts.js
+++ b/src/libs/actions/BankAccounts.js
@@ -57,10 +57,10 @@ 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() {
+function getVBBADataForOnyx(currentStep = undefined) {
return {
optimisticData: [
{
@@ -79,6 +79,12 @@ function getVBBADataForOnyx() {
value: {
isLoading: false,
errors: null,
+ // When setting up a bank account, we save the draft form values in Onyx.
+ // When we update the information for a step, the value of some fields that are returned from the API
+ // can be different from the value that we stored as the draft in Onyx (i.e. the phone number is formatted).
+ // This is why we store the current step used to call the API in order to update the corresponding draft data in Onyx.
+ // If currentStep is undefined that means this step don't need to update the data of the draft in Onyx.
+ draftStep: currentStep,
},
},
],
@@ -222,7 +228,7 @@ function deletePaymentBankAccount(bankAccountID) {
* @param {Boolean} [params.isOnfidoSetupComplete]
*/
function updatePersonalInformationForBankAccount(params) {
- API.write('UpdatePersonalInformationForBankAccount', params, getVBBADataForOnyx());
+ API.write('UpdatePersonalInformationForBankAccount', params, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR));
}
/**
@@ -269,6 +275,10 @@ function validateBankAccount(bankAccountID, validateCode) {
);
}
+function clearReimbursementAccount() {
+ Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, null);
+}
+
function openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep) {
const onyxData = {
optimisticData: [
@@ -339,7 +349,7 @@ function openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep) {
* @param {String} policyID
*/
function updateCompanyInformationForBankAccount(bankAccount, policyID) {
- API.write('UpdateCompanyInformationForBankAccount', {...bankAccount, policyID}, getVBBADataForOnyx());
+ API.write('UpdateCompanyInformationForBankAccount', {...bankAccount, policyID}, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY));
}
/**
@@ -376,7 +386,7 @@ function connectBankAccountManually(bankAccountID, accountNumber, routingNumber,
routingNumber,
plaidMask,
},
- getVBBADataForOnyx(),
+ getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT),
);
}
@@ -430,6 +440,7 @@ export {
deletePaymentBankAccount,
handlePlaidError,
openPersonalBankAccountSetupView,
+ clearReimbursementAccount,
openReimbursementAccountPage,
updateBeneficialOwnersForBankAccount,
updateCompanyInformationForBankAccount,
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 c469ed02a084..422dd8fbb03a 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},
@@ -1767,7 +1788,8 @@ function editMoneyRequest(transactionID, transactionThreadReportID, transactionC
updatedMoneyRequestReport.lastMessageHtml = lastMessage[0].html;
// Update the last message of the chat report
- const messageText = Localize.translateLocal('iou.payerOwesAmount', {
+ const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport);
+ const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {
payer: updatedMoneyRequestReport.managerEmail,
amount: CurrencyUtils.convertToDisplayString(updatedMoneyRequestReport.total, updatedMoneyRequestReport.currency),
});
@@ -1987,7 +2009,8 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView
updatedIOUReport.lastVisibleActionCreated = lastVisibleAction.created;
updatedReportPreviewAction = {...reportPreviewAction};
- const messageText = Localize.translateLocal('iou.payerOwesAmount', {
+ const hasNonReimbursableTransactions = ReportUtils.hasNonReimbursableTransactions(iouReport);
+ const messageText = Localize.translateLocal(hasNonReimbursableTransactions ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {
payer: updatedIOUReport.managerEmail,
amount: CurrencyUtils.convertToDisplayString(updatedIOUReport.total, updatedIOUReport.currency),
});
@@ -2819,9 +2842,12 @@ function setMoneyRequestReceipt(receiptPath, receiptFilename) {
Onyx.merge(ONYXKEYS.IOU, {receiptPath, receiptFilename, merchant: ''});
}
-function createEmptyTransaction() {
+function setUpDistanceTransaction() {
const transactionID = NumberUtils.rand64();
- Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {transactionID});
+ Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
+ transactionID,
+ comment: {type: CONST.TRANSACTION.TYPE.CUSTOM_UNIT, customUnit: {name: CONST.CUSTOM_UNITS.NAME_DISTANCE}},
+ });
Onyx.merge(ONYXKEYS.IOU, {transactionID});
}
@@ -2916,7 +2942,7 @@ export {
setMoneyRequestBillable,
setMoneyRequestParticipants,
setMoneyRequestReceipt,
- createEmptyTransaction,
+ setUpDistanceTransaction,
navigateToNextPage,
updateDistanceRequest,
replaceReceipt,
diff --git a/src/libs/actions/InputFocus/index.desktop.ts b/src/libs/actions/InputFocus/index.desktop.ts
new file mode 100644
index 000000000000..b6cf1aba6138
--- /dev/null
+++ b/src/libs/actions/InputFocus/index.desktop.ts
@@ -0,0 +1,29 @@
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '../../../ONYXKEYS';
+import ReportActionComposeFocusManager from '../../ReportActionComposeFocusManager';
+
+function inputFocusChange(focus: boolean) {
+ Onyx.set(ONYXKEYS.INPUT_FOCUSED, focus);
+}
+
+let refSave: HTMLElement | undefined;
+function composerFocusKeepFocusOn(ref: HTMLElement, isFocused: boolean, modal: {willAlertModalBecomeVisible: boolean; isVisible: boolean}, onyxFocused: boolean) {
+ if (isFocused && !onyxFocused) {
+ inputFocusChange(true);
+ ref.focus();
+ }
+ if (isFocused) {
+ refSave = ref;
+ }
+ if (!isFocused && !onyxFocused && !modal.willAlertModalBecomeVisible && !modal.isVisible && refSave) {
+ if (!ReportActionComposeFocusManager.isFocused()) {
+ refSave.focus();
+ } else {
+ refSave = undefined;
+ }
+ }
+}
+
+const callback = (method: () => void) => method();
+
+export {composerFocusKeepFocusOn, inputFocusChange, callback};
diff --git a/src/libs/actions/InputFocus/index.ts b/src/libs/actions/InputFocus/index.ts
new file mode 100644
index 000000000000..1840b0625626
--- /dev/null
+++ b/src/libs/actions/InputFocus/index.ts
@@ -0,0 +1,5 @@
+function inputFocusChange() {}
+function composerFocusKeepFocusOn() {}
+const callback = () => {};
+
+export {composerFocusKeepFocusOn, inputFocusChange, callback};
diff --git a/src/libs/actions/InputFocus/index.website.ts b/src/libs/actions/InputFocus/index.website.ts
new file mode 100644
index 000000000000..7c044b169a03
--- /dev/null
+++ b/src/libs/actions/InputFocus/index.website.ts
@@ -0,0 +1,30 @@
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '../../../ONYXKEYS';
+import * as Browser from '../../Browser';
+import ReportActionComposeFocusManager from '../../ReportActionComposeFocusManager';
+
+function inputFocusChange(focus: boolean) {
+ Onyx.set(ONYXKEYS.INPUT_FOCUSED, focus);
+}
+
+let refSave: HTMLElement | undefined;
+function composerFocusKeepFocusOn(ref: HTMLElement, isFocused: boolean, modal: {willAlertModalBecomeVisible: boolean; isVisible: boolean}, onyxFocused: boolean) {
+ if (isFocused && !onyxFocused) {
+ inputFocusChange(true);
+ ref.focus();
+ }
+ if (isFocused) {
+ refSave = ref;
+ }
+ if (!isFocused && !onyxFocused && !modal.willAlertModalBecomeVisible && !modal.isVisible && refSave) {
+ if (!ReportActionComposeFocusManager.isFocused()) {
+ refSave.focus();
+ } else {
+ refSave = undefined;
+ }
+ }
+}
+
+const callback = (method: () => void) => !Browser.isMobile() && method();
+
+export {composerFocusKeepFocusOn, inputFocusChange, callback};
diff --git a/src/libs/actions/ReimbursementAccount/index.js b/src/libs/actions/ReimbursementAccount/index.js
index 49ff30e7be8e..68774d0ba8b0 100644
--- a/src/libs/actions/ReimbursementAccount/index.js
+++ b/src/libs/actions/ReimbursementAccount/index.js
@@ -31,6 +31,7 @@ function setWorkspaceIDForReimbursementAccount(workspaceID) {
*/
function updateReimbursementAccountDraft(bankAccountData) {
Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT_DRAFT, bankAccountData);
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {draftStep: undefined});
}
/**
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index 00c3ab325f29..c56e9c567745 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -1430,8 +1430,9 @@ function navigateToConciergeChat(ignoreConciergeReportID = false) {
* @param {String} visibility
* @param {Array} policyMembersAccountIDs
* @param {String} writeCapability
+ * @param {String} welcomeMessage
*/
-function addPolicyReport(policyID, reportName, visibility, policyMembersAccountIDs, writeCapability = CONST.REPORT.WRITE_CAPABILITIES.ALL) {
+function addPolicyReport(policyID, reportName, visibility, policyMembersAccountIDs, writeCapability = CONST.REPORT.WRITE_CAPABILITIES.ALL, welcomeMessage = '') {
// The participants include the current user (admin), and for restricted rooms, the policy members. Participants must not be empty.
const members = visibility === CONST.REPORT.VISIBILITY.RESTRICTED ? policyMembersAccountIDs : [];
const participants = _.unique([currentUserAccountID, ...members]);
@@ -1448,6 +1449,9 @@ function addPolicyReport(policyID, reportName, visibility, policyMembersAccountI
// The room might contain all policy members so notifying always should be opt-in only.
CONST.REPORT.NOTIFICATION_PREFERENCE.DAILY,
+ '',
+ '',
+ welcomeMessage,
);
const createdReportAction = ReportUtils.buildOptimisticCreatedReportAction(CONST.POLICY.OWNER_EMAIL_FAKE);
@@ -1512,6 +1516,7 @@ function addPolicyReport(policyID, reportName, visibility, policyMembersAccountI
reportID: policyReport.reportID,
createdReportActionID: createdReportAction.reportActionID,
writeCapability,
+ welcomeMessage,
},
{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 (
-