;
- iconSize?: number;
-};
-
/**
* Check to see if user has either a debit card or personal bank account added
*/
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/Permissions.ts b/src/libs/Permissions.ts
index 05322472a407..13489c396c3c 100644
--- a/src/libs/Permissions.ts
+++ b/src/libs/Permissions.ts
@@ -17,13 +17,6 @@ function canUseDefaultRooms(betas: Beta[]): boolean {
return betas?.includes(CONST.BETAS.DEFAULT_ROOMS) || canUseAllBetas(betas);
}
-/**
- * IOU Send feature is temporarily disabled.
- */
-function canUseIOUSend(): boolean {
- return false;
-}
-
function canUseWallet(betas: Beta[]): boolean {
return betas?.includes(CONST.BETAS.BETA_EXPENSIFY_WALLET) || canUseAllBetas(betas);
}
@@ -68,7 +61,6 @@ export default {
canUseChronos,
canUsePayWithExpensify,
canUseDefaultRooms,
- canUseIOUSend,
canUseWallet,
canUseCommentLinking,
canUsePolicyRooms,
diff --git a/src/libs/PolicyUtils.js b/src/libs/PolicyUtils.js
index 347a825f59cc..6bbae72f1d80 100644
--- a/src/libs/PolicyUtils.js
+++ b/src/libs/PolicyUtils.js
@@ -174,7 +174,7 @@ function getMemberAccountIDsForWorkspace(policyMembers, personalDetails) {
if (!personalDetail || !personalDetail.login) {
return;
}
- memberEmailsToAccountIDs[personalDetail.login] = accountID;
+ memberEmailsToAccountIDs[personalDetail.login] = Number(accountID);
});
return memberEmailsToAccountIDs;
}
diff --git a/src/libs/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/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts
index cdc45cb119d5..13e8a195cccb 100644
--- a/src/libs/ReceiptUtils.ts
+++ b/src/libs/ReceiptUtils.ts
@@ -6,10 +6,13 @@ import ReceiptHTML from '../../assets/images/receipt-html.png';
import ReceiptDoc from '../../assets/images/receipt-doc.png';
import ReceiptGeneric from '../../assets/images/receipt-generic.png';
import ReceiptSVG from '../../assets/images/receipt-svg.png';
+import {Transaction} from '../types/onyx';
+import ROUTES from '../ROUTES';
type ThumbnailAndImageURI = {
image: ImageSourcePropType | string;
thumbnail: string | null;
+ transaction?: Transaction;
};
type FileNameAndExtension = {
@@ -20,12 +23,23 @@ type FileNameAndExtension = {
/**
* Grab the appropriate receipt image and thumbnail URIs based on file type
*
- * @param path URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg
- * @param filename of uploaded image or last part of remote URI
+ * @param transaction
+ * @param receiptPath
+ * @param receiptFileName
*/
-function getThumbnailAndImageURIs(path: string, filename: string): ThumbnailAndImageURI {
+function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string | null = null, receiptFileName: string | null = null): ThumbnailAndImageURI {
+ // URI to image, i.e. blob:new.expensify.com/9ef3a018-4067-47c6-b29f-5f1bd35f213d or expensify.com/receipts/w_e616108497ef940b7210ec6beb5a462d01a878f4.jpg
+ const path = transaction?.receipt?.source ?? receiptPath ?? '';
+ // filename of uploaded image or last part of remote URI
+ const filename = transaction?.filename ?? receiptFileName ?? '';
const isReceiptImage = Str.isImage(filename);
+ const hasEReceipt = transaction?.hasEReceipt;
+
+ if (hasEReceipt) {
+ return {thumbnail: null, image: ROUTES.ERECEIPT.getRoute(transaction.transactionID), transaction};
+ }
+
// For local files, we won't have a thumbnail yet
if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) {
return {thumbnail: null, image: path};
diff --git a/src/libs/ReportActionComposeFocusManager.ts b/src/libs/ReportActionComposeFocusManager.ts
index ca4f9d77898b..65466fa4a204 100644
--- a/src/libs/ReportActionComposeFocusManager.ts
+++ b/src/libs/ReportActionComposeFocusManager.ts
@@ -1,5 +1,7 @@
import React from 'react';
import {TextInput} from 'react-native';
+import ROUTES from '../ROUTES';
+import Navigation from './Navigation/Navigation';
type FocusCallback = () => void;
@@ -28,6 +30,11 @@ function onComposerFocus(callback: FocusCallback, isMainComposer = false) {
* Request focus on the ReportActionComposer
*/
function focus() {
+ /** Do not trigger the refocusing when the active route is not the report route, */
+ if (!Navigation.isActiveRoute(ROUTES.REPORT_WITH_ID.getRoute(Navigation.getTopmostReportId() ?? ''))) {
+ return;
+ }
+
if (typeof focusCallback !== 'function') {
if (typeof mainComposerFocusCallback !== 'function') {
return;
diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.ts
similarity index 50%
rename from src/libs/ReportActionsUtils.js
rename to src/libs/ReportActionsUtils.ts
index 258582d9f653..1f71b290e386 100644
--- a/src/libs/ReportActionsUtils.js
+++ b/src/libs/ReportActionsUtils.ts
@@ -1,16 +1,23 @@
-/* eslint-disable rulesdir/prefer-underscore-method */
-import lodashGet from 'lodash/get';
-import _ from 'underscore';
-import {max, parseISO, isEqual} from 'date-fns';
+import {isEqual, max, parseISO} from 'date-fns';
import lodashFindLast from 'lodash/findLast';
-import Onyx from 'react-native-onyx';
-import * as CollectionUtils from './CollectionUtils';
+import Onyx, {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
+import {ValueOf} from 'type-fest';
import CONST from '../CONST';
import ONYXKEYS from '../ONYXKEYS';
+import ReportAction, {ReportActions} from '../types/onyx/ReportAction';
+import Report from '../types/onyx/Report';
+import {ActionName} from '../types/onyx/OriginalMessage';
+import * as CollectionUtils from './CollectionUtils';
import Log from './Log';
import isReportMessageAttachment from './isReportMessageAttachment';
-const allReports = {};
+type LastVisibleMessage = {
+ lastMessageTranslationKey?: string;
+ lastMessageText: string;
+ lastMessageHtml?: string;
+};
+
+const allReports: OnyxCollection = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
callback: (report, key) => {
@@ -23,7 +30,7 @@ Onyx.connect({
},
});
-const allReportActions = {};
+const allReportActions: OnyxCollection = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
callback: (actions, key) => {
@@ -39,126 +46,85 @@ Onyx.connect({
let isNetworkOffline = false;
Onyx.connect({
key: ONYXKEYS.NETWORK,
- callback: (val) => (isNetworkOffline = lodashGet(val, 'isOffline', false)),
+ callback: (val) => (isNetworkOffline = val?.isOffline ?? false),
});
-/**
- * @param {Object} reportAction
- * @returns {Boolean}
- */
-function isCreatedAction(reportAction) {
- return lodashGet(reportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED;
+function isCreatedAction(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED;
}
-/**
- * @param {Object} reportAction
- * @returns {Boolean}
- */
-function isDeletedAction(reportAction) {
+function isDeletedAction(reportAction: OnyxEntry): boolean {
// A deleted comment has either an empty array or an object with html field with empty string as value
- const message = lodashGet(reportAction, 'message', []);
- return message.length === 0 || lodashGet(message, [0, 'html']) === '';
+ const message = reportAction?.message ?? [];
+ return message.length === 0 || message[0]?.html === '';
}
-/**
- * @param {Object} reportAction
- * @returns {Boolean}
- */
-function isDeletedParentAction(reportAction) {
- return lodashGet(reportAction, ['message', 0, 'isDeletedParentAction'], false) && lodashGet(reportAction, 'childVisibleActionCount', 0) > 0;
+function isDeletedParentAction(reportAction: OnyxEntry): boolean {
+ return (reportAction?.message?.[0]?.isDeletedParentAction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0;
}
-/**
- * @param {Object} reportAction
- * @returns {Boolean}
- */
-function isPendingRemove(reportAction) {
- return lodashGet(reportAction, 'message[0].moderationDecision.decision') === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE;
+function isReversedTransaction(reportAction: OnyxEntry) {
+ return (reportAction?.message?.[0].isReversedTransaction ?? false) && (reportAction?.childVisibleActionCount ?? 0) > 0;
}
-/**
- * @param {Object} reportAction
- * @returns {Boolean}
- */
-function isMoneyRequestAction(reportAction) {
- return lodashGet(reportAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.IOU;
+function isPendingRemove(reportAction: OnyxEntry): boolean {
+ return reportAction?.message?.[0]?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE;
}
-/**
- * @param {Object} reportAction
- * @returns {Boolean}
- */
-function isReportPreviewAction(reportAction) {
- return lodashGet(reportAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW;
+function isMoneyRequestAction(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU;
}
-/**
- * @param {Object} reportAction
- * @returns {Boolean}
- */
-function isModifiedExpenseAction(reportAction) {
- return lodashGet(reportAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE;
+function isReportPreviewAction(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW;
+}
+
+function isModifiedExpenseAction(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE;
}
-function isWhisperAction(action) {
- return (action.whisperedToAccountIDs || []).length > 0;
+function isWhisperAction(reportAction: OnyxEntry): boolean {
+ return (reportAction?.whisperedToAccountIDs ?? []).length > 0;
}
/**
* Returns whether the comment is a thread parent message/the first message in a thread
- *
- * @param {Object} reportAction
- * @param {String} reportID
- * @returns {Boolean}
*/
-function isThreadParentMessage(reportAction = {}, reportID) {
- const {childType, childVisibleActionCount = 0, childReportID} = reportAction;
+function isThreadParentMessage(reportAction: OnyxEntry, reportID: string): boolean {
+ const {childType, childVisibleActionCount = 0, childReportID} = reportAction ?? {};
return childType === CONST.REPORT.TYPE.CHAT && (childVisibleActionCount > 0 || String(childReportID) === reportID);
}
/**
* Returns the parentReportAction if the given report is a thread/task.
*
- * @param {Object} report
- * @param {Object} [allReportActionsParam]
- * @returns {Object}
* @deprecated Use Onyx.connect() or withOnyx() instead
*/
-function getParentReportAction(report, allReportActionsParam = undefined) {
- if (!report || !report.parentReportID || !report.parentReportActionID) {
+function getParentReportAction(report: OnyxEntry, allReportActionsParam?: OnyxCollection): ReportAction | Record {
+ if (!report?.parentReportID || !report.parentReportActionID) {
return {};
}
- return lodashGet(allReportActionsParam || allReportActions, [report.parentReportID, report.parentReportActionID], {});
+ return (allReportActionsParam ?? allReportActions)?.[report.parentReportID]?.[report.parentReportActionID] ?? {};
}
/**
* Determines if the given report action is sent money report action by checking for 'pay' type and presence of IOUDetails object.
- *
- * @param {Object} reportAction
- * @returns {Boolean}
*/
-function isSentMoneyReportAction(reportAction) {
+function isSentMoneyReportAction(reportAction: OnyxEntry): boolean {
return (
- reportAction &&
- reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
- lodashGet(reportAction, 'originalMessage.type') === CONST.IOU.REPORT_ACTION_TYPE.PAY &&
- _.has(reportAction.originalMessage, 'IOUDetails')
+ reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction?.originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !!reportAction?.originalMessage?.IOUDetails
);
}
/**
* Returns whether the thread is a transaction thread, which is any thread with IOU parent
* report action from requesting money (type - create) or from sending money (type - pay with IOUDetails field)
- *
- * @param {Object} parentReportAction
- * @returns {Boolean}
*/
-function isTransactionThread(parentReportAction) {
- const originalMessage = lodashGet(parentReportAction, 'originalMessage', {});
+function isTransactionThread(parentReportAction: OnyxEntry): boolean {
return (
- parentReportAction &&
- parentReportAction.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
- (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE || (originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails')))
+ parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU &&
+ (parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.CREATE ||
+ (parentReportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && !!parentReportAction.originalMessage.IOUDetails))
);
}
@@ -166,67 +132,56 @@ function isTransactionThread(parentReportAction) {
* Sort an array of reportActions by their created timestamp first, and reportActionID second
* This gives us a stable order even in the case of multiple reportActions created on the same millisecond
*
- * @param {Array} reportActions
- * @param {Boolean} shouldSortInDescendingOrder
- * @returns {Array}
*/
-function getSortedReportActions(reportActions, shouldSortInDescendingOrder = false) {
- if (!_.isArray(reportActions)) {
+function getSortedReportActions(reportActions: ReportAction[] | null, shouldSortInDescendingOrder = false): ReportAction[] {
+ if (!Array.isArray(reportActions)) {
throw new Error(`ReportActionsUtils.getSortedReportActions requires an array, received ${typeof reportActions}`);
}
const invertedMultiplier = shouldSortInDescendingOrder ? -1 : 1;
- return _.chain(reportActions)
- .compact()
- .sort((first, second) => {
- // First sort by timestamp
- if (first.created !== second.created) {
- return (first.created < second.created ? -1 : 1) * invertedMultiplier;
- }
-
- // Then by action type, ensuring that `CREATED` actions always come first if they have the same timestamp as another action type
- if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED || second.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) && first.actionName !== second.actionName) {
- return (first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED ? -1 : 1) * invertedMultiplier;
- }
- // Ensure that `REPORTPREVIEW` actions always come after if they have the same timestamp as another action type
- if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW || second.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) && first.actionName !== second.actionName) {
- return (first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? 1 : -1) * invertedMultiplier;
- }
-
- // Then fallback on reportActionID as the final sorting criteria. It is a random number,
- // but using this will ensure that the order of reportActions with the same created time and action type
- // will be consistent across all users and devices
- return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier;
- })
- .value();
+
+ return reportActions.filter(Boolean).sort((first, second) => {
+ // First sort by timestamp
+ if (first.created !== second.created) {
+ return (first.created < second.created ? -1 : 1) * invertedMultiplier;
+ }
+
+ // Then by action type, ensuring that `CREATED` actions always come first if they have the same timestamp as another action type
+ if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED || second.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) && first.actionName !== second.actionName) {
+ return (first.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED ? -1 : 1) * invertedMultiplier;
+ }
+ // Ensure that `REPORTPREVIEW` actions always come after if they have the same timestamp as another action type
+ if ((first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW || second.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW) && first.actionName !== second.actionName) {
+ return (first.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? 1 : -1) * invertedMultiplier;
+ }
+
+ // Then fallback on reportActionID as the final sorting criteria. It is a random number,
+ // but using this will ensure that the order of reportActions with the same created time and action type
+ // will be consistent across all users and devices
+ return (first.reportActionID < second.reportActionID ? -1 : 1) * invertedMultiplier;
+ });
}
/**
* Finds most recent IOU request action ID.
- *
- * @param {Array} reportActions
- * @returns {String}
*/
-function getMostRecentIOURequestActionID(reportActions) {
- const iouRequestTypes = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT];
- const iouRequestActions = _.filter(reportActions, (action) => iouRequestTypes.includes(lodashGet(action, 'originalMessage.type')));
+function getMostRecentIOURequestActionID(reportActions: ReportAction[] | null): string | null {
+ const iouRequestTypes: Array> = [CONST.IOU.REPORT_ACTION_TYPE.CREATE, CONST.IOU.REPORT_ACTION_TYPE.SPLIT];
+ const iouRequestActions = reportActions?.filter((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && iouRequestTypes.includes(action.originalMessage.type)) ?? [];
- if (_.isEmpty(iouRequestActions)) {
+ if (iouRequestActions.length === 0) {
return null;
}
const sortedReportActions = getSortedReportActions(iouRequestActions);
- return _.last(sortedReportActions).reportActionID;
+ return sortedReportActions.at(-1)?.reportActionID ?? null;
}
/**
* Returns array of links inside a given report action
- *
- * @param {Object} reportAction
- * @returns {Array}
*/
-function extractLinksFromMessageHtml(reportAction) {
- const htmlContent = lodashGet(reportAction, ['message', 0, 'html']);
+function extractLinksFromMessageHtml(reportAction: OnyxEntry): string[] {
+ const htmlContent = reportAction?.message?.[0]?.html;
// Regex to get link in href prop inside of component
const regex = /]*?\s+)?href="([^"]*)"/gi;
@@ -235,16 +190,19 @@ function extractLinksFromMessageHtml(reportAction) {
return [];
}
- return _.map([...htmlContent.matchAll(regex)], (match) => match[1]);
+ return [...htmlContent.matchAll(regex)].map((match) => match[1]);
}
/**
* Returns the report action immediately before the specified index.
- * @param {Array} reportActions - all actions
- * @param {Number} actionIndex - index of the action
- * @returns {Object|null}
+ * @param reportActions - all actions
+ * @param actionIndex - index of the action
*/
-function findPreviousAction(reportActions, actionIndex) {
+function findPreviousAction(reportActions: ReportAction[] | null, actionIndex: number): OnyxEntry {
+ if (!reportActions) {
+ return null;
+ }
+
for (let i = actionIndex + 1; i < reportActions.length; i++) {
// Find the next non-pending deletion report action, as the pending delete action means that it is not displayed in the UI, but still is in the report actions list.
// If we are offline, all actions are pending but shown in the UI, so we take the previous action, even if it is a delete.
@@ -252,6 +210,7 @@ function findPreviousAction(reportActions, actionIndex) {
return reportActions[i];
}
}
+
return null;
}
@@ -259,13 +218,11 @@ function findPreviousAction(reportActions, actionIndex) {
* Returns true when the report action immediately before the specified index is a comment made by the same actor who who is leaving a comment in the action at the specified index.
* Also checks to ensure that the comment is not too old to be shown as a grouped comment.
*
- * @param {Array} reportActions
- * @param {Number} actionIndex - index of the comment item in state to check
- * @returns {Boolean}
+ * @param actionIndex - index of the comment item in state to check
*/
-function isConsecutiveActionMadeByPreviousActor(reportActions, actionIndex) {
+function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[] | null, actionIndex: number): boolean {
const previousAction = findPreviousAction(reportActions, actionIndex);
- const currentAction = reportActions[actionIndex];
+ const currentAction = reportActions?.[actionIndex];
// It's OK for there to be no previous action, and in that case, false will be returned
// so that the comment isn't grouped
@@ -298,12 +255,8 @@ function isConsecutiveActionMadeByPreviousActor(reportActions, actionIndex) {
/**
* Checks if a reportAction is deprecated.
- *
- * @param {Object} reportAction
- * @param {String} key
- * @returns {Boolean}
*/
-function isReportActionDeprecated(reportAction, key) {
+function isReportActionDeprecated(reportAction: OnyxEntry, key: string): boolean {
if (!reportAction) {
return true;
}
@@ -321,12 +274,12 @@ function isReportActionDeprecated(reportAction, key) {
/**
* Checks if a reportAction is fit for display, meaning that it's not deprecated, is of a valid
* and supported type, it's not deleted and also not closed.
- *
- * @param {Object} reportAction
- * @param {String} key
- * @returns {Boolean}
*/
-function shouldReportActionBeVisible(reportAction, key) {
+function shouldReportActionBeVisible(reportAction: OnyxEntry, key: string): boolean {
+ if (!reportAction) {
+ return false;
+ }
+
if (isReportActionDeprecated(reportAction, key)) {
return false;
}
@@ -335,8 +288,11 @@ function shouldReportActionBeVisible(reportAction, key) {
return false;
}
+ const {POLICYCHANGELOG: policyChangelogTypes, ...otherActionTypes} = CONST.REPORT.ACTIONS.TYPE;
+ const supportedActionTypes: ActionName[] = [...Object.values(otherActionTypes), ...Object.values(policyChangelogTypes)];
+
// Filter out any unsupported reportAction types
- if (!Object.values(CONST.REPORT.ACTIONS.TYPE).includes(reportAction.actionName) && !Object.values(CONST.REPORT.ACTIONS.TYPE.POLICYCHANGELOG).includes(reportAction.actionName)) {
+ if (!supportedActionTypes.includes(reportAction.actionName)) {
return false;
}
@@ -352,69 +308,57 @@ function shouldReportActionBeVisible(reportAction, key) {
// All other actions are displayed except thread parents, deleted, or non-pending actions
const isDeleted = isDeletedAction(reportAction);
const isPending = !!reportAction.pendingAction;
- return !isDeleted || isPending || isDeletedParentAction(reportAction);
+ return !isDeleted || isPending || isDeletedParentAction(reportAction) || isReversedTransaction(reportAction);
}
/**
* Checks if a reportAction is fit for display as report last action, meaning that
* it satisfies shouldReportActionBeVisible, it's not whisper action and not deleted.
- *
- * @param {Object} reportAction
- * @returns {Boolean}
*/
-function shouldReportActionBeVisibleAsLastAction(reportAction) {
+function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxEntry): boolean {
if (!reportAction) {
return false;
}
- if (!_.isEmpty(reportAction.errors)) {
+ if (Object.keys(reportAction.errors ?? {}).length > 0) {
return false;
}
// If a whisper action is the REPORTPREVIEW action, we are displaying it.
+ // If the action's message text is empty and it is not a deleted parent with visible child actions, hide it. Else, consider the action to be displayable.
return (
shouldReportActionBeVisible(reportAction, reportAction.reportActionID) &&
!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) &&
- !isDeletedAction(reportAction)
+ !(isDeletedAction(reportAction) && !isDeletedParentAction(reportAction))
);
}
-/**
- * @param {String} reportID
- * @param {Object} [actionsToMerge]
- * @return {Object}
- */
-function getLastVisibleAction(reportID, actionsToMerge = {}) {
- const updatedActionsToMerge = {};
+function getLastVisibleAction(reportID: string, actionsToMerge: ReportActions = {}): OnyxEntry {
+ const updatedActionsToMerge: ReportActions = {};
if (actionsToMerge && Object.keys(actionsToMerge).length !== 0) {
Object.keys(actionsToMerge).forEach(
- (actionToMergeID) => (updatedActionsToMerge[actionToMergeID] = {...allReportActions[reportID][actionToMergeID], ...actionsToMerge[actionToMergeID]}),
+ (actionToMergeID) => (updatedActionsToMerge[actionToMergeID] = {...allReportActions?.[reportID]?.[actionToMergeID], ...actionsToMerge[actionToMergeID]}),
);
}
const actions = Object.values({
- ...allReportActions[reportID],
+ ...allReportActions?.[reportID],
...updatedActionsToMerge,
});
const visibleActions = actions.filter((action) => shouldReportActionBeVisibleAsLastAction(action));
if (visibleActions.length === 0) {
- return {};
+ return null;
}
const maxDate = max(visibleActions.map((action) => parseISO(action.created)));
const maxAction = visibleActions.find((action) => isEqual(parseISO(action.created), maxDate));
- return maxAction;
+ return maxAction ?? null;
}
-/**
- * @param {String} reportID
- * @param {Object} [actionsToMerge]
- * @return {Object}
- */
-function getLastVisibleMessage(reportID, actionsToMerge = {}) {
+function getLastVisibleMessage(reportID: string, actionsToMerge: ReportActions = {}): LastVisibleMessage {
const lastVisibleAction = getLastVisibleAction(reportID, actionsToMerge);
- const message = lodashGet(lastVisibleAction, ['message', 0], {});
+ const message = lastVisibleAction?.message?.[0];
- if (isReportMessageAttachment(message)) {
+ if (message && isReportMessageAttachment(message)) {
return {
lastMessageTranslationKey: CONST.TRANSLATION_KEYS.ATTACHMENT,
lastMessageText: CONST.ATTACHMENT_MESSAGE_TEXT,
@@ -428,7 +372,7 @@ function getLastVisibleMessage(reportID, actionsToMerge = {}) {
};
}
- const messageText = lodashGet(message, 'text', '');
+ const messageText = message?.text ?? '';
return {
lastMessageText: String(messageText).replace(CONST.REGEX.AFTER_FIRST_LINE_BREAK, '').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(),
};
@@ -436,12 +380,11 @@ function getLastVisibleMessage(reportID, actionsToMerge = {}) {
/**
* A helper method to filter out report actions keyed by sequenceNumbers.
- *
- * @param {Object} reportActions
- * @returns {Array}
*/
-function filterOutDeprecatedReportActions(reportActions) {
- return _.filter(reportActions, (reportAction, key) => !isReportActionDeprecated(reportAction, key));
+function filterOutDeprecatedReportActions(reportActions: ReportActions | null): ReportAction[] {
+ return Object.entries(reportActions ?? {})
+ .filter(([key, reportAction]) => !isReportActionDeprecated(reportAction, key))
+ .map((entry) => entry[1]);
}
/**
@@ -449,12 +392,11 @@ function filterOutDeprecatedReportActions(reportActions) {
* The report actions need to be sorted by created timestamp first, and reportActionID second
* to ensure they will always be displayed in the same order (in case multiple actions have the same timestamp).
* This is all handled with getSortedReportActions() which is used by several other methods to keep the code DRY.
- *
- * @param {Object} reportActions
- * @returns {Array}
*/
-function getSortedReportActionsForDisplay(reportActions) {
- const filteredReportActions = _.filter(reportActions, (reportAction, key) => shouldReportActionBeVisible(reportAction, key));
+function getSortedReportActionsForDisplay(reportActions: ReportActions | null): ReportAction[] {
+ const filteredReportActions = Object.entries(reportActions ?? {})
+ .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key))
+ .map((entry) => entry[1]);
return getSortedReportActions(filteredReportActions, true);
}
@@ -463,78 +405,66 @@ function getSortedReportActionsForDisplay(reportActions) {
* This method returns the last closed report action so we can always show the correct archived report reason.
* Additionally, archived #admins and #announce do not have the closed report action so we will return null if none is found.
*
- * @param {Object} reportActions
- * @returns {Object|null}
*/
-function getLastClosedReportAction(reportActions) {
+function getLastClosedReportAction(reportActions: ReportActions | null): OnyxEntry {
// If closed report action is not present, return early
- if (!_.some(reportActions, (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED)) {
+ if (!Object.values(reportActions ?? {}).some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED)) {
return null;
}
+
const filteredReportActions = filterOutDeprecatedReportActions(reportActions);
const sortedReportActions = getSortedReportActions(filteredReportActions);
- return lodashFindLast(sortedReportActions, (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED);
+ return lodashFindLast(sortedReportActions, (action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) ?? null;
}
/**
- * @param {Array} onyxData
- * @returns {Object} The latest report action in the `onyxData` or `null` if one couldn't be found
+ * @returns The latest report action in the `onyxData` or `null` if one couldn't be found
*/
-function getLatestReportActionFromOnyxData(onyxData) {
- const reportActionUpdate = _.find(onyxData, (onyxUpdate) => onyxUpdate.key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS));
+function getLatestReportActionFromOnyxData(onyxData: OnyxUpdate[] | null): OnyxEntry {
+ const reportActionUpdate = onyxData?.find((onyxUpdate) => onyxUpdate.key.startsWith(ONYXKEYS.COLLECTION.REPORT_ACTIONS));
if (!reportActionUpdate) {
return null;
}
- const reportActions = _.values(reportActionUpdate.value);
+ const reportActions = Object.values((reportActionUpdate.value as ReportActions) ?? {});
const sortedReportActions = getSortedReportActions(reportActions);
- return _.last(sortedReportActions);
+ return sortedReportActions.at(-1) ?? null;
}
/**
* Find the transaction associated with this reportAction, if one exists.
- *
- * @param {String} reportID
- * @param {String} reportActionID
- * @returns {String|null}
*/
-function getLinkedTransactionID(reportID, reportActionID) {
- const reportAction = lodashGet(allReportActions, [reportID, reportActionID]);
+function getLinkedTransactionID(reportID: string, reportActionID: string): string | null {
+ const reportAction = allReportActions?.[reportID]?.[reportActionID];
if (!reportAction || reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU) {
return null;
}
- return reportAction.originalMessage.IOUTransactionID;
+ return reportAction.originalMessage.IOUTransactionID ?? null;
}
-/**
- *
- * @param {String} reportID
- * @param {String} reportActionID
- * @returns {Object}
- */
-function getReportAction(reportID, reportActionID) {
- return lodashGet(allReportActions, [reportID, reportActionID], {});
+function getReportAction(reportID: string, reportActionID: string): OnyxEntry {
+ return allReportActions?.[reportID]?.[reportActionID] ?? null;
}
-/**
- * @returns {string}
- */
-function getMostRecentReportActionLastModified() {
+function getMostRecentReportActionLastModified(): string {
// Start with the oldest date possible
let mostRecentReportActionLastModified = new Date(0).toISOString();
// Flatten all the actions
// Loop over them all to find the one that is the most recent
- const flatReportActions = _.flatten(_.map(allReportActions, (actions) => _.values(actions)));
- _.each(flatReportActions, (action) => {
+ const flatReportActions = Object.values(allReportActions ?? {})
+ .flatMap((actions) => (actions ? Object.values(actions) : []))
+ .filter(Boolean);
+ flatReportActions.forEach((action) => {
// Pending actions should not be counted here as a user could create a comment or some other action while offline and the server might know about
// messages they have not seen yet.
- if (!_.isEmpty(action.pendingAction)) {
+ if (action.pendingAction) {
return;
}
- const lastModified = action.lastModified || action.created;
+ const lastModified = action.lastModified ?? action.created;
+
if (lastModified < mostRecentReportActionLastModified) {
return;
}
@@ -544,8 +474,8 @@ function getMostRecentReportActionLastModified() {
// We might not have actions so we also look at the report objects to see if any have a lastVisibleActionLastModified that is more recent. We don't need to get
// any reports that have been updated before either a recently updated report or reportAction as we should be up to date on these
- _.each(allReports, (report) => {
- const reportLastVisibleActionLastModified = report.lastVisibleActionLastModified || report.lastVisibleActionCreated;
+ Object.values(allReports ?? {}).forEach((report) => {
+ const reportLastVisibleActionLastModified = report?.lastVisibleActionLastModified ?? report?.lastVisibleActionCreated;
if (!reportLastVisibleActionLastModified || reportLastVisibleActionLastModified < mostRecentReportActionLastModified) {
return;
}
@@ -557,66 +487,47 @@ function getMostRecentReportActionLastModified() {
}
/**
- * @param {*} chatReportID
- * @param {*} iouReportID
- * @returns {Object} The report preview action or `null` if one couldn't be found
+ * @returns The report preview action or `null` if one couldn't be found
*/
-function getReportPreviewAction(chatReportID, iouReportID) {
- return _.find(
- allReportActions[chatReportID],
- (reportAction) => reportAction && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lodashGet(reportAction, 'originalMessage.linkedReportID') === iouReportID,
+function getReportPreviewAction(chatReportID: string, iouReportID: string): OnyxEntry {
+ return (
+ Object.values(allReportActions?.[chatReportID] ?? {}).find(
+ (reportAction) => reportAction && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && reportAction.originalMessage.linkedReportID === iouReportID,
+ ) ?? null
);
}
/**
* Get the iouReportID for a given report action.
- *
- * @param {Object} reportAction
- * @returns {String}
*/
-function getIOUReportIDFromReportActionPreview(reportAction) {
- return lodashGet(reportAction, 'originalMessage.linkedReportID', '');
+function getIOUReportIDFromReportActionPreview(reportAction: OnyxEntry): string {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW ? reportAction.originalMessage.linkedReportID : '';
}
-function isCreatedTaskReportAction(reportAction) {
- return reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && _.has(reportAction.originalMessage, 'taskReportID');
+function isCreatedTaskReportAction(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT && !!reportAction.originalMessage?.taskReportID;
}
/**
* A helper method to identify if the message is deleted or not.
- *
- * @param {Object} reportAction
- * @returns {Boolean}
*/
-function isMessageDeleted(reportAction) {
- return lodashGet(reportAction, ['message', 0, 'isDeletedParentAction'], false);
+function isMessageDeleted(reportAction: OnyxEntry): boolean {
+ return reportAction?.message?.[0]?.isDeletedParentAction ?? false;
}
/**
* Returns the number of money requests associated with a report preview
- *
- * @param {Object|null} reportPreviewAction
- * @returns {Number}
*/
-function getNumberOfMoneyRequests(reportPreviewAction) {
- return lodashGet(reportPreviewAction, 'childMoneyRequestCount', 0);
+function getNumberOfMoneyRequests(reportPreviewAction: OnyxEntry): number {
+ return reportPreviewAction?.childMoneyRequestCount ?? 0;
}
-/**
- * @param {*} reportAction
- * @returns {Boolean}
- */
-function isSplitBillAction(reportAction) {
- return lodashGet(reportAction, 'originalMessage.type', '') === CONST.IOU.REPORT_ACTION_TYPE.SPLIT;
+function isSplitBillAction(reportAction: OnyxEntry): boolean {
+ return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU && reportAction.originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.SPLIT;
}
-/**
- *
- * @param {*} reportAction
- * @returns {Boolean}
- */
-function isTaskAction(reportAction) {
- const reportActionName = lodashGet(reportAction, 'actionName', '');
+function isTaskAction(reportAction: OnyxEntry): boolean {
+ const reportActionName = reportAction?.actionName;
return (
reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED ||
reportActionName === CONST.REPORT.ACTIONS.TYPE.TASKCANCELLED ||
@@ -624,66 +535,76 @@ function isTaskAction(reportAction) {
);
}
-/**
- * @param {*} reportID
- * @returns {[Object]}
- */
-function getAllReportActions(reportID) {
- return lodashGet(allReportActions, reportID, []);
+function getAllReportActions(reportID: string): ReportActions {
+ return allReportActions?.[reportID] ?? {};
}
/**
* Check whether a report action is an attachment (a file, such as an image or a zip).
*
- * @param {Object} reportAction report action
- * @returns {Boolean}
*/
-function isReportActionAttachment(reportAction) {
- const message = _.first(lodashGet(reportAction, 'message', [{}]));
- return _.has(reportAction, 'isAttachment') ? reportAction.isAttachment : isReportMessageAttachment(message);
+function isReportActionAttachment(reportAction: OnyxEntry): boolean {
+ const message = reportAction?.message?.[0];
+
+ if (reportAction && 'isAttachment' in reportAction) {
+ return reportAction.isAttachment ?? false;
+ }
+
+ if (message) {
+ return isReportMessageAttachment(message);
+ }
+
+ return false;
}
// eslint-disable-next-line rulesdir/no-negated-variables
-function isNotifiableReportAction(reportAction) {
- return reportAction && _.contains([CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE], reportAction.actionName);
+function isNotifiableReportAction(reportAction: OnyxEntry): boolean {
+ if (!reportAction) {
+ return false;
+ }
+
+ const actions: ActionName[] = [CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT, CONST.REPORT.ACTIONS.TYPE.IOU, CONST.REPORT.ACTIONS.TYPE.MODIFIEDEXPENSE];
+
+ return actions.includes(reportAction.actionName);
}
export {
- getSortedReportActions,
- getLastVisibleAction,
- getLastVisibleMessage,
- getMostRecentIOURequestActionID,
extractLinksFromMessageHtml,
- isCreatedAction,
- isDeletedAction,
- shouldReportActionBeVisible,
- shouldReportActionBeVisibleAsLastAction,
- isReportActionDeprecated,
- isConsecutiveActionMadeByPreviousActor,
- getSortedReportActionsForDisplay,
+ getAllReportActions,
+ getIOUReportIDFromReportActionPreview,
getLastClosedReportAction,
+ getLastVisibleAction,
+ getLastVisibleMessage,
getLatestReportActionFromOnyxData,
- isMoneyRequestAction,
- isThreadParentMessage,
getLinkedTransactionID,
+ getMostRecentIOURequestActionID,
getMostRecentReportActionLastModified,
+ getNumberOfMoneyRequests,
+ getParentReportAction,
+ getReportAction,
getReportPreviewAction,
+ getSortedReportActions,
+ getSortedReportActionsForDisplay,
+ isConsecutiveActionMadeByPreviousActor,
+ isCreatedAction,
isCreatedTaskReportAction,
- getParentReportAction,
- isTransactionThread,
- isSentMoneyReportAction,
+ isDeletedAction,
isDeletedParentAction,
- isReportPreviewAction,
- isModifiedExpenseAction,
- getIOUReportIDFromReportActionPreview,
isMessageDeleted,
- isWhisperAction,
+ isModifiedExpenseAction,
+ isMoneyRequestAction,
+ isNotifiableReportAction,
isPendingRemove,
- getReportAction,
- getNumberOfMoneyRequests,
+ isReversedTransaction,
+ isReportActionAttachment,
+ isReportActionDeprecated,
+ isReportPreviewAction,
+ isSentMoneyReportAction,
isSplitBillAction,
isTaskAction,
- getAllReportActions,
- isReportActionAttachment,
- isNotifiableReportAction,
+ isThreadParentMessage,
+ isTransactionThread,
+ isWhisperAction,
+ shouldReportActionBeVisible,
+ shouldReportActionBeVisibleAsLastAction,
};
diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js
index 591656b5c06a..4e351d2dc5e3 100644
--- a/src/libs/ReportUtils.js
+++ b/src/libs/ReportUtils.js
@@ -1212,6 +1212,46 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR
});
}
+/**
+ * For a deleted parent report action within a chat report,
+ * let us return the appropriate display message
+ *
+ * @param {Object} reportAction - The deleted report action of a chat report for which we need to return message.
+ * @return {String}
+ */
+function getDeletedParentActionMessageForChatReport(reportAction) {
+ // By default, let us display [Deleted message]
+ let deletedMessageText = Localize.translateLocal('parentReportAction.deletedMessage');
+ if (ReportActionsUtils.isCreatedTaskReportAction(reportAction)) {
+ // For canceled task report, let us display [Deleted task]
+ deletedMessageText = Localize.translateLocal('parentReportAction.deletedTask');
+ }
+ return deletedMessageText;
+}
+
+/**
+ * Returns the last visible message for a given report after considering the given optimistic actions
+ *
+ * @param {String} reportID - the report for which last visible message has to be fetched
+ * @param {Object} [actionsToMerge] - the optimistic merge actions that needs to be considered while fetching last visible message
+ * @return {Object}
+ */
+function getLastVisibleMessage(reportID, actionsToMerge = {}) {
+ const report = getReport(reportID);
+ const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID, actionsToMerge);
+
+ // For Chat Report with deleted parent actions, let us fetch the correct message
+ if (ReportActionsUtils.isDeletedParentAction(lastVisibleAction) && isChatReport(report)) {
+ const lastMessageText = getDeletedParentActionMessageForChatReport(lastVisibleAction);
+ return {
+ lastMessageText,
+ };
+ }
+
+ // Fetch the last visible message for report represented by reportID and based on actions to merge.
+ return ReportActionsUtils.getLastVisibleMessage(reportID, actionsToMerge);
+}
+
/**
* Determines if a report has an IOU that is waiting for an action from the current user (either Pay or Add a credit bank account)
*
@@ -1267,12 +1307,23 @@ 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
* @returns {Number}
*/
-function getMoneyRequestTotal(report, allReportsDict = null) {
+function getMoneyRequestReimbursableTotal(report, allReportsDict = null) {
const allAvailableReports = allReportsDict || allReports;
let moneyRequestReport;
if (isMoneyRequestReport(report)) {
@@ -1283,7 +1334,6 @@ function getMoneyRequestTotal(report, allReportsDict = null) {
}
if (moneyRequestReport) {
const total = lodashGet(moneyRequestReport, 'total', 0);
-
if (total !== 0) {
// There is a possibility that if the Expense report has a negative total.
// This is because there are instances where you can get a credit back on your card,
@@ -1294,6 +1344,45 @@ function getMoneyRequestTotal(report, allReportsDict = null) {
return 0;
}
+/**
+ * @param {Object} report
+ * @param {Object} allReportsDict
+ * @returns {Object}
+ */
+function getMoneyRequestSpendBreakdown(report, allReportsDict = null) {
+ const allAvailableReports = allReportsDict || allReports;
+ let moneyRequestReport;
+ if (isMoneyRequestReport(report)) {
+ moneyRequestReport = report;
+ }
+ if (allAvailableReports && report.hasOutstandingIOU && report.iouReportID) {
+ moneyRequestReport = allAvailableReports[`${ONYXKEYS.COLLECTION.REPORT}${report.iouReportID}`];
+ }
+ if (moneyRequestReport) {
+ let nonReimbursableSpend = lodashGet(moneyRequestReport, 'nonReimbursableTotal', 0);
+ let reimbursableSpend = lodashGet(moneyRequestReport, 'total', 0);
+
+ if (nonReimbursableSpend + reimbursableSpend !== 0) {
+ // There is a possibility that if the Expense report has a negative total.
+ // This is because there are instances where you can get a credit back on your card,
+ // or you enter a negative expense to “offset” future expenses
+ nonReimbursableSpend = isExpenseReport(moneyRequestReport) ? nonReimbursableSpend * -1 : Math.abs(nonReimbursableSpend);
+ reimbursableSpend = isExpenseReport(moneyRequestReport) ? reimbursableSpend * -1 : Math.abs(reimbursableSpend);
+ const totalDisplaySpend = nonReimbursableSpend + reimbursableSpend;
+ return {
+ nonReimbursableSpend,
+ reimbursableSpend,
+ totalDisplaySpend,
+ };
+ }
+ }
+ return {
+ nonReimbursableSpend: 0,
+ reimbursableSpend: 0,
+ totalDisplaySpend: 0,
+ };
+}
+
/**
* Get the title for a policy expense chat which depends on the role of the policy member seeing this report
*
@@ -1333,7 +1422,7 @@ function getPolicyExpenseChatName(report, policy = undefined) {
* @returns {String}
*/
function getMoneyRequestReportName(report, policy = undefined) {
- const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(report), report.currency);
+ const formattedAmount = CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(report), report.currency);
const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID);
const payerPaidAmountMesssage = Localize.translateLocal('iou.payerPaidAmount', {
payer: payerName,
@@ -1344,6 +1433,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});
}
@@ -1356,12 +1449,13 @@ function getMoneyRequestReportName(report, policy = undefined) {
* into a flat object. Used for displaying transactions and sending them in API commands
*
* @param {Object} transaction
+ * @param {Object} createdDateFormat
* @returns {Object}
*/
-function getTransactionDetails(transaction) {
+function getTransactionDetails(transaction, createdDateFormat = CONST.DATE.FNS_FORMAT_STRING) {
const report = getReport(transaction.reportID);
return {
- created: TransactionUtils.getCreated(transaction),
+ created: TransactionUtils.getCreated(transaction, createdDateFormat),
amount: TransactionUtils.getAmount(transaction, isExpenseReport(report)),
currency: TransactionUtils.getCurrency(transaction),
comment: TransactionUtils.getDescription(transaction),
@@ -1370,6 +1464,10 @@ function getTransactionDetails(transaction) {
category: TransactionUtils.getCategory(transaction),
billable: TransactionUtils.getBillable(transaction),
tag: TransactionUtils.getTag(transaction),
+ mccGroup: TransactionUtils.getMCCGroup(transaction),
+ cardID: TransactionUtils.getCardID(transaction),
+ originalAmount: TransactionUtils.getOriginalAmount(transaction),
+ originalCurrency: TransactionUtils.getOriginalCurrency(transaction),
};
}
@@ -1474,7 +1572,11 @@ function hasMissingSmartscanFields(iouReportID) {
* @returns {String}
*/
function getTransactionReportName(reportAction) {
- if (ReportActionsUtils.isDeletedParentAction(reportAction)) {
+ if (ReportActionsUtils.isReversedTransaction(reportAction)) {
+ return Localize.translateLocal('parentReportAction.reversedTransaction');
+ }
+
+ if (ReportActionsUtils.isDeletedAction(reportAction)) {
return Localize.translateLocal('parentReportAction.deletedRequest');
}
@@ -1512,7 +1614,21 @@ function getReportPreviewMessage(report, reportAction = {}, shouldConsiderReceip
return reportActionMessage;
}
- const totalAmount = getMoneyRequestTotal(report);
+ if (!isIOUReport(report) && ReportActionsUtils.isSplitBillAction(reportAction)) {
+ // This covers group chats where the last action is a split bill action
+ const linkedTransaction = TransactionUtils.getLinkedTransaction(reportAction);
+ if (_.isEmpty(linkedTransaction)) {
+ return reportActionMessage;
+ }
+ if (TransactionUtils.isReceiptBeingScanned(linkedTransaction)) {
+ return Localize.translateLocal('iou.receiptScanning');
+ }
+ const {amount, currency, comment} = getTransactionDetails(linkedTransaction);
+ const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency);
+ return Localize.translateLocal('iou.didSplitAmount', {formattedAmount, comment});
+ }
+
+ const totalAmount = getMoneyRequestReimbursableTotal(report);
const payerName = isExpenseReport(report) ? getPolicyName(report) : getDisplayNameForParticipant(report.managerID, true);
const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency);
@@ -1545,7 +1661,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});
}
/**
@@ -1900,15 +2017,11 @@ function getParentNavigationSubtitle(report) {
function navigateToDetailsPage(report) {
const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []);
- if (isChatRoom(report) || isPolicyExpenseChat(report) || isChatThread(report) || isTaskReport(report) || isMoneyRequestReport(report)) {
- Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
- return;
- }
- if (participantAccountIDs.length === 1) {
+ if (isDM(report) && participantAccountIDs.length === 1) {
Navigation.navigate(ROUTES.PROFILE.getRoute(participantAccountIDs[0]));
return;
}
- Navigation.navigate(ROUTES.REPORT_PARTICIPANTS.getRoute(report.reportID));
+ Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
}
/**
@@ -2122,6 +2235,7 @@ function buildOptimisticIOUReport(payeeAccountID, payerAccountID, total, chatRep
reportID: generateReportID(),
state: CONST.REPORT.STATE.SUBMITTED,
stateNum: isSendingMoney ? CONST.REPORT.STATE_NUM.SUBMITTED : CONST.REPORT.STATE_NUM.PROCESSING,
+ statusNum: isSendingMoney ? CONST.REPORT.STATUS.REIMBURSED : CONST.REPORT.STATE_NUM.PROCESSING,
total,
// We don't translate reportName because the server response is always in English
@@ -2183,7 +2297,7 @@ function buildOptimisticExpenseReport(chatReportID, policyID, payeeAccountID, to
function getIOUReportActionMessage(iouReportID, type, total, comment, currency, paymentType = '', isSettlingUp = false) {
const amount =
type === CONST.IOU.REPORT_ACTION_TYPE.PAY
- ? CurrencyUtils.convertToDisplayString(getMoneyRequestTotal(getReport(iouReportID)), currency)
+ ? CurrencyUtils.convertToDisplayString(getMoneyRequestReimbursableTotal(getReport(iouReportID)), currency)
: CurrencyUtils.convertToDisplayString(total, currency);
let paymentMethodMessage;
@@ -2414,6 +2528,7 @@ function buildOptimisticReportPreview(chatReport, iouReport, comment = '', trans
const hasReceipt = TransactionUtils.hasReceipt(transaction);
const isReceiptBeingScanned = hasReceipt && TransactionUtils.isReceiptBeingScanned(transaction);
const message = getReportPreviewMessage(iouReport);
+ const created = DateUtils.getDBTime();
return {
reportActionID: NumberUtils.rand64(),
reportID: chatReport.reportID,
@@ -2430,13 +2545,13 @@ function buildOptimisticReportPreview(chatReport, iouReport, comment = '', trans
type: CONST.REPORT.MESSAGE.TYPE.COMMENT,
},
],
- created: DateUtils.getDBTime(),
+ created,
accountID: iouReport.managerID || 0,
// The preview is initially whispered if created with a receipt, so the actor is the current user as well
actorAccountID: hasReceipt ? currentUserAccountID : iouReport.managerID || 0,
childMoneyRequestCount: 1,
childLastMoneyRequestComment: comment,
- childLastReceiptTransactionIDs: hasReceipt ? transaction.transactionID : '',
+ childRecentReceiptTransactionIDs: hasReceipt ? {[transaction.transactionID]: created} : [],
whisperedToAccountIDs: isReceiptBeingScanned ? [currentUserAccountID] : [],
};
}
@@ -2495,8 +2610,9 @@ function buildOptimisticModifiedExpenseReportAction(transactionThread, oldTransa
*/
function updateReportPreview(iouReport, reportPreviewAction, isPayRequest = false, comment = '', transaction = undefined) {
const hasReceipt = TransactionUtils.hasReceipt(transaction);
- const lastReceiptTransactionIDs = lodashGet(reportPreviewAction, 'childLastReceiptTransactionIDs', '');
- const previousTransactionIDs = lastReceiptTransactionIDs.split(',').slice(0, 2);
+ const recentReceiptTransactions = lodashGet(reportPreviewAction, 'childRecentReceiptTransactionIDs', {});
+ const transactionsToKeep = TransactionUtils.getRecentTransactions(recentReceiptTransactions);
+ const previousTransactions = _.mapObject(recentReceiptTransactions, (value, key) => (_.contains(transactionsToKeep, key) ? value : null));
const message = getReportPreviewMessage(iouReport, reportPreviewAction);
return {
@@ -2512,7 +2628,12 @@ function updateReportPreview(iouReport, reportPreviewAction, isPayRequest = fals
],
childLastMoneyRequestComment: comment || reportPreviewAction.childLastMoneyRequestComment,
childMoneyRequestCount: reportPreviewAction.childMoneyRequestCount + (isPayRequest ? 0 : 1),
- childLastReceiptTransactionIDs: hasReceipt ? [transaction.transactionID, ...previousTransactionIDs].join(',') : lastReceiptTransactionIDs,
+ childRecentReceiptTransactionIDs: hasReceipt
+ ? {
+ [transaction.transactionID]: transaction.created,
+ ...previousTransactions,
+ }
+ : recentReceiptTransactions,
// As soon as we add a transaction without a receipt to the report, it will have ready money requests,
// so we remove the whisper
whisperedToAccountIDs: hasReceipt ? reportPreviewAction.whisperedToAccountIDs : [],
@@ -2570,6 +2691,7 @@ function buildOptimisticTaskReportAction(taskReportID, actionName, message = '')
* @param {String} notificationPreference
* @param {String} parentReportActionID
* @param {String} parentReportID
+ * @param {String} welcomeMessage
* @returns {Object}
*/
function buildOptimisticChatReport(
@@ -2585,6 +2707,7 @@ function buildOptimisticChatReport(
notificationPreference = CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
parentReportActionID = '',
parentReportID = '',
+ welcomeMessage = '',
) {
const currentTime = DateUtils.getDBTime();
return {
@@ -2611,7 +2734,7 @@ function buildOptimisticChatReport(
stateNum: 0,
statusNum: 0,
visibility,
- welcomeMessage: '',
+ welcomeMessage,
writeCapability,
};
}
@@ -2826,6 +2949,7 @@ function buildOptimisticTaskReport(ownerAccountID, assigneeAccountID = 0, parent
policyID,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
statusNum: CONST.REPORT.STATUS.OPEN,
+ notificationPreference: CONST.REPORT.NOTIFICATION_PREFERENCE.ALWAYS,
};
}
@@ -3027,6 +3151,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas,
if (
!report ||
!report.reportID ||
+ !report.type ||
report.isHidden ||
(report.participantAccountIDs &&
report.participantAccountIDs.length === 0 &&
@@ -3367,10 +3492,9 @@ function canRequestMoney(report, participants) {
*
* @param {Object} report
* @param {Array} reportParticipants
- * @param {Array} betas
* @returns {Array}
*/
-function getMoneyRequestOptions(report, reportParticipants, betas) {
+function getMoneyRequestOptions(report, reportParticipants) {
// In any thread or task report, we do not allow any new money requests yet
if (isChatThread(report) || isTaskReport(report)) {
return [];
@@ -3390,19 +3514,23 @@ function getMoneyRequestOptions(report, reportParticipants, betas) {
// 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)) {
- return [CONST.IOU.MONEY_REQUEST_TYPE.SPLIT];
+ // 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.TYPE.SPLIT];
}
// DM chats that only have 2 people will see the Send / Request money options.
// IOU and open or processing expense reports should show the Request option.
// Workspace chats should only see the Request money option or Split option in case of Control policies
return [
- ...(canRequestMoney(report, participants) ? [CONST.IOU.MONEY_REQUEST_TYPE.REQUEST] : []),
+ ...(canRequestMoney(report, participants) ? [CONST.IOU.TYPE.REQUEST] : []),
// Send money option should be visible only in DMs
- ...(Permissions.canUseIOUSend(betas) && isChatReport(report) && !isPolicyExpenseChat(report) && hasSingleParticipantInReport ? [CONST.IOU.MONEY_REQUEST_TYPE.SEND] : []),
+ ...(isChatReport(report) && !isPolicyExpenseChat(report) && hasSingleParticipantInReport ? [CONST.IOU.TYPE.SEND] : []),
];
}
@@ -3547,7 +3675,8 @@ function shouldDisableWriteActions(report) {
* @returns {String}
*/
function getOriginalReportID(reportID, reportAction) {
- return isThreadFirstChat(reportAction, reportID) ? lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, 'parentReportID']) : reportID;
+ const currentReportAction = ReportActionsUtils.getReportAction(reportID, reportAction.reportActionID);
+ return isThreadFirstChat(reportAction, reportID) && _.isEmpty(currentReportAction) ? lodashGet(allReports, [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, 'parentReportID']) : reportID;
}
/**
@@ -3589,14 +3718,6 @@ function getWorkspaceChats(policyID, accountIDs) {
return _.filter(allReports, (report) => isPolicyExpenseChat(report) && lodashGet(report, 'policyID', '') === policyID && _.contains(accountIDs, lodashGet(report, 'ownerAccountID', '')));
}
-/*
- * @param {Object|null} report
- * @returns {Boolean}
- */
-function shouldDisableSettings(report) {
- return !isMoneyRequestReport(report) && !isPolicyExpenseChat(report) && !isChatRoom(report) && !isChatThread(report);
-}
-
/**
* @param {Object|null} report
* @param {Object|null} policy - the workspace the report is on, null if the user isn't a member of the workspace
@@ -3758,27 +3879,6 @@ function getParticipantsIDs(report) {
return participants;
}
-/**
- * Get the last 3 transactions with receipts of an IOU report that will be displayed on the report preview
- *
- * @param {Object} reportPreviewAction
- * @returns {Object}
- */
-function getReportPreviewDisplayTransactions(reportPreviewAction) {
- const transactionIDs = lodashGet(reportPreviewAction, ['childLastReceiptTransactionIDs'], '').split(',');
- return _.reduce(
- transactionIDs,
- (transactions, transactionID) => {
- const transaction = TransactionUtils.getTransaction(transactionID);
- if (TransactionUtils.hasReceipt(transaction)) {
- transactions.push(transaction);
- }
- return transactions;
- },
- [],
- );
-}
-
/**
* Return iou report action display message
*
@@ -3792,7 +3892,7 @@ function getIOUReportActionDisplayMessage(reportAction) {
const {amount, currency, IOUReportID} = originalMessage;
const formattedAmount = CurrencyUtils.convertToDisplayString(amount, currency);
const iouReport = getReport(IOUReportID);
- const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport.managerID);
+ const payerName = isExpenseReport(iouReport) ? getPolicyName(iouReport) : getDisplayNameForParticipant(iouReport.managerID, true);
let translationKey;
switch (originalMessage.paymentType) {
case CONST.IOU.PAYMENT_TYPE.ELSEWHERE:
@@ -3824,7 +3924,15 @@ function getIOUReportActionDisplayMessage(reportAction) {
* @returns {Boolean}
*/
function isReportDraft(report) {
- return lodashGet(report, 'stateNum') === CONST.REPORT.STATE_NUM.OPEN && lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.OPEN;
+ return isExpenseReport(report) && lodashGet(report, 'stateNum') === CONST.REPORT.STATE_NUM.OPEN && lodashGet(report, 'statusNum') === CONST.REPORT.STATUS.OPEN;
+}
+
+/**
+ * @param {Object} report
+ * @returns {Boolean}
+ */
+function shouldUseFullTitleToDisplay(report) {
+ return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report);
}
export {
@@ -3859,7 +3967,8 @@ export {
hasExpensifyGuidesEmails,
isWaitingForIOUActionFromCurrentUser,
isIOUOwnedByCurrentUser,
- getMoneyRequestTotal,
+ getMoneyRequestReimbursableTotal,
+ getMoneyRequestSpendBreakdown,
canShowReportRecipientLocalTime,
formatReportLastMessageText,
chatIncludesConcierge,
@@ -3874,6 +3983,8 @@ export {
getReport,
getReportIDFromLink,
getRouteFromLink,
+ getDeletedParentActionMessageForChatReport,
+ getLastVisibleMessage,
navigateToDetailsPage,
generateReportID,
hasReportNameError,
@@ -3955,7 +4066,6 @@ export {
getPolicy,
getPolicyExpenseChatReportIDByOwner,
getWorkspaceChats,
- shouldDisableSettings,
shouldDisableRename,
hasSingleParticipant,
getReportRecipientAccountIDs,
@@ -3967,10 +4077,11 @@ export {
canEditMoneyRequest,
buildTransactionThread,
areAllRequestsBeingSmartScanned,
- getReportPreviewDisplayTransactions,
getTransactionsWithReceipts,
+ hasNonReimbursableTransactions,
hasMissingSmartscanFields,
getIOUReportActionDisplayMessage,
isWaitingForTaskCompleteFromAssignee,
isReportDraft,
+ shouldUseFullTitleToDisplay,
};
diff --git a/src/libs/Request.ts b/src/libs/Request.ts
index 903e70358da9..9c4af4aa7e18 100644
--- a/src/libs/Request.ts
+++ b/src/libs/Request.ts
@@ -3,24 +3,24 @@ import enhanceParameters from './Network/enhanceParameters';
import * as NetworkStore from './Network/NetworkStore';
import Request from '../types/onyx/Request';
import Response from '../types/onyx/Response';
-
-type Middleware = (response: Promise, request: Request, isFromSequentialQueue: boolean) => Promise;
+import Middleware from './Middleware/types';
let middlewares: Middleware[] = [];
-function makeXHR(request: Request): Promise {
+function makeXHR(request: Request): Promise {
const finalParameters = enhanceParameters(request.command, request?.data ?? {});
- return NetworkStore.hasReadRequiredDataFromStorage().then(() => {
+ return NetworkStore.hasReadRequiredDataFromStorage().then((): Promise => {
// If we're using the Supportal token and this is not a Supportal request
// let's just return a promise that will resolve itself.
if (NetworkStore.getSupportAuthToken() && !NetworkStore.isSupportRequest(request.command)) {
return new Promise((resolve) => resolve());
}
- return HttpUtils.xhr(request.command, finalParameters, request.type, request.shouldUseSecure);
- }) as Promise;
+
+ return HttpUtils.xhr(request.command, finalParameters, request.type, request.shouldUseSecure) as Promise;
+ });
}
-function processWithMiddleware(request: Request, isFromSequentialQueue = false): Promise {
+function processWithMiddleware(request: Request, isFromSequentialQueue = false): Promise {
return middlewares.reduce((last, middleware) => middleware(last, request, isFromSequentialQueue), makeXHR(request));
}
diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js
index 7a32db660021..dd6db33902fb 100644
--- a/src/libs/SidebarUtils.js
+++ b/src/libs/SidebarUtils.js
@@ -158,7 +158,7 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p
report.displayName = ReportUtils.getReportName(report);
// eslint-disable-next-line no-param-reassign
- report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReportsDict);
+ report.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(report, allReportsDict);
});
// The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order:
@@ -347,17 +347,17 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale,
if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport) && !result.isArchivedRoom) {
const lastAction = visibleReportActionItems[report.reportID];
- if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
+ if (lastAction && lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) {
const newName = lodashGet(lastAction, 'originalMessage.newName', '');
result.alternateText = Localize.translate(preferredLocale, 'newRoomPage.roomRenamedTo', {newName});
- } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED) {
+ } else if (lastAction && lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.TASKREOPENED) {
result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.reopened')}`;
- } else if (lodashGet(lastAction, 'actionName', '') === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED) {
+ } else if (lastAction && lastAction.actionName === CONST.REPORT.ACTIONS.TYPE.TASKCOMPLETED) {
result.alternateText = `${Localize.translate(preferredLocale, 'task.messages.completed')}`;
- } else if (lodashGet(lastAction, 'actionName', '') !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
+ } else if (lastAction && lastAction.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) {
result.alternateText = `${lastActorDisplayName}: ${lastMessageText}`;
} else {
- result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet');
+ result.alternateText = lastAction && lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet');
}
} else {
if (!lastMessageText) {
@@ -384,7 +384,7 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale,
}
result.isIOUReportOwner = ReportUtils.isIOUOwnedByCurrentUser(result);
- result.iouReportAmount = ReportUtils.getMoneyRequestTotal(result);
+ result.iouReportAmount = ReportUtils.getMoneyRequestReimbursableTotal(result);
if (!hasMultipleParticipants) {
result.accountID = personalDetail.accountID;
diff --git a/src/libs/SuggestionUtils.js b/src/libs/SuggestionUtils.js
index aa2640d006c8..9c3e92799334 100644
--- a/src/libs/SuggestionUtils.js
+++ b/src/libs/SuggestionUtils.js
@@ -26,4 +26,22 @@ function trimLeadingSpace(str) {
return str.slice(0, 1) === ' ' ? str.slice(1) : str;
}
-export {getMaxArrowIndex, trimLeadingSpace};
+/**
+ * Checks if space is available to render large suggestion menu
+ * @param {Number} listHeight
+ * @param {Number} composerHeight
+ * @param {Number} totalSuggestions
+ * @returns {Boolean}
+ */
+function hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, totalSuggestions) {
+ const maxSuggestions = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER;
+ const chatFooterHeight = CONST.CHAT_FOOTER_SECONDARY_ROW_HEIGHT + 2 * CONST.CHAT_FOOTER_SECONDARY_ROW_PADDING;
+ const availableHeight = listHeight - composerHeight - chatFooterHeight;
+ const menuHeight =
+ (!totalSuggestions || totalSuggestions > maxSuggestions ? maxSuggestions : totalSuggestions) * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT +
+ CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING * 2;
+
+ return availableHeight > menuHeight;
+}
+
+export {getMaxArrowIndex, trimLeadingSpace, hasEnoughSpaceForLargeSuggestionMenu};
diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts
index beb1f9c323d6..31cad217666c 100644
--- a/src/libs/TransactionUtils.ts
+++ b/src/libs/TransactionUtils.ts
@@ -3,9 +3,10 @@ import {format, parseISO, isValid} from 'date-fns';
import CONST from '../CONST';
import ONYXKEYS from '../ONYXKEYS';
import DateUtils from './DateUtils';
+import {isExpensifyCard} from './CardUtils';
import * as NumberUtils from './NumberUtils';
import {RecentWaypoint, ReportAction, Transaction} from '../types/onyx';
-import {Receipt, Comment, WaypointCollection} from '../types/onyx/Transaction';
+import {Receipt, Comment, WaypointCollection, Waypoint} from '../types/onyx/Transaction';
type AdditionalTransactionChanges = {comment?: string; waypoints?: WaypointCollection};
@@ -58,16 +59,13 @@ function buildOptimisticTransaction(
commentJSON.originalTransactionID = originalTransactionID;
}
- // For the SmartScan to run successfully, we need to pass the merchant field empty to the API
- const defaultMerchant = !receipt || Object.keys(receipt).length === 0 ? CONST.TRANSACTION.DEFAULT_MERCHANT : '';
-
return {
transactionID,
amount,
currency,
reportID,
comment: commentJSON,
- merchant: merchant || defaultMerchant,
+ merchant: merchant || CONST.TRANSACTION.DEFAULT_MERCHANT,
created: created || DateUtils.getDBTime(),
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
receipt,
@@ -78,25 +76,46 @@ function buildOptimisticTransaction(
};
}
+/**
+ * Check if the transaction has an Ereceipt
+ */
+function hasEReceipt(transaction: Transaction | undefined | null): boolean {
+ return !!transaction?.hasEReceipt;
+}
+
function hasReceipt(transaction: Transaction | undefined | null): boolean {
- return !!transaction?.receipt?.state;
+ return !!transaction?.receipt?.state || hasEReceipt(transaction);
}
-function areRequiredFieldsEmpty(transaction: Transaction): boolean {
- return (
+function isMerchantMissing(transaction: Transaction) {
+ const isMerchantEmpty =
+ transaction.merchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || transaction.merchant === '';
+
+ const isModifiedMerchantEmpty =
+ !transaction.modifiedMerchant ||
transaction.modifiedMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT ||
transaction.modifiedMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT ||
- (transaction.modifiedMerchant === '' &&
- (transaction.merchant === CONST.TRANSACTION.UNKNOWN_MERCHANT || transaction.merchant === '' || transaction.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT)) ||
- (transaction.modifiedAmount === 0 && transaction.amount === 0) ||
- (transaction.modifiedCreated === '' && transaction.created === '')
- );
+ transaction.modifiedMerchant === '';
+
+ return isMerchantEmpty && isModifiedMerchantEmpty;
+}
+
+function isAmountMissing(transaction: Transaction) {
+ return transaction.amount === 0 && (!transaction.modifiedAmount || transaction.modifiedAmount === 0);
+}
+
+function isCreatedMissing(transaction: Transaction) {
+ return transaction.created === '' && (!transaction.created || transaction.modifiedCreated === '');
+}
+
+function areRequiredFieldsEmpty(transaction: Transaction): boolean {
+ return isMerchantMissing(transaction) || isAmountMissing(transaction) || isCreatedMissing(transaction);
}
/**
* Given the edit made to the money request, return an updated transaction object.
*/
-function getUpdatedTransaction(transaction: Transaction, transactionChanges: TransactionChanges, isFromExpenseReport: boolean): Transaction {
+function getUpdatedTransaction(transaction: Transaction, transactionChanges: TransactionChanges, isFromExpenseReport: boolean, shouldUpdateReceiptState = true): Transaction {
// Only changing the first level fields so no need for deep clone now
const updatedTransaction = {...transaction};
let shouldStopSmartscan = false;
@@ -143,7 +162,13 @@ function getUpdatedTransaction(transaction: Transaction, transactionChanges: Tra
updatedTransaction.tag = transactionChanges.tag;
}
- if (shouldStopSmartscan && transaction?.receipt && Object.keys(transaction.receipt).length > 0 && transaction?.receipt?.state !== CONST.IOU.RECEIPT_STATE.OPEN) {
+ if (
+ shouldUpdateReceiptState &&
+ shouldStopSmartscan &&
+ transaction?.receipt &&
+ Object.keys(transaction.receipt).length > 0 &&
+ transaction?.receipt?.state !== CONST.IOU.RECEIPT_STATE.OPEN
+ ) {
updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN;
}
@@ -216,6 +241,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.
*/
@@ -244,6 +284,13 @@ function getCategory(transaction: Transaction): string {
return transaction?.category ?? '';
}
+/**
+ * Return the cardID from the transaction.
+ */
+function getCardID(transaction: Transaction): number {
+ return transaction?.cardID ?? 0;
+}
+
/**
* Return the billable field from the transaction. This "billable" field has no "modified" complement.
*/
@@ -261,11 +308,11 @@ function getTag(transaction: Transaction): string {
/**
* Return the created field from the transaction, return the modifiedCreated if present.
*/
-function getCreated(transaction: Transaction): string {
+function getCreated(transaction: Transaction, dateFormat: string = CONST.DATE.FNS_FORMAT_STRING): string {
const created = transaction?.modifiedCreated ? transaction.modifiedCreated : transaction?.created || '';
const createdDate = parseISO(created);
if (isValid(createdDate)) {
- return format(createdDate, CONST.DATE.FNS_FORMAT_STRING);
+ return format(createdDate, dateFormat);
}
return '';
@@ -277,6 +324,36 @@ 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;
+ }
+ return isExpensifyCard(transaction.cardID);
+}
+
+/**
+ * Check if the transaction status is set to Pending.
+ */
+function isPending(transaction: Transaction): boolean {
+ if (!transaction.status) {
+ return false;
+ }
+ 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;
+ }
+ return transaction.status === CONST.TRANSACTION.STATUS.POSTED;
+}
+
function isReceiptBeingScanned(transaction: Transaction): boolean {
return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction.receipt.state);
}
@@ -295,13 +372,6 @@ function hasRoute(transaction: Transaction): boolean {
return !!transaction?.routes?.route0?.geometry?.coordinates;
}
-/**
- * Check if the transaction has an Ereceipt
- */
-function hasEreceipt(transaction: Transaction): boolean {
- return !!transaction?.hasEReceipt;
-}
-
/**
* Get the transactions related to a report preview with receipts
* Get the details linked to the IOU reportAction
@@ -329,7 +399,7 @@ function getAllReportTransactions(reportID?: string): Transaction[] {
/**
* Checks if a waypoint has a valid address
*/
-function waypointHasValidAddress(waypoint: RecentWaypoint | null): boolean {
+function waypointHasValidAddress(waypoint: RecentWaypoint | Waypoint): boolean {
return !!waypoint?.address?.trim();
}
@@ -353,7 +423,7 @@ function getValidWaypoints(waypoints: WaypointCollection, reArrangeIndexes = fal
let lastWaypointIndex = -1;
- return waypointValues.reduce((acc, currentWaypoint, index) => {
+ return waypointValues.reduce((acc, currentWaypoint, index) => {
const previousWaypoint = waypointValues[lastWaypointIndex];
// Check if the waypoint has a valid address
@@ -374,6 +444,15 @@ function getValidWaypoints(waypoints: WaypointCollection, reArrangeIndexes = fal
}, {});
}
+/**
+ * Returns the most recent transactions in an object
+ */
+function getRecentTransactions(transactions: Record, size = 2): string[] {
+ return Object.keys(transactions)
+ .sort((transactionID1, transactionID2) => (new Date(transactions[transactionID1]) < new Date(transactions[transactionID2]) ? 1 : -1))
+ .slice(0, size);
+}
+
export {
buildOptimisticTransaction,
getUpdatedTransaction,
@@ -381,6 +460,9 @@ export {
getDescription,
getAmount,
getCurrency,
+ getCardID,
+ getOriginalCurrency,
+ getOriginalAmount,
getMerchant,
getMCCGroup,
getCreated,
@@ -390,13 +472,21 @@ export {
getLinkedTransaction,
getAllReportTransactions,
hasReceipt,
- hasEreceipt,
+ hasEReceipt,
hasRoute,
isReceiptBeingScanned,
getValidWaypoints,
isDistanceRequest,
+ isExpensifyCardTransaction,
+ isPending,
+ isPosted,
getWaypoints,
+ isAmountMissing,
+ isMerchantMissing,
+ isCreatedMissing,
+ areRequiredFieldsEmpty,
hasMissingSmartscanFields,
getWaypointIndex,
waypointHasValidAddress,
+ getRecentTransactions,
};
diff --git a/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js b/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js
index 244eaf805d10..4c829239ef14 100644
--- a/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js
+++ b/src/libs/UnreadIndicatorUpdater/updateUnread/index.website.js
@@ -3,6 +3,7 @@
*/
import CONFIG from '../../../CONFIG';
+let unreadTotalCount = 0;
/**
* Set the page title on web
*
@@ -10,7 +11,7 @@ import CONFIG from '../../../CONFIG';
*/
function updateUnread(totalCount) {
const hasUnread = totalCount !== 0;
-
+ unreadTotalCount = totalCount;
// This setTimeout is required because due to how react rendering messes with the DOM, the document title can't be modified synchronously, and we must wait until all JS is done
// running before setting the title.
setTimeout(() => {
@@ -22,4 +23,8 @@ function updateUnread(totalCount) {
}, 0);
}
+window.addEventListener('popstate', () => {
+ updateUnread(unreadTotalCount);
+});
+
export default updateUnread;
diff --git a/src/libs/UpdateMultilineInputRange/index.ios.js b/src/libs/UpdateMultilineInputRange/index.ios.js
index 85ed529a33bc..4c10f768a2a2 100644
--- a/src/libs/UpdateMultilineInputRange/index.ios.js
+++ b/src/libs/UpdateMultilineInputRange/index.ios.js
@@ -8,8 +8,9 @@
* See https://github.com/Expensify/App/issues/20836 for more details.
*
* @param {Object} input the input element
+ * @param {boolean} shouldAutoFocus
*/
-export default function updateMultilineInputRange(input) {
+export default function updateMultilineInputRange(input, shouldAutoFocus = true) {
if (!input) {
return;
}
@@ -19,5 +20,7 @@ export default function updateMultilineInputRange(input) {
* Issue: does not scroll multiline input when text exceeds the maximum number of lines
* For more details: https://github.com/Expensify/App/pull/27702#issuecomment-1728651132
*/
- input.focus();
+ if (shouldAutoFocus) {
+ input.focus();
+ }
}
diff --git a/src/libs/UpdateMultilineInputRange/index.js b/src/libs/UpdateMultilineInputRange/index.js
index 179d30dc611d..66fb1889be21 100644
--- a/src/libs/UpdateMultilineInputRange/index.js
+++ b/src/libs/UpdateMultilineInputRange/index.js
@@ -8,8 +8,10 @@
* See https://github.com/Expensify/App/issues/20836 for more details.
*
* @param {Object} input the input element
+ * @param {boolean} shouldAutoFocus
*/
-export default function updateMultilineInputRange(input) {
+// eslint-disable-next-line no-unused-vars
+export default function updateMultilineInputRange(input, shouldAutoFocus = true) {
if (!input) {
return;
}
diff --git a/src/libs/__mocks__/Permissions.ts b/src/libs/__mocks__/Permissions.ts
index 2c062590573e..66ef64bbb994 100644
--- a/src/libs/__mocks__/Permissions.ts
+++ b/src/libs/__mocks__/Permissions.ts
@@ -12,6 +12,5 @@ export default {
...jest.requireActual('../Permissions'),
canUseDefaultRooms: (betas: Beta[]) => betas.includes(CONST.BETAS.DEFAULT_ROOMS),
canUsePolicyRooms: (betas: Beta[]) => betas.includes(CONST.BETAS.POLICY_ROOMS),
- canUseIOUSend: (betas: Beta[]) => betas.includes(CONST.BETAS.IOU_SEND),
canUseCustomStatus: (betas: Beta[]) => betas.includes(CONST.BETAS.CUSTOM_STATUS),
};
diff --git a/src/libs/actions/App.js b/src/libs/actions/App.js
index 7500af6d829e..75520d483f98 100644
--- a/src/libs/actions/App.js
+++ b/src/libs/actions/App.js
@@ -44,6 +44,19 @@ Onyx.connect({
callback: (val) => (preferredLocale = val),
});
+let priorityMode;
+Onyx.connect({
+ key: ONYXKEYS.NVP_PRIORITY_MODE,
+ callback: (nextPriorityMode) => {
+ // When someone switches their priority mode we need to fetch all their chats because only #focus mode works with a subset of a user's chats. This is only possible via the OpenApp command.
+ if (nextPriorityMode === CONST.PRIORITY_MODE.DEFAULT && priorityMode === CONST.PRIORITY_MODE.GSD) {
+ // eslint-disable-next-line no-use-before-define
+ openApp();
+ }
+ priorityMode = nextPriorityMode;
+ },
+});
+
let resolveIsReadyPromise;
const isReadyToOpenApp = new Promise((resolve) => {
resolveIsReadyPromise = resolve;
@@ -207,7 +220,8 @@ function getOnyxDataForOpenOrReconnect(isOpenApp = false) {
*/
function openApp() {
getPolicyParamsForOpenOrReconnect().then((policyParams) => {
- API.read('OpenApp', policyParams, getOnyxDataForOpenOrReconnect(true));
+ const params = {enablePriorityModeFilter: true, ...policyParams};
+ API.read('OpenApp', params, getOnyxDataForOpenOrReconnect(true));
});
}
@@ -336,6 +350,40 @@ function createWorkspaceAndNavigateToIt(policyOwnerEmail = '', makeMeAdmin = fal
.then(endSignOnTransition);
}
+/**
+ * Create a new draft workspace and navigate to it
+ *
+ * @param {String} [policyOwnerEmail] Optional, the email of the account to make the owner of the policy
+ * @param {String} [policyName] Optional, custom policy name we will use for created workspace
+ * @param {Boolean} [transitionFromOldDot] Optional, if the user is transitioning from old dot
+ */
+function createWorkspaceWithPolicyDraftAndNavigateToIt(policyOwnerEmail = '', policyName = '', transitionFromOldDot = false) {
+ const policyID = Policy.generatePolicyID();
+ Policy.createDraftInitialWorkspace(policyOwnerEmail, policyName, policyID);
+
+ Navigation.isNavigationReady()
+ .then(() => {
+ if (transitionFromOldDot) {
+ // We must call goBack() to remove the /transition route from history
+ Navigation.goBack(ROUTES.HOME);
+ }
+ Navigation.navigate(ROUTES.WORKSPACE_INITIAL.getRoute(policyID));
+ })
+ .then(endSignOnTransition);
+}
+
+/**
+ * Create a new workspace and delete the draft
+ *
+ * @param {String} [policyID] the ID of the policy to use
+ * @param {String} [policyName] custom policy name we will use for created workspace
+ * @param {String} [policyOwnerEmail] Optional, the email of the account to make the owner of the policy
+ * @param {Boolean} [makeMeAdmin] Optional, leave the calling account as an admin on the policy
+ */
+function savePolicyDraftByNewWorkspace(policyID, policyName, policyOwnerEmail = '', makeMeAdmin = false) {
+ Policy.createWorkspace(policyOwnerEmail, makeMeAdmin, policyName, policyID);
+}
+
/**
* This action runs when the Navigator is ready and the current route changes
*
@@ -375,9 +423,6 @@ function setUpPoliciesAndNavigate(session, shouldNavigateToAdminChat) {
// Sign out the current user if we're transitioning with a different user
const isTransitioning = Str.startsWith(url.pathname, Str.normalizeUrl(ROUTES.TRANSITION_BETWEEN_APPS));
- if (isLoggingInAsNewUser && isTransitioning) {
- Session.signOut();
- }
const shouldCreateFreePolicy = !isLoggingInAsNewUser && isTransitioning && exitTo === ROUTES.WORKSPACE_NEW;
if (shouldCreateFreePolicy) {
@@ -513,4 +558,6 @@ export {
createWorkspaceAndNavigateToIt,
getMissingOnyxUpdates,
finalReconnectAppAfterActivatingReliableUpdates,
+ savePolicyDraftByNewWorkspace,
+ createWorkspaceWithPolicyDraftAndNavigateToIt,
};
diff --git a/src/libs/actions/BankAccounts.js b/src/libs/actions/BankAccounts.js
deleted file mode 100644
index b1cb09a8a5e2..000000000000
--- a/src/libs/actions/BankAccounts.js
+++ /dev/null
@@ -1,441 +0,0 @@
-import Onyx from 'react-native-onyx';
-import CONST from '../../CONST';
-import * as API from '../API';
-import ONYXKEYS from '../../ONYXKEYS';
-import * as ErrorUtils from '../ErrorUtils';
-import * as PlaidDataProps from '../../pages/ReimbursementAccount/plaidDataPropTypes';
-import Navigation from '../Navigation/Navigation';
-import ROUTES from '../../ROUTES';
-import * as ReimbursementAccount from './ReimbursementAccount';
-
-export {
- goToWithdrawalAccountSetupStep,
- setBankAccountFormValidationErrors,
- resetReimbursementAccount,
- resetFreePlanBankAccount,
- hideBankAccountErrors,
- setWorkspaceIDForReimbursementAccount,
- setBankAccountSubStep,
- updateReimbursementAccountDraft,
- requestResetFreePlanBankAccount,
- cancelResetFreePlanBankAccount,
-} from './ReimbursementAccount';
-export {openPlaidBankAccountSelector, openPlaidBankLogin} from './Plaid';
-export {openOnfidoFlow, answerQuestionsForWallet, verifyIdentity, acceptWalletTerms} from './Wallet';
-
-function clearPlaid() {
- Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, '');
-
- return Onyx.set(ONYXKEYS.PLAID_DATA, PlaidDataProps.plaidDataDefaultProps);
-}
-
-function openPlaidView() {
- clearPlaid().then(() => ReimbursementAccount.setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID));
-}
-
-/**
- * Open the personal bank account setup flow, with an optional exitReportID to redirect to once the flow is finished.
- * @param {String} exitReportID
- */
-function openPersonalBankAccountSetupView(exitReportID) {
- clearPlaid().then(() => {
- if (exitReportID) {
- Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {exitReportID});
- }
- Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT);
- });
-}
-
-function clearPersonalBankAccount() {
- clearPlaid();
- Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {});
-}
-
-function clearOnfidoToken() {
- Onyx.merge(ONYXKEYS.ONFIDO_TOKEN, '');
-}
-
-/**
- * Helper method to build the Onyx data required during setup of a Verified Business Bank Account
- *
- * @returns {Object}
- */
-function getVBBADataForOnyx() {
- return {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: true,
- errors: null,
- },
- },
- ],
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: false,
- errors: null,
- },
- },
- ],
- failureData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: false,
- errors: ErrorUtils.getMicroSecondOnyxError('walletPage.addBankAccountFailure'),
- },
- },
- ],
- };
-}
-
-/**
- * Submit Bank Account step with Plaid data so php can perform some checks.
- *
- * @param {Number} bankAccountID
- * @param {Object} selectedPlaidBankAccount
- */
-function connectBankAccountWithPlaid(bankAccountID, selectedPlaidBankAccount) {
- const commandName = 'ConnectBankAccountWithPlaid';
-
- const parameters = {
- bankAccountID,
- routingNumber: selectedPlaidBankAccount.routingNumber,
- accountNumber: selectedPlaidBankAccount.accountNumber,
- bank: selectedPlaidBankAccount.bankName,
- plaidAccountID: selectedPlaidBankAccount.plaidAccountID,
- plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken,
- };
-
- API.write(commandName, parameters, getVBBADataForOnyx());
-}
-
-/**
- * Adds a bank account via Plaid
- *
- * @param {Object} account
- * @TODO offline pattern for this command will have to be added later once the pattern B design doc is complete
- */
-function addPersonalBankAccount(account) {
- const commandName = 'AddPersonalBankAccount';
-
- const parameters = {
- addressName: account.addressName,
- routingNumber: account.routingNumber,
- accountNumber: account.accountNumber,
- isSavings: account.isSavings,
- setupType: 'plaid',
- bank: account.bankName,
- plaidAccountID: account.plaidAccountID,
- plaidAccessToken: account.plaidAccessToken,
- };
-
- const onyxData = {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.PERSONAL_BANK_ACCOUNT,
- value: {
- isLoading: true,
- errors: null,
- plaidAccountID: account.plaidAccountID,
- },
- },
- ],
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.PERSONAL_BANK_ACCOUNT,
- value: {
- isLoading: false,
- errors: null,
- shouldShowSuccess: true,
- },
- },
- ],
- failureData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.PERSONAL_BANK_ACCOUNT,
- value: {
- isLoading: false,
- errors: ErrorUtils.getMicroSecondOnyxError('walletPage.addBankAccountFailure'),
- },
- },
- ],
- };
-
- API.write(commandName, parameters, onyxData);
-}
-
-function deletePaymentBankAccount(bankAccountID) {
- API.write(
- 'DeletePaymentBankAccount',
- {
- bankAccountID,
- },
- {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.BANK_ACCOUNT_LIST}`,
- value: {[bankAccountID]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}},
- },
- ],
-
- // Sometimes pusher updates aren't received when we close the App while still offline,
- // so we are setting the bankAccount to null here to ensure that it gets cleared out once we come back online.
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.BANK_ACCOUNT_LIST}`,
- value: {[bankAccountID]: null},
- },
- ],
- },
- );
-}
-
-/**
- * Update the user's personal information on the bank account in database.
- *
- * This action is called by the requestor step in the Verified Bank Account flow
- *
- * @param {Object} params
- *
- * @param {String} [params.dob]
- * @param {String} [params.firstName]
- * @param {String} [params.lastName]
- * @param {String} [params.requestorAddressStreet]
- * @param {String} [params.requestorAddressCity]
- * @param {String} [params.requestorAddressState]
- * @param {String} [params.requestorAddressZipCode]
- * @param {String} [params.ssnLast4]
- * @param {String} [params.isControllingOfficer]
- * @param {Object} [params.onfidoData]
- * @param {Boolean} [params.isOnfidoSetupComplete]
- */
-function updatePersonalInformationForBankAccount(params) {
- API.write('UpdatePersonalInformationForBankAccount', params, getVBBADataForOnyx());
-}
-
-/**
- * @param {Number} bankAccountID
- * @param {String} validateCode
- */
-function validateBankAccount(bankAccountID, validateCode) {
- API.write(
- 'ValidateBankAccountWithTransactions',
- {
- bankAccountID,
- validateCode,
- },
- {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: true,
- errors: null,
- },
- },
- ],
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: false,
- },
- },
- ],
- failureData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: false,
- },
- },
- ],
- },
- );
-}
-
-function openReimbursementAccountPage(stepToOpen, subStep, localCurrentStep) {
- const onyxData = {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: true,
- },
- },
- ],
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: false,
- },
- },
- ],
- failureData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
- value: {
- isLoading: false,
- },
- },
- ],
- };
-
- const param = {
- stepToOpen,
- subStep,
- localCurrentStep,
- };
-
- return API.read('OpenReimbursementAccountPage', param, onyxData);
-}
-
-/**
- * Updates the bank account in the database with the company step data
- *
- * @param {Object} bankAccount
- * @param {Number} [bankAccount.bankAccountID]
- *
- * Fields from BankAccount step
- * @param {String} [bankAccount.routingNumber]
- * @param {String} [bankAccount.accountNumber]
- * @param {String} [bankAccount.bankName]
- * @param {String} [bankAccount.plaidAccountID]
- * @param {String} [bankAccount.plaidAccessToken]
- * @param {Boolean} [bankAccount.isSavings]
- *
- * Fields from Company step
- * @param {String} [bankAccount.companyName]
- * @param {String} [bankAccount.addressStreet]
- * @param {String} [bankAccount.addressCity]
- * @param {String} [bankAccount.addressState]
- * @param {String} [bankAccount.addressZipCode]
- * @param {String} [bankAccount.companyPhone]
- * @param {String} [bankAccount.website]
- * @param {String} [bankAccount.companyTaxID]
- * @param {String} [bankAccount.incorporationType]
- * @param {String} [bankAccount.incorporationState]
- * @param {String} [bankAccount.incorporationDate]
- * @param {Boolean} [bankAccount.hasNoConnectionToCannabis]
- * @param {String} policyID
- */
-function updateCompanyInformationForBankAccount(bankAccount, policyID) {
- API.write('UpdateCompanyInformationForBankAccount', {...bankAccount, policyID}, getVBBADataForOnyx());
-}
-
-/**
- * Add beneficial owners for the bank account, accept the ACH terms and conditions and verify the accuracy of the information provided
- *
- * @param {Object} params
- *
- * // ACH Contract Step
- * @param {Boolean} [params.ownsMoreThan25Percent]
- * @param {Boolean} [params.hasOtherBeneficialOwners]
- * @param {Boolean} [params.acceptTermsAndConditions]
- * @param {Boolean} [params.certifyTrueInformation]
- * @param {String} [params.beneficialOwners]
- */
-function updateBeneficialOwnersForBankAccount(params) {
- API.write('UpdateBeneficialOwnersForBankAccount', {...params}, getVBBADataForOnyx());
-}
-
-/**
- * Create the bank account with manually entered data.
- *
- * @param {number} [bankAccountID]
- * @param {String} [accountNumber]
- * @param {String} [routingNumber]
- * @param {String} [plaidMask]
- *
- */
-function connectBankAccountManually(bankAccountID, accountNumber, routingNumber, plaidMask) {
- API.write(
- 'ConnectBankAccountManually',
- {
- bankAccountID,
- accountNumber,
- routingNumber,
- plaidMask,
- },
- getVBBADataForOnyx(),
- );
-}
-
-/**
- * Verify the user's identity via Onfido
- *
- * @param {Number} bankAccountID
- * @param {Object} onfidoData
- */
-function verifyIdentityForBankAccount(bankAccountID, onfidoData) {
- API.write(
- 'VerifyIdentityForBankAccount',
- {
- bankAccountID,
- onfidoData: JSON.stringify(onfidoData),
- },
- getVBBADataForOnyx(),
- );
-}
-
-function openWorkspaceView() {
- API.read('OpenWorkspaceView');
-}
-
-function handlePlaidError(bankAccountID, error, error_description, plaidRequestID) {
- API.write('BankAccount_HandlePlaidError', {
- bankAccountID,
- error,
- error_description,
- plaidRequestID,
- });
-}
-
-/**
- * Set the reimbursement account loading so that it happens right away, instead of when the API command is processed.
- *
- * @param {Boolean} isLoading
- */
-function setReimbursementAccountLoading(isLoading) {
- Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading});
-}
-
-export {
- addPersonalBankAccount,
- clearOnfidoToken,
- clearPersonalBankAccount,
- clearPlaid,
- openPlaidView,
- connectBankAccountManually,
- connectBankAccountWithPlaid,
- deletePaymentBankAccount,
- handlePlaidError,
- openPersonalBankAccountSetupView,
- openReimbursementAccountPage,
- updateBeneficialOwnersForBankAccount,
- updateCompanyInformationForBankAccount,
- updatePersonalInformationForBankAccount,
- openWorkspaceView,
- validateBankAccount,
- verifyIdentityForBankAccount,
- setReimbursementAccountLoading,
-};
diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts
new file mode 100644
index 000000000000..a0d035292773
--- /dev/null
+++ b/src/libs/actions/BankAccounts.ts
@@ -0,0 +1,452 @@
+import Onyx from 'react-native-onyx';
+import CONST from '../../CONST';
+import * as API from '../API';
+import ONYXKEYS from '../../ONYXKEYS';
+import * as ErrorUtils from '../ErrorUtils';
+import * as PlaidDataProps from '../../pages/ReimbursementAccount/plaidDataPropTypes';
+import Navigation from '../Navigation/Navigation';
+import ROUTES from '../../ROUTES';
+import * as ReimbursementAccount from './ReimbursementAccount';
+import type PlaidBankAccount from '../../types/onyx/PlaidBankAccount';
+import type {ACHContractStepProps, BankAccountStepProps, CompanyStepProps, OnfidoData, ReimbursementAccountProps, RequestorStepProps} from '../../types/onyx/ReimbursementAccountDraft';
+import type {OnyxData} from '../../types/onyx/Request';
+import type {BankAccountStep, BankAccountSubStep} from '../../types/onyx/ReimbursementAccount';
+
+export {
+ goToWithdrawalAccountSetupStep,
+ setBankAccountFormValidationErrors,
+ resetReimbursementAccount,
+ resetFreePlanBankAccount,
+ hideBankAccountErrors,
+ setWorkspaceIDForReimbursementAccount,
+ setBankAccountSubStep,
+ updateReimbursementAccountDraft,
+ requestResetFreePlanBankAccount,
+ cancelResetFreePlanBankAccount,
+} from './ReimbursementAccount';
+export {openPlaidBankAccountSelector, openPlaidBankLogin} from './Plaid';
+export {openOnfidoFlow, answerQuestionsForWallet, verifyIdentity, acceptWalletTerms} from './Wallet';
+
+type BankAccountCompanyInformation = BankAccountStepProps & CompanyStepProps & ReimbursementAccountProps;
+
+type ReimbursementAccountStep = BankAccountStep | '';
+
+type ReimbursementAccountSubStep = BankAccountSubStep | '';
+
+function clearPlaid(): Promise {
+ Onyx.set(ONYXKEYS.PLAID_LINK_TOKEN, '');
+
+ return Onyx.set(ONYXKEYS.PLAID_DATA, PlaidDataProps.plaidDataDefaultProps);
+}
+
+function openPlaidView() {
+ clearPlaid().then(() => ReimbursementAccount.setBankAccountSubStep(CONST.BANK_ACCOUNT.SETUP_TYPE.PLAID));
+}
+
+/**
+ * Open the personal bank account setup flow, with an optional exitReportID to redirect to once the flow is finished.
+ */
+function openPersonalBankAccountSetupView(exitReportID: string) {
+ clearPlaid().then(() => {
+ if (exitReportID) {
+ Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {exitReportID});
+ }
+ Navigation.navigate(ROUTES.SETTINGS_ADD_BANK_ACCOUNT);
+ });
+}
+
+/**
+ * Whether after adding a bank account we should continue with the KYC flow
+ */
+function setPersonalBankAccountContinueKYCOnSuccess(onSuccessFallbackRoute: string) {
+ Onyx.merge(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {onSuccessFallbackRoute});
+}
+
+function clearPersonalBankAccount() {
+ clearPlaid();
+ Onyx.set(ONYXKEYS.PERSONAL_BANK_ACCOUNT, {});
+}
+
+function clearOnfidoToken() {
+ Onyx.merge(ONYXKEYS.ONFIDO_TOKEN, '');
+}
+
+/**
+ * Helper method to build the Onyx data required during setup of a Verified Business Bank Account
+ */
+function getVBBADataForOnyx(currentStep?: BankAccountStep): OnyxData {
+ return {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: true,
+ errors: null,
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
+ 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,
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
+ errors: ErrorUtils.getMicroSecondOnyxError('walletPage.addBankAccountFailure'),
+ },
+ },
+ ],
+ };
+}
+
+/**
+ * Submit Bank Account step with Plaid data so php can perform some checks.
+ */
+function connectBankAccountWithPlaid(bankAccountID: number, selectedPlaidBankAccount: PlaidBankAccount) {
+ const commandName = 'ConnectBankAccountWithPlaid';
+
+ type ConnectBankAccountWithPlaidParams = {
+ bankAccountID: number;
+ routingNumber: string;
+ accountNumber: string;
+ bank?: string;
+ plaidAccountID: string;
+ plaidAccessToken: string;
+ };
+
+ const parameters: ConnectBankAccountWithPlaidParams = {
+ bankAccountID,
+ routingNumber: selectedPlaidBankAccount.routingNumber,
+ accountNumber: selectedPlaidBankAccount.accountNumber,
+ bank: selectedPlaidBankAccount.bankName,
+ plaidAccountID: selectedPlaidBankAccount.plaidAccountID,
+ plaidAccessToken: selectedPlaidBankAccount.plaidAccessToken,
+ };
+
+ API.write(commandName, parameters, getVBBADataForOnyx());
+}
+
+/**
+ * Adds a bank account via Plaid
+ *
+ * @TODO offline pattern for this command will have to be added later once the pattern B design doc is complete
+ */
+function addPersonalBankAccount(account: PlaidBankAccount) {
+ const commandName = 'AddPersonalBankAccount';
+
+ type AddPersonalBankAccountParams = {
+ addressName: string;
+ routingNumber: string;
+ accountNumber: string;
+ isSavings: boolean;
+ setupType: string;
+ bank?: string;
+ plaidAccountID: string;
+ plaidAccessToken: string;
+ };
+
+ const parameters: AddPersonalBankAccountParams = {
+ addressName: account.addressName,
+ routingNumber: account.routingNumber,
+ accountNumber: account.accountNumber,
+ isSavings: account.isSavings,
+ setupType: 'plaid',
+ bank: account.bankName,
+ plaidAccountID: account.plaidAccountID,
+ plaidAccessToken: account.plaidAccessToken,
+ };
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_BANK_ACCOUNT,
+ value: {
+ isLoading: true,
+ errors: null,
+ plaidAccountID: account.plaidAccountID,
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_BANK_ACCOUNT,
+ value: {
+ isLoading: false,
+ errors: null,
+ shouldShowSuccess: true,
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_BANK_ACCOUNT,
+ value: {
+ isLoading: false,
+ errors: ErrorUtils.getMicroSecondOnyxError('walletPage.addBankAccountFailure'),
+ },
+ },
+ ],
+ };
+
+ API.write(commandName, parameters, onyxData);
+}
+
+function deletePaymentBankAccount(bankAccountID: number) {
+ type DeletePaymentBankAccountParams = {bankAccountID: number};
+
+ const parameters: DeletePaymentBankAccountParams = {bankAccountID};
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.BANK_ACCOUNT_LIST}`,
+ value: {[bankAccountID]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}},
+ },
+ ],
+
+ // Sometimes pusher updates aren't received when we close the App while still offline,
+ // so we are setting the bankAccount to null here to ensure that it gets cleared out once we come back online.
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.BANK_ACCOUNT_LIST}`,
+ value: {[bankAccountID]: null},
+ },
+ ],
+ };
+
+ API.write('DeletePaymentBankAccount', parameters, onyxData);
+}
+
+/**
+ * Update the user's personal information on the bank account in database.
+ *
+ * This action is called by the requestor step in the Verified Bank Account flow
+ */
+function updatePersonalInformationForBankAccount(params: RequestorStepProps) {
+ API.write('UpdatePersonalInformationForBankAccount', params, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.REQUESTOR));
+}
+
+function validateBankAccount(bankAccountID: number, validateCode: string) {
+ type ValidateBankAccountWithTransactionsParams = {
+ bankAccountID: number;
+ validateCode: string;
+ };
+
+ const parameters: ValidateBankAccountWithTransactionsParams = {
+ bankAccountID,
+ validateCode,
+ };
+
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: true,
+ errors: null,
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ };
+
+ API.write('ValidateBankAccountWithTransactions', parameters, onyxData);
+}
+
+function clearReimbursementAccount() {
+ Onyx.set(ONYXKEYS.REIMBURSEMENT_ACCOUNT, null);
+}
+
+function openReimbursementAccountPage(stepToOpen: ReimbursementAccountStep, subStep: ReimbursementAccountSubStep, localCurrentStep: ReimbursementAccountStep) {
+ const onyxData: OnyxData = {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: true,
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.REIMBURSEMENT_ACCOUNT,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ };
+
+ type OpenReimbursementAccountPageParams = {
+ stepToOpen: ReimbursementAccountStep;
+ subStep: ReimbursementAccountSubStep;
+ localCurrentStep: ReimbursementAccountStep;
+ };
+
+ const parameters: OpenReimbursementAccountPageParams = {
+ stepToOpen,
+ subStep,
+ localCurrentStep,
+ };
+
+ return API.read('OpenReimbursementAccountPage', parameters, onyxData);
+}
+
+/**
+ * Updates the bank account in the database with the company step data
+ */
+function updateCompanyInformationForBankAccount(bankAccount: BankAccountCompanyInformation, policyID: string) {
+ type UpdateCompanyInformationForBankAccountParams = BankAccountCompanyInformation & {policyID: string};
+
+ const parameters: UpdateCompanyInformationForBankAccountParams = {...bankAccount, policyID};
+
+ API.write('UpdateCompanyInformationForBankAccount', parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.COMPANY));
+}
+
+/**
+ * Add beneficial owners for the bank account, accept the ACH terms and conditions and verify the accuracy of the information provided
+ */
+function updateBeneficialOwnersForBankAccount(params: ACHContractStepProps) {
+ API.write('UpdateBeneficialOwnersForBankAccount', params, getVBBADataForOnyx());
+}
+
+/**
+ * Create the bank account with manually entered data.
+ *
+ */
+function connectBankAccountManually(bankAccountID: number, accountNumber?: string, routingNumber?: string, plaidMask?: string) {
+ type ConnectBankAccountManuallyParams = {
+ bankAccountID: number;
+ accountNumber?: string;
+ routingNumber?: string;
+ plaidMask?: string;
+ };
+
+ const parameters: ConnectBankAccountManuallyParams = {
+ bankAccountID,
+ accountNumber,
+ routingNumber,
+ plaidMask,
+ };
+
+ API.write('ConnectBankAccountManually', parameters, getVBBADataForOnyx(CONST.BANK_ACCOUNT.STEP.BANK_ACCOUNT));
+}
+
+/**
+ * Verify the user's identity via Onfido
+ */
+function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoData) {
+ type VerifyIdentityForBankAccountParams = {
+ bankAccountID: number;
+ onfidoData: string;
+ };
+
+ const parameters: VerifyIdentityForBankAccountParams = {
+ bankAccountID,
+ onfidoData: JSON.stringify(onfidoData),
+ };
+
+ API.write('VerifyIdentityForBankAccount', parameters, getVBBADataForOnyx());
+}
+
+function openWorkspaceView() {
+ API.read('OpenWorkspaceView', {}, {});
+}
+
+function handlePlaidError(bankAccountID: number, error: string, errorDescription: string, plaidRequestID: string) {
+ type BankAccountHandlePlaidErrorParams = {
+ bankAccountID: number;
+ error: string;
+ errorDescription: string;
+ plaidRequestID: string;
+ };
+
+ const parameters: BankAccountHandlePlaidErrorParams = {
+ bankAccountID,
+ error,
+ errorDescription,
+ plaidRequestID,
+ };
+
+ API.write('BankAccount_HandlePlaidError', parameters);
+}
+
+/**
+ * Set the reimbursement account loading so that it happens right away, instead of when the API command is processed.
+ */
+function setReimbursementAccountLoading(isLoading: boolean) {
+ Onyx.merge(ONYXKEYS.REIMBURSEMENT_ACCOUNT, {isLoading});
+}
+
+export {
+ addPersonalBankAccount,
+ clearOnfidoToken,
+ clearPersonalBankAccount,
+ clearPlaid,
+ openPlaidView,
+ connectBankAccountManually,
+ connectBankAccountWithPlaid,
+ deletePaymentBankAccount,
+ handlePlaidError,
+ setPersonalBankAccountContinueKYCOnSuccess,
+ openPersonalBankAccountSetupView,
+ clearReimbursementAccount,
+ openReimbursementAccountPage,
+ updateBeneficialOwnersForBankAccount,
+ updateCompanyInformationForBankAccount,
+ updatePersonalInformationForBankAccount,
+ openWorkspaceView,
+ validateBankAccount,
+ verifyIdentityForBankAccount,
+ setReimbursementAccountLoading,
+};
diff --git a/src/libs/actions/CanvasSize.js b/src/libs/actions/CanvasSize.js
index ed83562a3e43..0c4cd88fea70 100644
--- a/src/libs/actions/CanvasSize.js
+++ b/src/libs/actions/CanvasSize.js
@@ -1,16 +1,25 @@
import Onyx from 'react-native-onyx';
import canvasSize from 'canvas-size';
import ONYXKEYS from '../../ONYXKEYS';
+import * as Browser from '../Browser';
/**
* Calculate the max area of canvas on this specific platform and save it in onyx
*/
function retrieveMaxCanvasArea() {
- canvasSize.maxArea({
- onSuccess: (width, height) => {
- Onyx.merge(ONYXKEYS.MAX_CANVAS_AREA, width * height);
- },
- });
+ // We're limiting the maximum value on mobile web to prevent a crash related to rendering large canvas elements.
+ // More information at: https://github.com/jhildenbiddle/canvas-size/issues/13
+ canvasSize
+ .maxArea({
+ max: Browser.isMobile() ? 8192 : null,
+ usePromise: true,
+ useWorker: false,
+ })
+ .then(() => ({
+ onSuccess: (width, height) => {
+ Onyx.merge(ONYXKEYS.MAX_CANVAS_AREA, width * height);
+ },
+ }));
}
/**
diff --git a/src/libs/actions/Card.js b/src/libs/actions/Card.js
new file mode 100644
index 000000000000..92b23e2103ee
--- /dev/null
+++ b/src/libs/actions/Card.js
@@ -0,0 +1,149 @@
+import Onyx from 'react-native-onyx';
+import ONYXKEYS from '../../ONYXKEYS';
+import * as API from '../API';
+
+/**
+ * @param {Number} cardID
+ */
+function reportVirtualExpensifyCardFraud(cardID) {
+ API.write(
+ 'ReportVirtualExpensifyCardFraud',
+ {
+ cardID,
+ },
+ {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD,
+ value: {
+ isLoading: true,
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.REPORT_VIRTUAL_CARD_FRAUD,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ },
+ );
+}
+
+/**
+ * Call the API to deactivate the card and request a new one
+ * @param {String} cardId - id of the card that is going to be replaced
+ * @param {String} reason - reason for replacement ('damaged' | 'stolen')
+ */
+function requestReplacementExpensifyCard(cardId, reason) {
+ API.write(
+ 'RequestReplacementExpensifyCard',
+ {
+ cardId,
+ reason,
+ },
+ {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM,
+ value: {
+ isLoading: true,
+ errors: null,
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.REPORT_PHYSICAL_CARD_FORM,
+ value: {
+ isLoading: false,
+ },
+ },
+ ],
+ },
+ );
+}
+
+/**
+ * Activates the physical Expensify card based on the last four digits of the card number
+ *
+ * @param {Number} lastFourDigits
+ * @param {Number} cardID
+ */
+function activatePhysicalExpensifyCard(lastFourDigits, cardID) {
+ API.write(
+ 'ActivatePhysicalExpensifyCard',
+ {lastFourDigits, cardID},
+ {
+ optimisticData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CARD_LIST,
+ value: {
+ [cardID]: {
+ errors: null,
+ isLoading: true,
+ },
+ },
+ },
+ ],
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CARD_LIST,
+ value: {
+ [cardID]: {
+ isLoading: false,
+ },
+ },
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.CARD_LIST,
+ value: {
+ [cardID]: {
+ isLoading: false,
+ },
+ },
+ },
+ ],
+ },
+ );
+}
+
+/**
+ * Clears errors for a specific cardID
+ *
+ * @param {Number} cardID
+ */
+function clearCardListErrors(cardID) {
+ Onyx.merge(ONYXKEYS.CARD_LIST, {[cardID]: {errors: null, isLoading: false}});
+}
+
+export {requestReplacementExpensifyCard, activatePhysicalExpensifyCard, clearCardListErrors, reportVirtualExpensifyCardFraud};
diff --git a/src/libs/actions/Chronos.ts b/src/libs/actions/Chronos.ts
index 1b46a68a1afe..ce821e524722 100644
--- a/src/libs/actions/Chronos.ts
+++ b/src/libs/actions/Chronos.ts
@@ -1,11 +1,11 @@
-import Onyx from 'react-native-onyx';
+import Onyx, {OnyxUpdate} from 'react-native-onyx';
import CONST from '../../CONST';
import ONYXKEYS from '../../ONYXKEYS';
import * as API from '../API';
import {ChronosOOOEvent} from '../../types/onyx/OriginalMessage';
const removeEvent = (reportID: string, reportActionID: string, eventID: string, events: ChronosOOOEvent[]) => {
- const optimisticData = [
+ const optimisticData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
@@ -20,7 +20,7 @@ const removeEvent = (reportID: string, reportActionID: string, eventID: string,
},
];
- const successData = [
+ const successData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
@@ -32,7 +32,7 @@ const removeEvent = (reportID: string, reportActionID: string, eventID: string,
},
];
- const failureData = [
+ const failureData: OnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`,
diff --git a/src/libs/actions/DemoActions.js b/src/libs/actions/DemoActions.js
new file mode 100644
index 000000000000..e7ce02d2796b
--- /dev/null
+++ b/src/libs/actions/DemoActions.js
@@ -0,0 +1,70 @@
+import Config from 'react-native-config';
+import Onyx from 'react-native-onyx';
+import lodashGet from 'lodash/get';
+import * as API from '../API';
+import * as ReportUtils from '../ReportUtils';
+import Navigation from '../Navigation/Navigation';
+import ROUTES from '../../ROUTES';
+import ONYXKEYS from '../../ONYXKEYS';
+
+let currentUserEmail;
+Onyx.connect({
+ key: ONYXKEYS.SESSION,
+ callback: (val) => {
+ currentUserEmail = lodashGet(val, 'email', '');
+ },
+});
+
+function runMoney2020Demo() {
+ // Try to navigate to existing demo chat if it exists in Onyx
+ const money2020AccountID = Number(lodashGet(Config, 'EXPENSIFY_ACCOUNT_ID_MONEY2020', 15864555));
+ const existingChatReport = ReportUtils.getChatByParticipants([money2020AccountID]);
+ if (existingChatReport) {
+ // We must call goBack() to remove the demo route from nav history
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(existingChatReport.reportID));
+ return;
+ }
+
+ // We use makeRequestWithSideEffects here because we need to get the chat report ID to navigate to it after it's created
+ // eslint-disable-next-line rulesdir/no-api-side-effects-method
+ API.makeRequestWithSideEffects('CreateChatReport', {
+ emailList: `${currentUserEmail},money2020@expensify.com`,
+ activationConference: 'money2020',
+ }).then((response) => {
+ // If there's no response or no reportID in the response, navigate the user home so user doesn't get stuck.
+ if (!response || !response.reportID) {
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.HOME);
+ return;
+ }
+
+ // Get reportID & navigate to it
+ // Note: We must call goBack() to remove the demo route from history
+ const chatReportID = response.reportID;
+ Navigation.goBack();
+ Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(chatReportID));
+ });
+}
+
+/**
+ * Runs code for specific demos, based on the provided URL
+ *
+ * @param {String} url - URL user is navigating to via deep link (or regular link in web)
+ */
+function runDemoByURL(url = '') {
+ const cleanUrl = (url || '').toLowerCase();
+
+ if (cleanUrl.endsWith(ROUTES.MONEY2020)) {
+ Onyx.set(ONYXKEYS.DEMO_INFO, {
+ money2020: {
+ isBeginningDemo: true,
+ },
+ });
+ } else {
+ // No demo is being run, so clear out demo info
+ Onyx.set(ONYXKEYS.DEMO_INFO, {});
+ }
+}
+
+export {runMoney2020Demo, runDemoByURL};
diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js
index 2c046bfc2a24..07e814f92884 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';
@@ -24,6 +25,14 @@ import ReceiptGeneric from '../../../assets/images/receipt-generic.png';
import * as LocalePhoneNumber from '../LocalePhoneNumber';
import * as Policy from './Policy';
+let allPersonalDetails;
+Onyx.connect({
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ callback: (val) => {
+ allPersonalDetails = val || {};
+ },
+});
+
let allReports;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
@@ -45,6 +54,15 @@ Onyx.connect({
},
});
+let allDraftSplitTransactions;
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT,
+ waitForCollectionCallback: true,
+ callback: (val) => {
+ allDraftSplitTransactions = val || {};
+ },
+});
+
let allRecentlyUsedTags = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY_RECENTLY_USED_TAGS,
@@ -116,7 +134,7 @@ function resetMoneyRequestInfo(id = '') {
tag: '',
created,
receiptPath: '',
- receiptSource: '',
+ receiptFilename: '',
transactionID: '',
billable: null,
});
@@ -472,7 +490,10 @@ function getMoneyRequestInformation(
billable,
);
- const optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category);
+ let optimisticPolicyRecentlyUsedCategories = [];
+ if (category) {
+ optimisticPolicyRecentlyUsedCategories = Policy.buildOptimisticPolicyRecentlyUsedCategories(iouReport.policyID, category);
+ }
const optimisticPolicyRecentlyUsedTags = {};
const policyTags = allPolicyTags[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${iouReport.policyID}`];
@@ -1044,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
@@ -1052,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]);
}
@@ -1084,7 +1106,7 @@ function createSplitsAndOnyxData(participants, currentUserLogin, currentUserAcco
oneOnOneIOUReport.reportID,
comment,
'',
- CONST.IOU.MONEY_REQUEST_TYPE.SPLIT,
+ CONST.IOU.TYPE.SPLIT,
splitTransaction.transactionID,
undefined,
undefined,
@@ -1259,6 +1281,467 @@ function splitBillAndOpenReport(participants, currentUserLogin, currentUserAccou
Report.notifyNewAction(splitData.chatReportID, currentUserAccountID);
}
+/** Used exclusively for starting a split bill request that contains a receipt, the split request will be completed once the receipt is scanned
+ * or user enters details manually.
+ *
+ * @param {Array} participants
+ * @param {String} currentUserLogin
+ * @param {Number} currentUserAccountID
+ * @param {String} comment
+ * @param {Object} receipt
+ * @param {String} existingSplitChatReportID - Either a group DM or a workspace chat
+ */
+function startSplitBill(participants, currentUserLogin, currentUserAccountID, comment, receipt, existingSplitChatReportID = '') {
+ const currentUserEmailForIOUSplit = OptionsListUtils.addSMSDomainIfPhoneNumber(currentUserLogin);
+ const participantAccountIDs = _.map(participants, (participant) => Number(participant.accountID));
+ const existingSplitChatReport =
+ existingSplitChatReportID || participants[0].reportID
+ ? allReports[`${ONYXKEYS.COLLECTION.REPORT}${existingSplitChatReportID || participants[0].reportID}`]
+ : ReportUtils.getChatByParticipants(participantAccountIDs);
+ const splitChatReport = existingSplitChatReport || ReportUtils.buildOptimisticChatReport(participantAccountIDs);
+ const isOwnPolicyExpenseChat = splitChatReport.isOwnPolicyExpenseChat || false;
+
+ const {name: filename, source, state = CONST.IOU.RECEIPT_STATE.SCANREADY} = receipt;
+ 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,
+ '',
+ '',
+ '',
+ 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);
+ const splitIOUReportAction = ReportUtils.buildOptimisticIOUReportAction(
+ CONST.IOU.REPORT_ACTION_TYPE.SPLIT,
+ 0,
+ CONST.CURRENCY.USD,
+ comment,
+ participants,
+ splitTransaction.transactionID,
+ '',
+ '',
+ false,
+ false,
+ receiptObject,
+ isOwnPolicyExpenseChat,
+ );
+
+ splitChatReport.lastReadTime = DateUtils.getDBTime();
+ splitChatReport.lastMessageText = splitIOUReportAction.message[0].text;
+ splitChatReport.lastMessageHtml = splitIOUReportAction.message[0].html;
+
+ // If we have an existing splitChatReport (group chat or workspace) use it's pending fields, otherwise indicate that we are adding a chat
+ if (!existingSplitChatReport) {
+ splitChatReport.pendingFields = {
+ createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ };
+ }
+
+ const optimisticData = [
+ {
+ // Use set for new reports because it doesn't exist yet, is faster,
+ // and we need the data to be available when we navigate to the chat page
+ onyxMethod: existingSplitChatReport ? Onyx.METHOD.MERGE : Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${splitChatReport.reportID}`,
+ value: splitChatReport,
+ },
+ {
+ onyxMethod: existingSplitChatReport ? Onyx.METHOD.MERGE : Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitChatReport.reportID}`,
+ value: {
+ ...(existingSplitChatReport ? {} : {[splitChatCreatedReportAction.reportActionID]: splitChatCreatedReportAction}),
+ [splitIOUReportAction.reportActionID]: splitIOUReportAction,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`,
+ value: splitTransaction,
+ },
+ ];
+
+ const successData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitChatReport.reportID}`,
+ value: {
+ ...(existingSplitChatReport ? {} : {[splitChatCreatedReportAction.reportActionID]: {pendingAction: null}}),
+ [splitIOUReportAction.reportActionID]: {pendingAction: null},
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`,
+ value: {pendingAction: null},
+ },
+ ];
+
+ if (!existingSplitChatReport) {
+ successData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${splitChatReport.reportID}`,
+ value: {pendingFields: {createChat: null}},
+ });
+ }
+
+ const failureData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`,
+ value: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ },
+ },
+ ];
+
+ if (existingSplitChatReport) {
+ failureData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitChatReport.reportID}`,
+ value: {
+ [splitIOUReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ },
+ },
+ });
+ } else {
+ failureData.push(
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT}${splitChatReport.reportID}`,
+ value: {
+ errorFields: {
+ createChat: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${splitChatReport.reportID}`,
+ value: {
+ [splitChatCreatedReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('report.genericCreateReportFailureMessage'),
+ },
+ [splitIOUReportAction.reportActionID]: {
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ },
+ },
+ },
+ );
+ }
+
+ const splits = [{email: currentUserEmailForIOUSplit, accountID: currentUserAccountID}];
+
+ _.each(participants, (participant) => {
+ const email = participant.isOwnPolicyExpenseChat ? '' : OptionsListUtils.addSMSDomainIfPhoneNumber(participant.login || participant.text).toLowerCase();
+ const accountID = participant.isOwnPolicyExpenseChat ? 0 : Number(participant.accountID);
+ if (email === currentUserEmailForIOUSplit) {
+ return;
+ }
+
+ // When splitting with a workspace chat, we only need to supply the policyID and the workspace reportID as it's needed so we can update the report preview
+ if (participant.isOwnPolicyExpenseChat) {
+ splits.push({
+ policyID: participant.policyID,
+ chatReportID: splitChatReport.reportID,
+ });
+ return;
+ }
+
+ const participantPersonalDetails = allPersonalDetails[participant.accountID];
+ if (!participantPersonalDetails) {
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.PERSONAL_DETAILS_LIST,
+ value: {
+ [accountID]: {
+ accountID,
+ avatar: UserUtils.getDefaultAvatarURL(accountID),
+ displayName: LocalePhoneNumber.formatPhoneNumber(participant.displayName || email),
+ login: participant.login || participant.text,
+ isOptimisticPersonalDetail: true,
+ },
+ },
+ });
+ }
+
+ splits.push({
+ email,
+ accountID,
+ });
+ });
+
+ // Save the new splits array into the transaction's comment in case the user calls CompleteSplitBill while offline
+ optimisticData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${splitTransaction.transactionID}`,
+ value: {
+ comment: {
+ splits,
+ },
+ },
+ });
+
+ API.write(
+ 'StartSplitBill',
+ {
+ chatReportID: splitChatReport.reportID,
+ reportActionID: splitIOUReportAction.reportActionID,
+ transactionID: splitTransaction.transactionID,
+ splits: JSON.stringify(splits),
+ receipt,
+ comment,
+ isFromGroupDM: !existingSplitChatReport,
+ ...(existingSplitChatReport ? {} : {createdReportActionID: splitChatCreatedReportAction.reportActionID}),
+ },
+ {optimisticData, successData, failureData},
+ );
+
+ resetMoneyRequestInfo();
+ Navigation.dismissModal(splitChatReport.reportID);
+ Report.notifyNewAction(splitChatReport.chatReportID, currentUserAccountID);
+}
+
+/** Used for editing a split bill while it's still scanning or when SmartScan fails, it completes a split bill started by startSplitBill above.
+ *
+ * @param {number} chatReportID - The group chat or workspace reportID
+ * @param {Object} reportAction - The split action that lives in the chatReport above
+ * @param {Object} updatedTransaction - The updated **draft** split transaction
+ * @param {Number} sessionAccountID - accountID of the current user
+ * @param {String} sessionEmail - email of the current user
+ */
+function completeSplitBill(chatReportID, reportAction, updatedTransaction, sessionAccountID, sessionEmail) {
+ const currentUserEmailForIOUSplit = OptionsListUtils.addSMSDomainIfPhoneNumber(sessionEmail);
+ const {transactionID} = updatedTransaction;
+ const unmodifiedTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+
+ // Save optimistic updated transaction and action
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: {
+ ...updatedTransaction,
+ receipt: {
+ state: CONST.IOU.RECEIPT_STATE.OPEN,
+ },
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ lastModified: DateUtils.getDBTime(),
+ whisperedToAccountIDs: [],
+ },
+ },
+ },
+ ];
+
+ const successData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: {pendingAction: null},
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`,
+ value: null,
+ },
+ ];
+
+ const failureData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: {
+ ...unmodifiedTransaction,
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReportID}`,
+ value: {
+ [reportAction.reportActionID]: {
+ ...reportAction,
+ errors: ErrorUtils.getMicroSecondOnyxError('iou.error.genericCreateFailureMessage'),
+ },
+ },
+ },
+ ];
+
+ const splitParticipants = updatedTransaction.comment.splits;
+ const {modifiedAmount: amount, modifiedCurrency: currency} = updatedTransaction;
+
+ // Exclude the current user when calculating the split amount, `calculateAmount` takes it into account
+ const splitAmount = IOUUtils.calculateAmount(splitParticipants.length - 1, amount, currency, false);
+
+ const splits = [{email: currentUserEmailForIOUSplit}];
+ _.each(splitParticipants, (participant) => {
+ // Skip creating the transaction for the current user
+ if (participant.email === currentUserEmailForIOUSplit) {
+ return;
+ }
+ const isPolicyExpenseChat = !_.isEmpty(participant.policyID);
+
+ if (!isPolicyExpenseChat) {
+ // In case this is still the optimistic accountID saved in the splits array, return early as we cannot know
+ // if there is an existing chat between the split creator and this participant
+ // Instead, we will rely on Auth generating the report IDs and the user won't see any optimistic chats or reports created
+ const participantPersonalDetails = allPersonalDetails[participant.accountID] || {};
+ if (!participantPersonalDetails || participantPersonalDetails.isOptimisticPersonalDetail) {
+ splits.push({
+ email: participant.email,
+ });
+ return;
+ }
+ }
+
+ let oneOnOneChatReport;
+ let isNewOneOnOneChatReport = false;
+ if (isPolicyExpenseChat) {
+ // The workspace chat reportID is saved in the splits array when starting a split bill with a workspace
+ oneOnOneChatReport = allReports[`${ONYXKEYS.COLLECTION.REPORT}${participant.chatReportID}`];
+ } else {
+ const existingChatReport = ReportUtils.getChatByParticipants([participant.accountID]);
+ isNewOneOnOneChatReport = !existingChatReport;
+ oneOnOneChatReport = existingChatReport || ReportUtils.buildOptimisticChatReport([participant.accountID]);
+ }
+
+ let oneOnOneIOUReport = lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${oneOnOneChatReport.iouReportID}`, undefined);
+ const shouldCreateNewOneOnOneIOUReport =
+ _.isUndefined(oneOnOneIOUReport) || (isPolicyExpenseChat && ReportUtils.isControlPolicyExpenseReport(oneOnOneIOUReport) && ReportUtils.isReportApproved(oneOnOneIOUReport));
+
+ if (shouldCreateNewOneOnOneIOUReport) {
+ oneOnOneIOUReport = isPolicyExpenseChat
+ ? ReportUtils.buildOptimisticExpenseReport(oneOnOneChatReport.reportID, participant.policyID, sessionAccountID, splitAmount, currency)
+ : ReportUtils.buildOptimisticIOUReport(sessionAccountID, participant.accountID, splitAmount, oneOnOneChatReport.reportID, currency);
+ } else if (isPolicyExpenseChat) {
+ // Because of the Expense reports are stored as negative values, we subtract the total from the amount
+ oneOnOneIOUReport.total -= splitAmount;
+ } else {
+ oneOnOneIOUReport = IOUUtils.updateIOUOwnerAndTotal(oneOnOneIOUReport, sessionAccountID, splitAmount, currency);
+ }
+
+ const oneOnOneTransaction = TransactionUtils.buildOptimisticTransaction(
+ isPolicyExpenseChat ? -splitAmount : splitAmount,
+ currency,
+ oneOnOneIOUReport.reportID,
+ updatedTransaction.comment.comment,
+ updatedTransaction.modifiedCreated,
+ CONST.IOU.TYPE.SPLIT,
+ transactionID,
+ updatedTransaction.modifiedMerchant,
+ {...updatedTransaction.receipt, state: CONST.IOU.RECEIPT_STATE.OPEN},
+ updatedTransaction.filename,
+ );
+
+ const oneOnOneCreatedActionForChat = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit);
+ const oneOnOneCreatedActionForIOU = ReportUtils.buildOptimisticCreatedReportAction(currentUserEmailForIOUSplit);
+ const oneOnOneIOUAction = ReportUtils.buildOptimisticIOUReportAction(
+ CONST.IOU.REPORT_ACTION_TYPE.CREATE,
+ splitAmount,
+ currency,
+ updatedTransaction.comment.comment,
+ [participant],
+ oneOnOneTransaction.transactionID,
+ '',
+ oneOnOneIOUReport.reportID,
+ );
+
+ let oneOnOneReportPreviewAction = ReportActionsUtils.getReportPreviewAction(oneOnOneChatReport.reportID, oneOnOneIOUReport.reportID);
+ if (oneOnOneReportPreviewAction) {
+ oneOnOneReportPreviewAction = ReportUtils.updateReportPreview(oneOnOneIOUReport, oneOnOneReportPreviewAction);
+ } else {
+ oneOnOneReportPreviewAction = ReportUtils.buildOptimisticReportPreview(oneOnOneChatReport, oneOnOneIOUReport, '', oneOnOneTransaction);
+ }
+
+ const [oneOnOneOptimisticData, oneOnOneSuccessData, oneOnOneFailureData] = buildOnyxDataForMoneyRequest(
+ oneOnOneChatReport,
+ oneOnOneIOUReport,
+ oneOnOneTransaction,
+ oneOnOneCreatedActionForChat,
+ oneOnOneCreatedActionForIOU,
+ oneOnOneIOUAction,
+ {},
+ oneOnOneReportPreviewAction,
+ {},
+ {},
+ isNewOneOnOneChatReport,
+ shouldCreateNewOneOnOneIOUReport,
+ );
+
+ splits.push({
+ email: participant.email,
+ accountID: participant.accountID,
+ policyID: participant.policyID,
+ iouReportID: oneOnOneIOUReport.reportID,
+ chatReportID: oneOnOneChatReport.reportID,
+ transactionID: oneOnOneTransaction.transactionID,
+ reportActionID: oneOnOneIOUAction.reportActionID,
+ createdChatReportActionID: oneOnOneCreatedActionForChat.reportActionID,
+ createdIOUReportActionID: oneOnOneCreatedActionForIOU.reportActionID,
+ reportPreviewReportActionID: oneOnOneReportPreviewAction.reportActionID,
+ });
+
+ optimisticData.push(...oneOnOneOptimisticData);
+ successData.push(...oneOnOneSuccessData);
+ failureData.push(...oneOnOneFailureData);
+ });
+
+ const {
+ amount: transactionAmount,
+ currency: transactionCurrency,
+ created: transactionCreated,
+ merchant: transactionMerchant,
+ comment: transactionComment,
+ } = ReportUtils.getTransactionDetails(updatedTransaction);
+
+ API.write(
+ 'CompleteSplitBill',
+ {
+ transactionID,
+ amount: transactionAmount,
+ currency: transactionCurrency,
+ created: transactionCreated,
+ merchant: transactionMerchant,
+ comment: transactionComment,
+ splits: JSON.stringify(splits),
+ },
+ {optimisticData, successData, failureData},
+ );
+ Navigation.dismissModal(chatReportID);
+ Report.notifyNewAction(chatReportID, sessionAccountID);
+}
+
+/**
+ * @param {String} transactionID
+ * @param {Object} transactionChanges
+ */
+function setDraftSplitTransaction(transactionID, transactionChanges = {}) {
+ let draftSplitTransaction = allDraftSplitTransactions[`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`];
+
+ if (!draftSplitTransaction) {
+ draftSplitTransaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`];
+ }
+
+ const updatedTransaction = TransactionUtils.getUpdatedTransaction(draftSplitTransaction, transactionChanges, false, false);
+
+ Onyx.merge(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`, updatedTransaction);
+}
+
/**
* @param {String} transactionID
* @param {Number} transactionThreadReportID
@@ -1305,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),
});
@@ -1522,10 +2006,11 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView
}
updatedIOUReport.lastMessageText = iouReportLastMessageText;
- updatedIOUReport.lastVisibleActionCreated = lastVisibleAction.created;
+ updatedIOUReport.lastVisibleActionCreated = lodashGet(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),
});
@@ -1583,7 +2068,7 @@ function deleteMoneyRequest(transactionID, reportAction, isSingleTransactionView
hasOutstandingIOU: false,
iouReportID: null,
lastMessageText: ReportActionsUtils.getLastVisibleMessage(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}).lastMessageText,
- lastVisibleActionCreated: ReportActionsUtils.getLastVisibleAction(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}).created,
+ lastVisibleActionCreated: lodashGet(ReportActionsUtils.getLastVisibleAction(iouReport.chatReportID, {[reportPreviewAction.reportActionID]: null}), 'created'),
},
},
]
@@ -1706,7 +2191,7 @@ function getSendMoneyParams(report, amount, currency, comment, paymentMethodType
}
const optimisticIOUReport = ReportUtils.buildOptimisticIOUReport(recipientAccountID, managerID, amount, chatReport.reportID, currency, true);
- const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount * 100, currency, optimisticIOUReport.reportID, comment);
+ const optimisticTransaction = TransactionUtils.buildOptimisticTransaction(amount, currency, optimisticIOUReport.reportID, comment);
const optimisticTransactionData = {
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.TRANSACTION}${optimisticTransaction.transactionID}`,
@@ -2162,7 +2647,7 @@ function submitReport(expenseReport) {
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`,
value: {
- state: CONST.REPORT.STATE.OPEN,
+ statusNum: CONST.REPORT.STATUS.OPEN,
stateNum: CONST.REPORT.STATE_NUM.OPEN,
},
},
@@ -2201,6 +2686,29 @@ function payMoneyRequest(paymentType, chatReport, iouReport) {
Navigation.dismissModal(chatReport.reportID);
}
+function detachReceipt(transactionID) {
+ const transaction = allTransactions[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] || {};
+ const newTransaction = {...transaction, filename: '', receipt: {}};
+
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: newTransaction,
+ },
+ ];
+
+ const failureData = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`,
+ value: transaction,
+ },
+ ];
+
+ API.write('DetachReceipt', {transactionID}, {optimisticData, failureData});
+}
+
/**
* @param {String} transactionID
* @param {Object} receipt
@@ -2328,15 +2836,18 @@ function setMoneyRequestParticipants(participants) {
/**
* @param {String} receiptPath
- * @param {String} receiptSource
+ * @param {String} receiptFilename
*/
-function setMoneyRequestReceipt(receiptPath, receiptSource) {
- Onyx.merge(ONYXKEYS.IOU, {receiptPath, receiptSource, merchant: ''});
+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});
}
@@ -2407,6 +2918,9 @@ export {
deleteMoneyRequest,
splitBill,
splitBillAndOpenReport,
+ setDraftSplitTransaction,
+ startSplitBill,
+ completeSplitBill,
requestMoney,
sendMoneyElsewhere,
approveMoneyRequest,
@@ -2428,9 +2942,10 @@ export {
setMoneyRequestBillable,
setMoneyRequestParticipants,
setMoneyRequestReceipt,
- createEmptyTransaction,
+ setUpDistanceTransaction,
navigateToNextPage,
updateDistanceRequest,
replaceReceipt,
+ detachReceipt,
getIOUReportID,
};
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/KeyboardShortcuts.js b/src/libs/actions/KeyboardShortcuts.js
deleted file mode 100644
index d66e362890b2..000000000000
--- a/src/libs/actions/KeyboardShortcuts.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import Onyx from 'react-native-onyx';
-import ONYXKEYS from '../../ONYXKEYS';
-
-let isShortcutsModalOpen;
-Onyx.connect({
- key: ONYXKEYS.IS_SHORTCUTS_MODAL_OPEN,
- callback: (flag) => (isShortcutsModalOpen = flag),
- initWithStoredValues: false,
-});
-
-/**
- * Set keyboard shortcuts flag to show modal
- */
-function showKeyboardShortcutModal() {
- if (isShortcutsModalOpen) {
- return;
- }
- Onyx.set(ONYXKEYS.IS_SHORTCUTS_MODAL_OPEN, true);
-}
-
-/**
- * Unset keyboard shortcuts flag to hide modal
- */
-function hideKeyboardShortcutModal() {
- if (!isShortcutsModalOpen) {
- return;
- }
- Onyx.set(ONYXKEYS.IS_SHORTCUTS_MODAL_OPEN, false);
-}
-
-export {showKeyboardShortcutModal, hideKeyboardShortcutModal};
diff --git a/src/libs/actions/PaymentMethods.js b/src/libs/actions/PaymentMethods.js
deleted file mode 100644
index 0ed6f8b036bb..000000000000
--- a/src/libs/actions/PaymentMethods.js
+++ /dev/null
@@ -1,356 +0,0 @@
-import _ from 'underscore';
-import {createRef} from 'react';
-import Onyx from 'react-native-onyx';
-import ONYXKEYS from '../../ONYXKEYS';
-import * as API from '../API';
-import CONST from '../../CONST';
-import Navigation from '../Navigation/Navigation';
-import * as CardUtils from '../CardUtils';
-import ROUTES from '../../ROUTES';
-
-/**
- * Sets up a ref to an instance of the KYC Wall component.
- */
-const kycWallRef = createRef();
-
-/**
- * When we successfully add a payment method or pass the KYC checks we will continue with our setup action if we have one set.
- */
-function continueSetup() {
- if (!kycWallRef.current || !kycWallRef.current.continue) {
- Navigation.goBack(ROUTES.HOME);
- return;
- }
-
- // Close the screen (Add Debit Card, Add Bank Account, or Enable Payments) on success and continue with setup
- Navigation.goBack(ROUTES.HOME);
- kycWallRef.current.continue();
-}
-
-function openWalletPage() {
- const onyxData = {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS,
- value: true,
- },
- ],
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS,
- value: false,
- },
- ],
- failureData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS,
- value: false,
- },
- ],
- };
-
- return API.read('OpenPaymentsPage', {}, onyxData);
-}
-
-/**
- *
- * @param {Number} bankAccountID
- * @param {Number} fundID
- * @param {Object} previousPaymentMethod
- * @param {Object} currentPaymentMethod
- * @param {Boolean} isOptimisticData
- * @return {Array}
- *
- */
-function getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, isOptimisticData = true) {
- const onyxData = [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.USER_WALLET,
- value: {
- walletLinkedAccountID: bankAccountID || fundID,
- walletLinkedAccountType: bankAccountID ? CONST.PAYMENT_METHODS.BANK_ACCOUNT : CONST.PAYMENT_METHODS.DEBIT_CARD,
- },
- },
- ];
-
- // Only clear the error if this is optimistic data. If this is failure data, we do not want to clear the error that came from the server.
- if (isOptimisticData) {
- onyxData[0].value.errors = null;
- }
-
- if (previousPaymentMethod) {
- onyxData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: previousPaymentMethod.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.FUND_LIST,
- value: {
- [previousPaymentMethod.methodID]: {
- isDefault: !isOptimisticData,
- },
- },
- });
- }
-
- if (currentPaymentMethod) {
- onyxData.push({
- onyxMethod: Onyx.METHOD.MERGE,
- key: currentPaymentMethod.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.FUND_LIST,
- value: {
- [currentPaymentMethod.methodID]: {
- isDefault: isOptimisticData,
- },
- },
- });
- }
-
- return onyxData;
-}
-
-/**
- * Sets the default bank account or debit card for an Expensify Wallet
- *
- * @param {Number} bankAccountID
- * @param {Number} fundID
- * @param {Object} previousPaymentMethod
- * @param {Object} currentPaymentMethod
- *
- */
-function makeDefaultPaymentMethod(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod) {
- API.write(
- 'MakeDefaultPaymentMethod',
- {
- bankAccountID,
- fundID,
- },
- {
- optimisticData: getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, true, ONYXKEYS.FUND_LIST),
- failureData: getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, false, ONYXKEYS.FUND_LIST),
- },
- );
-}
-
-/**
- * Calls the API to add a new card.
- *
- * @param {Object} params
- */
-function addPaymentCard(params) {
- const cardMonth = CardUtils.getMonthFromExpirationDateString(params.expirationDate);
- const cardYear = CardUtils.getYearFromExpirationDateString(params.expirationDate);
-
- API.write(
- 'AddPaymentCard',
- {
- cardNumber: params.cardNumber,
- cardYear,
- cardMonth,
- cardCVV: params.securityCode,
- addressName: params.nameOnCard,
- addressZip: params.addressZipCode,
- currency: CONST.CURRENCY.USD,
- isP2PDebitCard: true,
- },
- {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
- value: {isLoading: true},
- },
- ],
- successData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
- value: {isLoading: false},
- },
- ],
- failureData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
- value: {isLoading: false},
- },
- ],
- },
- );
-}
-
-/**
- * Resets the values for the add debit card form back to their initial states
- */
-function clearDebitCardFormErrorAndSubmit() {
- Onyx.set(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, {
- isLoading: false,
- errors: null,
- });
-}
-
-/**
- * Call the API to transfer wallet balance.
- * @param {Object} paymentMethod
- * @param {*} paymentMethod.methodID
- * @param {String} paymentMethod.accountType
- */
-function transferWalletBalance(paymentMethod) {
- const paymentMethodIDKey = paymentMethod.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? CONST.PAYMENT_METHOD_ID_KEYS.BANK_ACCOUNT : CONST.PAYMENT_METHOD_ID_KEYS.DEBIT_CARD;
- const parameters = {
- [paymentMethodIDKey]: paymentMethod.methodID,
- };
-
- API.write('TransferWalletBalance', parameters, {
- optimisticData: [
- {
- onyxMethod: 'merge',
- key: ONYXKEYS.WALLET_TRANSFER,
- value: {
- loading: true,
- errors: null,
- },
- },
- ],
- successData: [
- {
- onyxMethod: 'merge',
- key: ONYXKEYS.WALLET_TRANSFER,
- value: {
- loading: false,
- shouldShowSuccess: true,
- paymentMethodType: paymentMethod.accountType,
- },
- },
- ],
- failureData: [
- {
- onyxMethod: 'merge',
- key: ONYXKEYS.WALLET_TRANSFER,
- value: {
- loading: false,
- shouldShowSuccess: false,
- },
- },
- ],
- });
-}
-
-function resetWalletTransferData() {
- Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {
- selectedAccountType: '',
- selectedAccountID: null,
- filterPaymentMethodType: null,
- loading: false,
- shouldShowSuccess: false,
- });
-}
-
-/**
- * @param {String} selectedAccountType
- * @param {String} selectedAccountID
- */
-function saveWalletTransferAccountTypeAndID(selectedAccountType, selectedAccountID) {
- Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {selectedAccountType, selectedAccountID});
-}
-
-/**
- * Toggles the user's selected type of payment method (bank account or debit card) on the wallet transfer balance screen.
- * @param {String} filterPaymentMethodType
- */
-function saveWalletTransferMethodType(filterPaymentMethodType) {
- Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {filterPaymentMethodType});
-}
-
-function dismissSuccessfulTransferBalancePage() {
- Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {shouldShowSuccess: false});
- Navigation.goBack(ROUTES.SETTINGS_WALLET);
-}
-
-/**
- * Looks through each payment method to see if there is an existing error
- * @param {Object} bankList
- * @param {Object} fundList
- * @returns {Boolean}
- */
-function hasPaymentMethodError(bankList, fundList) {
- const combinedPaymentMethods = {...bankList, ...fundList};
- return _.some(combinedPaymentMethods, (item) => !_.isEmpty(item.errors));
-}
-
-/**
- * Clears the error for the specified payment item
- * @param {String} paymentListKey The onyx key for the provided payment method
- * @param {String} paymentMethodID
- */
-function clearDeletePaymentMethodError(paymentListKey, paymentMethodID) {
- Onyx.merge(paymentListKey, {
- [paymentMethodID]: {
- pendingAction: null,
- errors: null,
- },
- });
-}
-
-/**
- * If there was a failure adding a payment method, clearing it removes the payment method from the list entirely
- * @param {String} paymentListKey The onyx key for the provided payment method
- * @param {String} paymentMethodID
- */
-function clearAddPaymentMethodError(paymentListKey, paymentMethodID) {
- Onyx.merge(paymentListKey, {
- [paymentMethodID]: null,
- });
-}
-
-/**
- * Clear any error(s) related to the user's wallet
- */
-function clearWalletError() {
- Onyx.merge(ONYXKEYS.USER_WALLET, {errors: null});
-}
-
-/**
- * Clear any error(s) related to the user's wallet terms
- */
-function clearWalletTermsError() {
- Onyx.merge(ONYXKEYS.WALLET_TERMS, {errors: null});
-}
-
-function deletePaymentCard(fundID) {
- API.write(
- 'DeletePaymentCard',
- {
- fundID,
- },
- {
- optimisticData: [
- {
- onyxMethod: Onyx.METHOD.MERGE,
- key: `${ONYXKEYS.FUND_LIST}`,
- value: {[fundID]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}},
- },
- ],
- },
- );
-}
-
-export {
- deletePaymentCard,
- addPaymentCard,
- openWalletPage,
- makeDefaultPaymentMethod,
- kycWallRef,
- continueSetup,
- clearDebitCardFormErrorAndSubmit,
- dismissSuccessfulTransferBalancePage,
- transferWalletBalance,
- resetWalletTransferData,
- saveWalletTransferAccountTypeAndID,
- saveWalletTransferMethodType,
- hasPaymentMethodError,
- clearDeletePaymentMethodError,
- clearAddPaymentMethodError,
- clearWalletError,
- clearWalletTermsError,
-};
diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts
new file mode 100644
index 000000000000..fe1b5ebe10e9
--- /dev/null
+++ b/src/libs/actions/PaymentMethods.ts
@@ -0,0 +1,393 @@
+import {createRef} from 'react';
+import Onyx, {OnyxUpdate} from 'react-native-onyx';
+import {ValueOf} from 'type-fest';
+import ONYXKEYS, {OnyxValues} from '../../ONYXKEYS';
+import * as API from '../API';
+import CONST from '../../CONST';
+import Navigation from '../Navigation/Navigation';
+import * as CardUtils from '../CardUtils';
+import ROUTES from '../../ROUTES';
+import {FilterMethodPaymentType} from '../../types/onyx/WalletTransfer';
+import PaymentMethod from '../../types/onyx/PaymentMethod';
+
+type KYCWallRef = {
+ continue?: () => void;
+};
+
+/**
+ * Sets up a ref to an instance of the KYC Wall component.
+ */
+const kycWallRef = createRef();
+
+/**
+ * When we successfully add a payment method or pass the KYC checks we will continue with our setup action if we have one set.
+ */
+function continueSetup(fallbackRoute = ROUTES.HOME) {
+ if (!kycWallRef.current?.continue) {
+ Navigation.goBack(fallbackRoute);
+ return;
+ }
+
+ // Close the screen (Add Debit Card, Add Bank Account, or Enable Payments) on success and continue with setup
+ Navigation.goBack(fallbackRoute);
+ kycWallRef.current.continue();
+}
+
+function openWalletPage() {
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS,
+ value: true,
+ },
+ ];
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS,
+ value: false,
+ },
+ ];
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IS_LOADING_PAYMENT_METHODS,
+ value: false,
+ },
+ ];
+
+ return API.read(
+ 'OpenPaymentsPage',
+ {},
+ {
+ optimisticData,
+ successData,
+ failureData,
+ },
+ );
+}
+
+function getMakeDefaultPaymentOnyxData(
+ bankAccountID: number,
+ fundID: number,
+ previousPaymentMethod: PaymentMethod,
+ currentPaymentMethod: PaymentMethod,
+ isOptimisticData = true,
+): OnyxUpdate[] {
+ const onyxData: OnyxUpdate[] = [
+ isOptimisticData
+ ? {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.USER_WALLET,
+ value: {
+ walletLinkedAccountID: bankAccountID || fundID,
+ walletLinkedAccountType: bankAccountID ? CONST.PAYMENT_METHODS.BANK_ACCOUNT : CONST.PAYMENT_METHODS.DEBIT_CARD,
+ // Only clear the error if this is optimistic data. If this is failure data, we do not want to clear the error that came from the server.
+ errors: null,
+ },
+ }
+ : {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.USER_WALLET,
+ value: {
+ walletLinkedAccountID: bankAccountID || fundID,
+ walletLinkedAccountType: bankAccountID ? CONST.PAYMENT_METHODS.BANK_ACCOUNT : CONST.PAYMENT_METHODS.DEBIT_CARD,
+ },
+ },
+ ];
+
+ if (previousPaymentMethod?.methodID) {
+ onyxData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: previousPaymentMethod.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.FUND_LIST,
+ value: {
+ [previousPaymentMethod.methodID]: {
+ isDefault: !isOptimisticData,
+ },
+ },
+ });
+ }
+
+ if (currentPaymentMethod?.methodID) {
+ onyxData.push({
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: currentPaymentMethod.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? ONYXKEYS.BANK_ACCOUNT_LIST : ONYXKEYS.FUND_LIST,
+ value: {
+ [currentPaymentMethod.methodID]: {
+ isDefault: isOptimisticData,
+ },
+ },
+ });
+ }
+
+ return onyxData;
+}
+
+/**
+ * Sets the default bank account or debit card for an Expensify Wallet
+ *
+ */
+function makeDefaultPaymentMethod(bankAccountID: number, fundID: number, previousPaymentMethod: PaymentMethod, currentPaymentMethod: PaymentMethod) {
+ type MakeDefaultPaymentMethodParams = {
+ bankAccountID: number;
+ fundID: number;
+ };
+
+ const parameters: MakeDefaultPaymentMethodParams = {
+ bankAccountID,
+ fundID,
+ };
+
+ API.write('MakeDefaultPaymentMethod', parameters, {
+ optimisticData: getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, true),
+ failureData: getMakeDefaultPaymentOnyxData(bankAccountID, fundID, previousPaymentMethod, currentPaymentMethod, false),
+ });
+}
+
+type PaymentCardParams = {expirationDate: string; cardNumber: string; securityCode: string; nameOnCard: string; addressZipCode: string};
+
+/**
+ * Calls the API to add a new card.
+ *
+ */
+function addPaymentCard(params: PaymentCardParams) {
+ const cardMonth = CardUtils.getMonthFromExpirationDateString(params.expirationDate);
+ const cardYear = CardUtils.getYearFromExpirationDateString(params.expirationDate);
+
+ type AddPaymentCardParams = {
+ cardNumber: string;
+ cardYear: string;
+ cardMonth: string;
+ cardCVV: string;
+ addressName: string;
+ addressZip: string;
+ currency: ValueOf;
+ isP2PDebitCard: boolean;
+ };
+
+ const parameters: AddPaymentCardParams = {
+ cardNumber: params.cardNumber,
+ cardYear,
+ cardMonth,
+ cardCVV: params.securityCode,
+ addressName: params.nameOnCard,
+ addressZip: params.addressZipCode,
+ currency: CONST.CURRENCY.USD,
+ isP2PDebitCard: true,
+ };
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
+ value: {isLoading: true},
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
+ value: {isLoading: false},
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM,
+ value: {isLoading: false},
+ },
+ ];
+
+ API.write('AddPaymentCard', parameters, {
+ optimisticData,
+ successData,
+ failureData,
+ });
+}
+
+/**
+ * Resets the values for the add debit card form back to their initial states
+ */
+function clearDebitCardFormErrorAndSubmit() {
+ Onyx.set(ONYXKEYS.FORMS.ADD_DEBIT_CARD_FORM, {
+ isLoading: false,
+ errors: undefined,
+ setupComplete: true,
+ });
+}
+
+/**
+ * Call the API to transfer wallet balance.
+ *
+ */
+function transferWalletBalance(paymentMethod: PaymentMethod) {
+ const paymentMethodIDKey = paymentMethod.accountType === CONST.PAYMENT_METHODS.BANK_ACCOUNT ? CONST.PAYMENT_METHOD_ID_KEYS.BANK_ACCOUNT : CONST.PAYMENT_METHOD_ID_KEYS.DEBIT_CARD;
+
+ type TransferWalletBalanceParameters = Partial, number | undefined>>;
+
+ const parameters: TransferWalletBalanceParameters = {
+ [paymentMethodIDKey]: paymentMethod.methodID,
+ };
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: 'merge',
+ key: ONYXKEYS.WALLET_TRANSFER,
+ value: {
+ loading: true,
+ errors: null,
+ },
+ },
+ ];
+
+ const successData: OnyxUpdate[] = [
+ {
+ onyxMethod: 'merge',
+ key: ONYXKEYS.WALLET_TRANSFER,
+ value: {
+ loading: false,
+ shouldShowSuccess: true,
+ paymentMethodType: paymentMethod.accountType,
+ },
+ },
+ ];
+
+ const failureData: OnyxUpdate[] = [
+ {
+ onyxMethod: 'merge',
+ key: ONYXKEYS.WALLET_TRANSFER,
+ value: {
+ loading: false,
+ shouldShowSuccess: false,
+ },
+ },
+ ];
+
+ API.write('TransferWalletBalance', parameters, {
+ optimisticData,
+ successData,
+ failureData,
+ });
+}
+
+function resetWalletTransferData() {
+ Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {
+ selectedAccountType: '',
+ selectedAccountID: null,
+ filterPaymentMethodType: null,
+ loading: false,
+ shouldShowSuccess: false,
+ });
+}
+
+function saveWalletTransferAccountTypeAndID(selectedAccountType: string, selectedAccountID: string) {
+ Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {selectedAccountType, selectedAccountID});
+}
+
+/**
+ * Toggles the user's selected type of payment method (bank account or debit card) on the wallet transfer balance screen.
+ *
+ */
+function saveWalletTransferMethodType(filterPaymentMethodType?: FilterMethodPaymentType) {
+ Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {filterPaymentMethodType});
+}
+
+function dismissSuccessfulTransferBalancePage() {
+ Onyx.merge(ONYXKEYS.WALLET_TRANSFER, {shouldShowSuccess: false});
+ Navigation.goBack(ROUTES.SETTINGS_WALLET);
+}
+
+/**
+ * Looks through each payment method to see if there is an existing error
+ *
+ */
+function hasPaymentMethodError(bankList: OnyxValues[typeof ONYXKEYS.BANK_ACCOUNT_LIST], fundList: OnyxValues[typeof ONYXKEYS.FUND_LIST]): boolean {
+ const combinedPaymentMethods = {...bankList, ...fundList};
+
+ return Object.values(combinedPaymentMethods).some((item) => Object.keys(item.errors ?? {}).length);
+}
+
+type PaymentListKey = typeof ONYXKEYS.BANK_ACCOUNT_LIST | typeof ONYXKEYS.FUND_LIST;
+
+/**
+ * Clears the error for the specified payment item
+ * @param paymentListKey The onyx key for the provided payment method
+ * @param paymentMethodID
+ */
+function clearDeletePaymentMethodError(paymentListKey: PaymentListKey, paymentMethodID: string) {
+ Onyx.merge(paymentListKey, {
+ [paymentMethodID]: {
+ pendingAction: null,
+ errors: null,
+ },
+ });
+}
+
+/**
+ * If there was a failure adding a payment method, clearing it removes the payment method from the list entirely
+ * @param paymentListKey The onyx key for the provided payment method
+ * @param paymentMethodID
+ */
+function clearAddPaymentMethodError(paymentListKey: PaymentListKey, paymentMethodID: string) {
+ Onyx.merge(paymentListKey, {
+ [paymentMethodID]: null,
+ });
+}
+
+/**
+ * Clear any error(s) related to the user's wallet
+ */
+function clearWalletError() {
+ Onyx.merge(ONYXKEYS.USER_WALLET, {errors: null});
+}
+
+/**
+ * Clear any error(s) related to the user's wallet terms
+ */
+function clearWalletTermsError() {
+ Onyx.merge(ONYXKEYS.WALLET_TERMS, {errors: null});
+}
+
+function deletePaymentCard(fundID: number) {
+ type DeletePaymentCardParams = {
+ fundID: number;
+ };
+
+ const parameters: DeletePaymentCardParams = {
+ fundID,
+ };
+
+ const optimisticData: OnyxUpdate[] = [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: `${ONYXKEYS.FUND_LIST}`,
+ value: {[fundID]: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}},
+ },
+ ];
+
+ API.write('DeletePaymentCard', parameters, {
+ optimisticData,
+ });
+}
+
+export {
+ deletePaymentCard,
+ addPaymentCard,
+ openWalletPage,
+ makeDefaultPaymentMethod,
+ kycWallRef,
+ continueSetup,
+ clearDebitCardFormErrorAndSubmit,
+ dismissSuccessfulTransferBalancePage,
+ transferWalletBalance,
+ resetWalletTransferData,
+ saveWalletTransferAccountTypeAndID,
+ saveWalletTransferMethodType,
+ hasPaymentMethodError,
+ clearDeletePaymentMethodError,
+ clearAddPaymentMethodError,
+ clearWalletError,
+ clearWalletTermsError,
+};
diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js
index 1a73b148e100..89324dd35485 100644
--- a/src/libs/actions/Policy.js
+++ b/src/libs/actions/Policy.js
@@ -73,6 +73,13 @@ Onyx.connect({
callback: (val) => (allRecentlyUsedCategories = val),
});
+let networkStatus = {};
+Onyx.connect({
+ key: ONYXKEYS.NETWORK,
+ waitForCollectionCallback: true,
+ callback: (val) => (networkStatus = val),
+});
+
/**
* Stores in Onyx the policy ID of the last workspace that was accessed by the user
* @param {String|null} policyID
@@ -766,7 +773,7 @@ function updateWorkspaceCustomUnitAndRate(policyID, currentCustomUnit, newCustom
'UpdateWorkspaceCustomUnitAndRate',
{
policyID,
- lastModified,
+ ...(!networkStatus.isOffline && {lastModified}),
customUnit: JSON.stringify(newCustomUnitParam),
customUnitRate: JSON.stringify(newCustomUnitParam.rates),
},
@@ -909,6 +916,48 @@ function buildOptimisticCustomUnits() {
};
}
+/**
+ * Optimistically creates a Policy Draft for a new workspace
+ *
+ * @param {String} [policyOwnerEmail] Optional, the email of the account to make the owner of the policy
+ * @param {String} [policyName] Optional, custom policy name we will use for created workspace
+ * @param {String} [policyID] Optional, custom policy id we will use for created workspace
+ */
+function createDraftInitialWorkspace(policyOwnerEmail = '', policyName = '', policyID = generatePolicyID()) {
+ const workspaceName = policyName || generateDefaultWorkspaceName(policyOwnerEmail);
+ const {customUnits} = buildOptimisticCustomUnits();
+
+ const optimisticData = [
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`,
+ value: {
+ id: policyID,
+ type: CONST.POLICY.TYPE.FREE,
+ name: workspaceName,
+ role: CONST.POLICY.ROLE.ADMIN,
+ owner: sessionEmail,
+ isPolicyExpenseChatEnabled: true,
+ outputCurrency: lodashGet(allPersonalDetails, [sessionAccountID, 'localCurrencyCode'], CONST.CURRENCY.USD),
+ pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
+ customUnits,
+ },
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${policyID}`,
+ value: {
+ [sessionAccountID]: {
+ role: CONST.POLICY.ROLE.ADMIN,
+ errors: {},
+ },
+ },
+ },
+ ];
+
+ Onyx.update(optimisticData);
+}
+
/**
* Optimistically creates a new workspace and default workspace chats
*
@@ -1027,6 +1076,16 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${expenseChatReportID}`,
value: expenseReportActionData,
},
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.POLICY_DRAFTS}${policyID}`,
+ value: null,
+ },
+ {
+ onyxMethod: Onyx.METHOD.SET,
+ key: `${ONYXKEYS.COLLECTION.POLICY_MEMBERS_DRAFTS}${policyID}`,
+ value: null,
+ },
],
successData: [
{
@@ -1131,6 +1190,7 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName
],
},
);
+
return adminsChatReportID;
}
@@ -1259,4 +1319,5 @@ export {
clearErrors,
openDraftWorkspaceRequest,
buildOptimisticPolicyRecentlyUsedCategories,
+ createDraftInitialWorkspace,
};
diff --git a/src/libs/actions/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/ReimbursementAccount/resetFreePlanBankAccount.js b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js
index edb169fc96aa..388010e99569 100644
--- a/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js
+++ b/src/libs/actions/ReimbursementAccount/resetFreePlanBankAccount.js
@@ -1,20 +1,20 @@
import Onyx from 'react-native-onyx';
import CONST from '../../../CONST';
import ONYXKEYS from '../../../ONYXKEYS';
-import * as store from './store';
import * as API from '../../API';
import * as PlaidDataProps from '../../../pages/ReimbursementAccount/plaidDataPropTypes';
import * as ReimbursementAccountProps from '../../../pages/ReimbursementAccount/reimbursementAccountPropTypes';
/**
* Reset user's reimbursement account. This will delete the bank account.
- * @param {number} bankAccountID
+ * @param {Number} bankAccountID
+ * @param {Object} session
*/
-function resetFreePlanBankAccount(bankAccountID) {
+function resetFreePlanBankAccount(bankAccountID, session) {
if (!bankAccountID) {
throw new Error('Missing bankAccountID when attempting to reset free plan bank account');
}
- if (!store.getCredentials() || !store.getCredentials().login) {
+ if (!session.email) {
throw new Error('Missing credentials when attempting to reset free plan bank account');
}
@@ -22,7 +22,7 @@ function resetFreePlanBankAccount(bankAccountID) {
'RestartBankAccountSetup',
{
bankAccountID,
- ownerEmail: store.getCredentials().login,
+ ownerEmail: session.email,
},
{
optimisticData: [
diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js
index ab2ac7fb0ca2..5ca0a4655730 100644
--- a/src/libs/actions/Report.js
+++ b/src/libs/actions/Report.js
@@ -1,6 +1,7 @@
import {InteractionManager} from 'react-native';
import _ from 'underscore';
import lodashGet from 'lodash/get';
+import lodashDebounce from 'lodash/debounce';
import ExpensiMark from 'expensify-common/lib/ExpensiMark';
import Onyx from 'react-native-onyx';
import Str from 'expensify-common/lib/str';
@@ -87,6 +88,19 @@ Onyx.connect({
},
});
+const draftNoteMap = {};
+Onyx.connect({
+ key: ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT,
+ callback: (value, key) => {
+ if (!key) {
+ return;
+ }
+
+ const reportID = key.replace(ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT, '');
+ draftNoteMap[reportID] = value;
+ },
+});
+
const allReports = {};
let conciergeChatReportID;
const typingWatchTimers = {};
@@ -361,8 +375,8 @@ function addActions(reportID, text = '', file) {
const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportActionsUtils.getLastVisibleMessage(reportID);
if (lastMessageText || lastMessageTranslationKey) {
const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(reportID);
- const lastVisibleActionCreated = lastVisibleAction.created;
- const lastActorAccountID = lastVisibleAction.actorAccountID;
+ const lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created');
+ const lastActorAccountID = lodashGet(lastVisibleAction, 'actorAccountID');
failureReport = {
lastMessageTranslationKey,
lastMessageText,
@@ -1040,11 +1054,11 @@ function deleteReportComment(reportID, reportAction) {
isLastMessageDeletedParentAction: true,
};
} else {
- const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportActionsUtils.getLastVisibleMessage(originalReportID, optimisticReportActions);
+ const {lastMessageText = '', lastMessageTranslationKey = ''} = ReportUtils.getLastVisibleMessage(originalReportID, optimisticReportActions);
if (lastMessageText || lastMessageTranslationKey) {
const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions);
- const lastVisibleActionCreated = lastVisibleAction.created;
- const lastActorAccountID = lastVisibleAction.actorAccountID;
+ const lastVisibleActionCreated = lodashGet(lastVisibleAction, 'created');
+ const lastActorAccountID = lodashGet(lastVisibleAction, 'actorAccountID');
optimisticReport = {
lastMessageTranslationKey,
lastMessageText,
@@ -1148,17 +1162,16 @@ const removeLinksFromHtml = (html, links) => {
* This function will handle removing only links that were purposely removed by the user while editing.
*
* @param {String} newCommentText text of the comment after editing.
- * @param {String} originalHtml original html of the comment before editing.
+ * @param {String} originalCommentMarkdown original markdown of the comment before editing.
* @returns {String}
*/
-const handleUserDeletedLinksInHtml = (newCommentText, originalHtml) => {
+const handleUserDeletedLinksInHtml = (newCommentText, originalCommentMarkdown) => {
const parser = new ExpensiMark();
if (newCommentText.length > CONST.MAX_MARKUP_LENGTH) {
return newCommentText;
}
- const markdownOriginalComment = parser.htmlToMarkdown(originalHtml).trim();
const htmlForNewComment = parser.replace(newCommentText);
- const removedLinks = parser.getRemovedMarkdownLinks(markdownOriginalComment, newCommentText);
+ const removedLinks = parser.getRemovedMarkdownLinks(originalCommentMarkdown, newCommentText);
return removeLinksFromHtml(htmlForNewComment, removedLinks);
};
@@ -1177,7 +1190,14 @@ function editReportComment(reportID, originalReportAction, textForNewComment) {
// https://github.com/Expensify/App/issues/9090
// https://github.com/Expensify/App/issues/13221
const originalCommentHTML = lodashGet(originalReportAction, 'message[0].html');
- const htmlForNewComment = handleUserDeletedLinksInHtml(textForNewComment, originalCommentHTML);
+ const originalCommentMarkdown = parser.htmlToMarkdown(originalCommentHTML).trim();
+
+ // Skip the Edit if draft is not changed
+ if (originalCommentMarkdown === textForNewComment) {
+ return;
+ }
+
+ const htmlForNewComment = handleUserDeletedLinksInHtml(textForNewComment, originalCommentMarkdown);
const reportComment = parser.htmlToText(htmlForNewComment);
// For comments shorter than or equal to 10k chars, convert the comment from MD into HTML because that's how it is stored in the database
@@ -1185,7 +1205,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) {
let parsedOriginalCommentHTML = originalCommentHTML;
if (textForNewComment.length <= CONST.MAX_MARKUP_LENGTH) {
const autolinkFilter = {filterRules: _.filter(_.pluck(parser.rules, 'name'), (name) => name !== 'autolink')};
- parsedOriginalCommentHTML = parser.replace(parser.htmlToMarkdown(originalCommentHTML).trim(), autolinkFilter);
+ parsedOriginalCommentHTML = parser.replace(originalCommentMarkdown, autolinkFilter);
}
// Delete the comment if it's empty
@@ -1225,7 +1245,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) {
];
const lastVisibleAction = ReportActionsUtils.getLastVisibleAction(originalReportID, optimisticReportActions);
- if (reportActionID === lastVisibleAction.reportActionID) {
+ if (reportActionID === lodashGet(lastVisibleAction, 'reportActionID')) {
const lastMessageText = ReportUtils.formatReportLastMessageText(reportComment);
const optimisticReport = {
lastMessageTranslationKey: '',
@@ -1388,9 +1408,15 @@ function updateWriteCapabilityAndNavigate(report, newValue) {
/**
* Navigates to the 1:1 report with Concierge
+ *
+ * @param {Boolean} ignoreConciergeReportID - Flag to ignore conciergeChatReportID during navigation. The default behavior is to not ignore.
*/
-function navigateToConciergeChat() {
- if (!conciergeChatReportID) {
+function navigateToConciergeChat(ignoreConciergeReportID = false) {
+ // If conciergeChatReportID contains a concierge report ID, we navigate to the concierge chat using the stored report ID.
+ // Otherwise, we would find the concierge chat and navigate to it.
+ // Now, when user performs sign-out and a sign-in again, conciergeChatReportID may contain a stale value.
+ // In order to prevent navigation to a stale value, we use ignoreConciergeReportID to forcefully find and navigate to concierge chat.
+ if (!conciergeChatReportID || ignoreConciergeReportID) {
// In order to avoid creating concierge repeatedly,
// we need to ensure that the server data has been successfully pulled
Welcome.serverDataIsReadyPromise().then(() => {
@@ -1410,8 +1436,9 @@ function navigateToConciergeChat() {
* @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]);
@@ -1428,6 +1455,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);
@@ -1492,6 +1522,7 @@ function addPolicyReport(policyID, reportName, visibility, policyMembersAccountI
reportID: policyReport.reportID,
createdReportActionID: createdReportAction.reportActionID,
writeCapability,
+ welcomeMessage,
},
{optimisticData, successData, failureData},
);
@@ -1884,7 +1915,7 @@ function openReportFromDeepLink(url, isAuthenticated) {
InteractionManager.runAfterInteractions(() => {
Session.waitForUserSignIn().then(() => {
if (route === ROUTES.CONCIERGE) {
- navigateToConciergeChat();
+ navigateToConciergeChat(true);
return;
}
Navigation.navigate(route, CONST.NAVIGATION.TYPE.PUSH);
@@ -2183,7 +2214,78 @@ function clearPrivateNotesError(reportID, accountID) {
Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {privateNotes: {[accountID]: {errors: null}}});
}
+function getDraftPrivateNote(reportID) {
+ return draftNoteMap[reportID];
+}
+
+/**
+ * Saves the private notes left by the user as they are typing. By saving this data the user can switch between chats, close
+ * tab, refresh etc without worrying about loosing what they typed out.
+ *
+ * @param {String} reportID
+ * @param {String} note
+ */
+function savePrivateNotesDraft(reportID, note) {
+ Onyx.merge(`${ONYXKEYS.COLLECTION.PRIVATE_NOTES_DRAFT}${reportID}`, note);
+}
+
+/**
+ * @private
+ * @param {string} searchInput
+ */
+function searchForReports(searchInput) {
+ // We do not try to make this request while offline because it sets a loading indicator optimistically
+ if (isNetworkOffline) {
+ Onyx.set(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, false);
+ return;
+ }
+
+ API.read(
+ 'SearchForReports',
+ {searchInput},
+ {
+ successData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
+ value: false,
+ },
+ ],
+ failureData: [
+ {
+ onyxMethod: Onyx.METHOD.MERGE,
+ key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS,
+ value: false,
+ },
+ ],
+ },
+ );
+}
+
+/**
+ * @private
+ * @param {string} searchInput
+ */
+const debouncedSearchInServer = lodashDebounce(searchForReports, CONST.TIMING.SEARCH_FOR_REPORTS_DEBOUNCE_TIME, {leading: false});
+
+/**
+ * @param {string} searchInput
+ */
+function searchInServer(searchInput) {
+ if (isNetworkOffline) {
+ Onyx.set(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, false);
+ return;
+ }
+
+ // Why not set this in optimistic data? It won't run until the API request happens and while the API request is debounced
+ // we want to show the loading state right away. Otherwise, we will see a flashing UI where the client options are sorted and
+ // tell the user there are no options, then we start searching, and tell them there are no options again.
+ Onyx.set(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, true);
+ debouncedSearchInServer(searchInput);
+}
+
export {
+ searchInServer,
addComment,
addAttachment,
reconnect,
@@ -2237,4 +2339,6 @@ export {
getReportPrivateNote,
clearPrivateNotesError,
hasErrorInPrivateNotes,
+ savePrivateNotesDraft,
+ getDraftPrivateNote,
};
diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js
index 117a092c3875..3b623a42689d 100644
--- a/src/libs/actions/Session/index.js
+++ b/src/libs/actions/Session/index.js
@@ -316,7 +316,7 @@ function signInWithShortLivedAuthToken(email, authToken) {
// If the user is signing in with a different account from the current app, should not pass the auto-generated login as it may be tied to the old account.
// scene 1: the user is transitioning to newDot from a different account on oldDot.
// scene 2: the user is transitioning to desktop app from a different account on web app.
- const oldPartnerUserID = credentials.login === email ? credentials.autoGeneratedLogin : '';
+ const oldPartnerUserID = credentials.login === email && credentials.autoGeneratedLogin ? credentials.autoGeneratedLogin : '';
API.read('SignInWithShortLivedAuthToken', {authToken, oldPartnerUserID, skipReauthentication: true}, {optimisticData, successData, failureData});
}
@@ -541,6 +541,10 @@ function clearAccountMessages() {
});
}
+function setAccountError(error) {
+ Onyx.merge(ONYXKEYS.ACCOUNT, {errors: ErrorUtils.getMicroSecondOnyxError(error)});
+}
+
// It's necessary to throttle requests to reauthenticate since calling this multiple times will cause Pusher to
// reconnect each time when we only need to reconnect once. This way, if an authToken is expired and we try to
// subscribe to a bunch of channels at once we will only reauthenticate and force reconnect Pusher once.
@@ -807,6 +811,7 @@ export {
unlinkLogin,
clearSignInData,
clearAccountMessages,
+ setAccountError,
authenticatePusher,
reauthenticatePusher,
invalidateCredentials,
diff --git a/src/libs/actions/Session/updateSessionAuthTokens.js b/src/libs/actions/Session/updateSessionAuthTokens.js
index 5be53c77a92c..e88b3b993c7a 100644
--- a/src/libs/actions/Session/updateSessionAuthTokens.js
+++ b/src/libs/actions/Session/updateSessionAuthTokens.js
@@ -2,8 +2,8 @@ import Onyx from 'react-native-onyx';
import ONYXKEYS from '../../../ONYXKEYS';
/**
- * @param {String} authToken
- * @param {String} encryptedAuthToken
+ * @param {String | undefined} authToken
+ * @param {String | undefined} encryptedAuthToken
*/
export default function updateSessionAuthTokens(authToken, encryptedAuthToken) {
Onyx.merge(ONYXKEYS.SESSION, {authToken, encryptedAuthToken});
diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.ts
similarity index 74%
rename from src/libs/actions/SignInRedirect.js
rename to src/libs/actions/SignInRedirect.ts
index a010621c4eea..67f5f2d8586f 100644
--- a/src/libs/actions/SignInRedirect.js
+++ b/src/libs/actions/SignInRedirect.ts
@@ -1,7 +1,5 @@
import Onyx from 'react-native-onyx';
-import lodashGet from 'lodash/get';
-import _ from 'underscore';
-import ONYXKEYS from '../../ONYXKEYS';
+import ONYXKEYS, {OnyxKey} from '../../ONYXKEYS';
import * as MainQueue from '../Network/MainQueue';
import * as PersistedRequests from './PersistedRequests';
import NetworkConnection from '../NetworkConnection';
@@ -12,27 +10,21 @@ import Navigation from '../Navigation/Navigation';
import * as ErrorUtils from '../ErrorUtils';
import * as SessionUtils from '../SessionUtils';
-let currentIsOffline;
-let currentShouldForceOffline;
+let currentIsOffline: boolean | undefined;
+let currentShouldForceOffline: boolean | undefined;
Onyx.connect({
key: ONYXKEYS.NETWORK,
callback: (network) => {
- if (!network) {
- return;
- }
- currentIsOffline = network.isOffline;
- currentShouldForceOffline = Boolean(network.shouldForceOffline);
+ currentIsOffline = network?.isOffline;
+ currentShouldForceOffline = network?.shouldForceOffline;
},
});
-/**
- * @param {String} errorMessage
- */
-function clearStorageAndRedirect(errorMessage) {
+function clearStorageAndRedirect(errorMessage?: string) {
// Under certain conditions, there are key-values we'd like to keep in storage even when a user is logged out.
// We pass these into the clear() method in order to avoid having to reset them on a delayed tick and getting
// flashes of unwanted default state.
- const keysToPreserve = [];
+ const keysToPreserve: OnyxKey[] = [];
keysToPreserve.push(ONYXKEYS.NVP_PREFERRED_LOCALE);
keysToPreserve.push(ONYXKEYS.ACTIVE_CLIENTS);
keysToPreserve.push(ONYXKEYS.DEVICE_ID);
@@ -58,15 +50,15 @@ function clearStorageAndRedirect(errorMessage) {
*/
function resetHomeRouteParams() {
Navigation.isNavigationReady().then(() => {
- const routes = navigationRef.current && lodashGet(navigationRef.current.getState(), 'routes');
- const homeRoute = _.find(routes, (route) => route.name === SCREENS.HOME);
+ const routes = navigationRef.current?.getState().routes;
+ const homeRoute = routes?.find((route) => route.name === SCREENS.HOME);
- const emptyParams = {};
- _.keys(lodashGet(homeRoute, 'params')).forEach((paramKey) => {
+ const emptyParams: Record = {};
+ Object.keys(homeRoute?.params ?? {}).forEach((paramKey) => {
emptyParams[paramKey] = undefined;
});
- Navigation.setParams(emptyParams, lodashGet(homeRoute, 'key', ''));
+ Navigation.setParams(emptyParams, homeRoute?.key ?? '');
Onyx.set(ONYXKEYS.IS_CHECKING_PUBLIC_ROOM, false);
});
}
@@ -79,9 +71,9 @@ function resetHomeRouteParams() {
*
* Normally this method would live in Session.js, but that would cause a circular dependency with Network.js.
*
- * @param {String} [errorMessage] error message to be displayed on the sign in page
+ * @param [errorMessage] error message to be displayed on the sign in page
*/
-function redirectToSignIn(errorMessage) {
+function redirectToSignIn(errorMessage?: string) {
MainQueue.clear();
HttpUtils.cancelPendingRequests();
PersistedRequests.clear();
diff --git a/src/libs/actions/Timing.js b/src/libs/actions/Timing.ts
similarity index 76%
rename from src/libs/actions/Timing.js
rename to src/libs/actions/Timing.ts
index 2be2cdc6fa63..13f40bab87c9 100644
--- a/src/libs/actions/Timing.js
+++ b/src/libs/actions/Timing.ts
@@ -4,15 +4,20 @@ import Firebase from '../Firebase';
import * as API from '../API';
import Log from '../Log';
-let timestampData = {};
+type TimestampData = {
+ startTime: number;
+ shouldUseFirebase: boolean;
+};
+
+let timestampData: Record = {};
/**
* Start a performance timing measurement
*
- * @param {String} eventName
- * @param {Boolean} shouldUseFirebase - adds an additional trace in Firebase
+ * @param eventName
+ * @param shouldUseFirebase - adds an additional trace in Firebase
*/
-function start(eventName, shouldUseFirebase = false) {
+function start(eventName: string, shouldUseFirebase = false) {
timestampData[eventName] = {startTime: Date.now(), shouldUseFirebase};
if (!shouldUseFirebase) {
@@ -25,11 +30,11 @@ function start(eventName, shouldUseFirebase = false) {
/**
* End performance timing. Measure the time between event start/end in milliseconds, and push to Grafana
*
- * @param {String} eventName - event name used as timestamp key
- * @param {String} [secondaryName] - optional secondary event name, passed to grafana
- * @param {number} [maxExecutionTime] - optional amount of time (ms) to wait before logging a warn
+ * @param eventName - event name used as timestamp key
+ * @param [secondaryName] - optional secondary event name, passed to grafana
+ * @param [maxExecutionTime] - optional amount of time (ms) to wait before logging a warn
*/
-function end(eventName, secondaryName = '', maxExecutionTime = 0) {
+function end(eventName: string, secondaryName = '', maxExecutionTime = 0) {
if (!timestampData[eventName]) {
return;
}
diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts
index 8653b038e381..8a7f0f7bd533 100644
--- a/src/libs/actions/Transaction.ts
+++ b/src/libs/actions/Transaction.ts
@@ -32,8 +32,8 @@ function createInitialWaypoints(transactionID: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
waypoints: {
- waypoint0: null,
- waypoint1: null,
+ waypoint0: {},
+ waypoint1: {},
},
},
});
@@ -107,15 +107,15 @@ function removeWaypoint(transactionID: string, currentIndex: string) {
const transaction = allTransactions?.[transactionID] ?? {};
const existingWaypoints = transaction?.comment?.waypoints ?? {};
const totalWaypoints = Object.keys(existingWaypoints).length;
- // Prevents removing the starting or ending waypoint but clear the stored address only if there are only two waypoints
- if (totalWaypoints === 2 && (index === 0 || index === totalWaypoints - 1)) {
- saveWaypoint(transactionID, index.toString(), null);
- return;
- }
const waypointValues = Object.values(existingWaypoints);
const removed = waypointValues.splice(index, 1);
- const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0] ?? null);
+ const isRemovedWaypointEmpty = removed.length > 0 && !TransactionUtils.waypointHasValidAddress(removed[0] ?? {});
+
+ // When there are only two waypoints we are adding empty waypoint back
+ if (totalWaypoints === 2 && (index === 0 || index === totalWaypoints - 1)) {
+ waypointValues.splice(index, 0, {});
+ }
const reIndexedWaypoints: WaypointCollection = {};
waypointValues.forEach((waypoint, idx) => {
diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js
index 78bd52988cdf..f65c20cd7e5b 100644
--- a/src/libs/actions/User.js
+++ b/src/libs/actions/User.js
@@ -541,7 +541,7 @@ function subscribeToUserEvents() {
/**
* Sync preferredSkinTone with Onyx and Server
- * @param {String} skinTone
+ * @param {Number} skinTone
*/
function updatePreferredSkinTone(skinTone) {
const optimisticData = [
diff --git a/src/libs/fileDownload/FileUtils.js b/src/libs/fileDownload/FileUtils.js
index e508d096128d..ba06b80f7c43 100644
--- a/src/libs/fileDownload/FileUtils.js
+++ b/src/libs/fileDownload/FileUtils.js
@@ -48,6 +48,27 @@ function showPermissionErrorAlert() {
]);
}
+/**
+ * Inform the users when they need to grant camera access and guide them to settings
+ */
+function showCameraPermissionsAlert() {
+ Alert.alert(
+ Localize.translateLocal('attachmentPicker.cameraPermissionRequired'),
+ Localize.translateLocal('attachmentPicker.expensifyDoesntHaveAccessToCamera'),
+ [
+ {
+ text: Localize.translateLocal('common.cancel'),
+ style: 'cancel',
+ },
+ {
+ text: Localize.translateLocal('common.settings'),
+ onPress: () => Linking.openSettings(),
+ },
+ ],
+ {cancelable: false},
+ );
+}
+
/**
* Generate a random file name with timestamp and file extension
* @param {String} url
@@ -170,4 +191,55 @@ const readFileAsync = (path, fileName) =>
});
});
-export {showGeneralErrorAlert, showSuccessAlert, showPermissionErrorAlert, splitExtensionFromFileName, getAttachmentName, getFileType, cleanFileName, appendTimeToFileName, readFileAsync};
+/**
+ * Converts a base64 encoded image string to a File instance.
+ * Adds a `uri` property to the File instance for accessing the blob as a URI.
+ *
+ * @param {string} base64 - The base64 encoded image string.
+ * @param {string} filename - Desired filename for the File instance.
+ * @returns {File} The File instance created from the base64 string with an additional `uri` property.
+ *
+ * @example
+ * const base64Image = "data:image/png;base64,..."; // your base64 encoded image
+ * const imageFile = base64ToFile(base64Image, "example.png");
+ * console.log(imageFile.uri); // Blob URI
+ */
+function base64ToFile(base64, filename) {
+ // Decode the base64 string
+ const byteString = atob(base64.split(',')[1]);
+
+ // Get the mime type from the base64 string
+ const mimeString = base64.split(',')[0].split(':')[1].split(';')[0];
+
+ // Convert byte string to Uint8Array
+ const arrayBuffer = new ArrayBuffer(byteString.length);
+ const uint8Array = new Uint8Array(arrayBuffer);
+ for (let i = 0; i < byteString.length; i++) {
+ uint8Array[i] = byteString.charCodeAt(i);
+ }
+
+ // Create a blob from the Uint8Array
+ const blob = new Blob([uint8Array], {type: mimeString});
+
+ // Create a File instance from the Blob
+ const file = new File([blob], filename, {type: mimeString, lastModified: Date.now()});
+
+ // Add a uri property to the File instance for accessing the blob as a URI
+ file.uri = URL.createObjectURL(blob);
+
+ return file;
+}
+
+export {
+ showGeneralErrorAlert,
+ showSuccessAlert,
+ showPermissionErrorAlert,
+ showCameraPermissionsAlert,
+ splitExtensionFromFileName,
+ getAttachmentName,
+ getFileType,
+ cleanFileName,
+ appendTimeToFileName,
+ readFileAsync,
+ base64ToFile,
+};
diff --git a/src/libs/getComponentDisplayName.ts b/src/libs/getComponentDisplayName.ts
index fd1bbcaea521..0bf52d543a84 100644
--- a/src/libs/getComponentDisplayName.ts
+++ b/src/libs/getComponentDisplayName.ts
@@ -1,6 +1,6 @@
import {ComponentType} from 'react';
/** Returns the display name of a component */
-export default function getComponentDisplayName(component: ComponentType): string {
+export default function getComponentDisplayName(component: ComponentType): string {
return component.displayName ?? component.name ?? 'Component';
}
diff --git a/src/libs/isReportMessageAttachment.ts b/src/libs/isReportMessageAttachment.ts
index c257e1db4191..8d1112261d1f 100644
--- a/src/libs/isReportMessageAttachment.ts
+++ b/src/libs/isReportMessageAttachment.ts
@@ -1,10 +1,5 @@
import CONST from '../CONST';
-
-type IsReportMessageAttachmentParams = {
- text: string;
- html: string;
- translationKey: string;
-};
+import {Message} from '../types/onyx/ReportAction';
/**
* Check whether a report action is Attachment or not.
@@ -12,7 +7,7 @@ type IsReportMessageAttachmentParams = {
*
* @param reportActionMessage report action's message as text, html and translationKey
*/
-export default function isReportMessageAttachment({text, html, translationKey}: IsReportMessageAttachmentParams): boolean {
+export default function isReportMessageAttachment({text, html, translationKey}: Message): boolean {
if (!text || !html) {
return false;
}
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/libs/migrateOnyx.js b/src/libs/migrateOnyx.js
index d96a37d95457..aaa706e71fb2 100644
--- a/src/libs/migrateOnyx.js
+++ b/src/libs/migrateOnyx.js
@@ -1,6 +1,5 @@
import _ from 'underscore';
import Log from './Log';
-import RenamePriorityModeKey from './migrations/RenamePriorityModeKey';
import PersonalDetailsByAccountID from './migrations/PersonalDetailsByAccountID';
import RenameReceiptFilename from './migrations/RenameReceiptFilename';
@@ -10,7 +9,7 @@ export default function () {
return new Promise((resolve) => {
// Add all migrations to an array so they are executed in order
- const migrationPromises = [RenamePriorityModeKey, PersonalDetailsByAccountID, RenameReceiptFilename];
+ const migrationPromises = [PersonalDetailsByAccountID, RenameReceiptFilename];
// Reduce all promises down to a single promise. All promises run in a linear fashion, waiting for the
// previous promise to finish before moving onto the next one.
diff --git a/src/libs/migrations/RenamePriorityModeKey.js b/src/libs/migrations/RenamePriorityModeKey.js
deleted file mode 100644
index a2be26880b52..000000000000
--- a/src/libs/migrations/RenamePriorityModeKey.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Onyx from 'react-native-onyx';
-import _ from 'underscore';
-import ONYXKEYS from '../../ONYXKEYS';
-import Log from '../Log';
-
-// This migration changes the name of the Onyx key NVP_PRIORITY_MODE from priorityMode to nvp_priorityMode
-export default function () {
- return new Promise((resolve) => {
- // Connect to the old key in Onyx to get the old value of priorityMode
- // then set the new key nvp_priorityMode to hold the old data
- // finally remove the old key by setting the value to null
- const connectionID = Onyx.connect({
- key: 'priorityMode',
- callback: (oldPriorityMode) => {
- Onyx.disconnect(connectionID);
-
- // Fail early here because there is nothing to migrate
- if (_.isEmpty(oldPriorityMode)) {
- Log.info('[Migrate Onyx] Skipped migration RenamePriorityModeKey');
- return resolve();
- }
-
- Onyx.multiSet({
- priorityMode: null,
- [ONYXKEYS.NVP_PRIORITY_MODE]: oldPriorityMode,
- }).then(() => {
- Log.info('[Migrate Onyx] Ran migration RenamePriorityModeKey');
- resolve();
- });
- },
- });
- });
-}
diff --git a/src/pages/AddPersonalBankAccountPage.js b/src/pages/AddPersonalBankAccountPage.js
index 7c04970c3980..a560a467bc7b 100644
--- a/src/pages/AddPersonalBankAccountPage.js
+++ b/src/pages/AddPersonalBankAccountPage.js
@@ -17,6 +17,7 @@ import Form from '../components/Form';
import ROUTES from '../ROUTES';
import * as PlaidDataProps from './ReimbursementAccount/plaidDataPropTypes';
import ConfirmationPage from '../components/ConfirmationPage';
+import * as PaymentMethods from '../libs/actions/PaymentMethods';
const propTypes = {
...withLocalizePropTypes,
@@ -86,10 +87,14 @@ class AddPersonalBankAccountPage extends React.Component {
BankAccounts.addPersonalBankAccount(selectedPlaidBankAccount);
}
- exitFlow() {
+ exitFlow(shouldContinue = false) {
const exitReportID = lodashGet(this.props, 'personalBankAccount.exitReportID');
+ const onSuccessFallbackRoute = lodashGet(this.props, 'personalBankAccount.onSuccessFallbackRoute', '');
+
if (exitReportID) {
Navigation.dismissModal(exitReportID);
+ } else if (shouldContinue && onSuccessFallbackRoute) {
+ PaymentMethods.continueSetup(onSuccessFallbackRoute);
} else {
Navigation.goBack(ROUTES.SETTINGS_WALLET);
}
@@ -115,7 +120,7 @@ class AddPersonalBankAccountPage extends React.Component {
description={this.props.translate('addPersonalBankAccountPage.successMessage')}
shouldShowButton
buttonText={this.props.translate('common.continue')}
- onButtonPress={this.exitFlow}
+ onButtonPress={() => this.exitFlow(true)}
/>
) : (
-
+
);
}
diff --git a/src/pages/EditRequestDistancePage.js b/src/pages/EditRequestDistancePage.js
index 3a606aeb8f07..4eb7340dd410 100644
--- a/src/pages/EditRequestDistancePage.js
+++ b/src/pages/EditRequestDistancePage.js
@@ -29,7 +29,7 @@ const propTypes = {
/** Parameters the route gets */
params: PropTypes.shape({
/** Type of IOU */
- iouType: PropTypes.oneOf(_.values(CONST.IOU.MONEY_REQUEST_TYPE)),
+ iouType: PropTypes.oneOf(_.values(CONST.IOU.TYPE)),
/** Id of the report on which the distance request is being created */
reportID: PropTypes.string,
@@ -39,13 +39,17 @@ const propTypes = {
/* Onyx props */
/** The original transaction that is being edited */
transaction: transactionPropTypes,
+
+ /** backup version of the original transaction */
+ transactionBackup: transactionPropTypes,
};
const defaultProps = {
transaction: {},
+ transactionBackup: {},
};
-function EditRequestDistancePage({report, route, transaction}) {
+function EditRequestDistancePage({report, route, transaction, transactionBackup}) {
const {isOffline} = useNetwork();
const {translate} = useLocalize();
const transactionWasSaved = useRef(false);
@@ -87,6 +91,16 @@ function EditRequestDistancePage({report, route, transaction}) {
* @param {Object} waypoints
*/
const saveTransaction = (waypoints) => {
+ // If nothing was changed, simply go to transaction thread
+ // We compare only addresses because numbers are rounded while backup
+ const oldWaypoints = lodashGet(transactionBackup, 'comment.waypoints', {});
+ const oldAddresses = _.mapObject(oldWaypoints, (waypoint) => _.pick(waypoint, 'address'));
+ const addresses = _.mapObject(waypoints, (waypoint) => _.pick(waypoint, 'address'));
+ if (_.isEqual(oldAddresses, addresses)) {
+ Navigation.dismissModal(report.reportID);
+ return;
+ }
+
transactionWasSaved.current = true;
IOU.updateDistanceRequest(transaction.transactionID, report.reportID, {waypoints});
@@ -125,4 +139,7 @@ export default withOnyx({
transaction: {
key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION}${props.transactionID}`,
},
+ transactionBackup: {
+ key: (props) => `${ONYXKEYS.COLLECTION.TRANSACTION}${props.transactionID}-backup`,
+ },
})(EditRequestDistancePage);
diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js
index 28e70dc1a47e..a85f490bbb42 100644
--- a/src/pages/EditRequestPage.js
+++ b/src/pages/EditRequestPage.js
@@ -5,6 +5,7 @@ import lodashValues from 'lodash/values';
import {withOnyx} from 'react-native-onyx';
import CONST from '../CONST';
import ONYXKEYS from '../ONYXKEYS';
+import ROUTES from '../ROUTES';
import compose from '../libs/compose';
import Navigation from '../libs/Navigation/Navigation';
import * as ReportActionsUtils from '../libs/ReportActionsUtils';
@@ -30,6 +31,7 @@ import EditRequestCategoryPage from './EditRequestCategoryPage';
import EditRequestTagPage from './EditRequestTagPage';
import categoryPropTypes from '../components/categoryPropTypes';
import ScreenWrapper from '../components/ScreenWrapper';
+import transactionPropTypes from '../components/transactionPropTypes';
const propTypes = {
/** Route from navigation */
@@ -75,6 +77,9 @@ const propTypes = {
/** Collection of tags attached to a policy */
policyTags: tagPropTypes,
+ /** The original transaction that is being edited */
+ transaction: transactionPropTypes,
+
...withCurrentUserPersonalDetailsPropTypes,
};
@@ -88,10 +93,12 @@ const defaultProps = {
},
policyCategories: {},
policyTags: {},
+ transaction: {},
};
function EditRequestPage({betas, report, route, parentReport, policy, session, policyCategories, policyTags, parentReportActions, transaction}) {
- const parentReportAction = parentReportActions[report.parentReportActionID];
+ const parentReportActionID = lodashGet(report, 'parentReportActionID', '0');
+ const parentReportAction = lodashGet(parentReportActions, parentReportActionID);
const {
amount: transactionAmount,
currency: transactionCurrency,
@@ -199,6 +206,10 @@ function EditRequestPage({betas, report, route, parentReport, policy, session, p
currency: defaultCurrency,
});
}}
+ onNavigateToCurrency={() => {
+ const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, ''));
+ Navigation.navigate(ROUTES.EDIT_CURRENCY_REQUEST.getRoute(report.reportID, defaultCurrency, activeRoute));
+ }}
/>
);
}
@@ -321,7 +332,8 @@ export default compose(
withOnyx({
transaction: {
key: ({report, parentReportActions}) => {
- const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]);
+ const parentReportActionID = lodashGet(report, 'parentReportActionID', '0');
+ const parentReportAction = lodashGet(parentReportActions, parentReportActionID);
return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(parentReportAction, 'originalMessage.IOUTransactionID', 0)}`;
},
},
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
new file mode 100644
index 000000000000..d10803cd40ea
--- /dev/null
+++ b/src/pages/EditSplitBillPage.js
@@ -0,0 +1,161 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import lodashGet from 'lodash/get';
+import {withOnyx} from 'react-native-onyx';
+import CONST from '../CONST';
+import ROUTES from '../ROUTES';
+import ONYXKEYS from '../ONYXKEYS';
+import compose from '../libs/compose';
+import transactionPropTypes from '../components/transactionPropTypes';
+import * as ReportUtils from '../libs/ReportUtils';
+import * as IOU from '../libs/actions/IOU';
+import * as CurrencyUtils from '../libs/CurrencyUtils';
+import Navigation from '../libs/Navigation/Navigation';
+import FullPageNotFoundView from '../components/BlockingViews/FullPageNotFoundView';
+import EditRequestDescriptionPage from './EditRequestDescriptionPage';
+import EditRequestMerchantPage from './EditRequestMerchantPage';
+import EditRequestCreatedPage from './EditRequestCreatedPage';
+import EditRequestAmountPage from './EditRequestAmountPage';
+
+const propTypes = {
+ /** Route from navigation */
+ route: PropTypes.shape({
+ /** Params from the route */
+ params: PropTypes.shape({
+ /** The transaction field we are editing */
+ field: PropTypes.string,
+
+ /** The chat reportID of the split */
+ reportID: PropTypes.string,
+
+ /** reportActionID of the split action */
+ reportActionID: PropTypes.string,
+ }),
+ }).isRequired,
+
+ /** The current transaction */
+ transaction: transactionPropTypes.isRequired,
+
+ /** The draft transaction that holds data to be persisted on the current transaction */
+ draftTransaction: transactionPropTypes,
+};
+
+const defaultProps = {
+ draftTransaction: undefined,
+};
+
+function EditSplitBillPage({route, transaction, draftTransaction}) {
+ const fieldToEdit = lodashGet(route, ['params', 'field'], '');
+ const reportID = lodashGet(route, ['params', 'reportID'], '');
+ const reportActionID = lodashGet(route, ['params', 'reportActionID'], '');
+
+ const {
+ amount: transactionAmount,
+ currency: transactionCurrency,
+ comment: transactionDescription,
+ merchant: transactionMerchant,
+ created: transactionCreated,
+ } = draftTransaction ? ReportUtils.getTransactionDetails(draftTransaction) : ReportUtils.getTransactionDetails(transaction);
+
+ const defaultCurrency = lodashGet(route, 'params.currency', '') || transactionCurrency;
+
+ function navigateBackToSplitDetails() {
+ Navigation.navigate(ROUTES.SPLIT_BILL_DETAILS.getRoute(reportID, reportActionID));
+ }
+
+ function setDraftSplitTransaction(transactionChanges) {
+ IOU.setDraftSplitTransaction(transaction.transactionID, transactionChanges);
+ navigateBackToSplitDetails();
+ }
+
+ if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DESCRIPTION) {
+ return (
+ {
+ setDraftSplitTransaction({
+ comment: transactionChanges.comment.trim(),
+ });
+ }}
+ />
+ );
+ }
+
+ if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.DATE) {
+ return (
+ {
+ setDraftSplitTransaction({
+ created: transactionChanges.created,
+ });
+ }}
+ />
+ );
+ }
+
+ if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.AMOUNT) {
+ return (
+ {
+ const amount = CurrencyUtils.convertToBackendAmount(Number.parseFloat(transactionChanges));
+
+ setDraftSplitTransaction({
+ amount,
+ currency: defaultCurrency,
+ });
+ }}
+ onNavigateToCurrency={() => {
+ const activeRoute = encodeURIComponent(Navigation.getActiveRoute().replace(/\?.*/, ''));
+ Navigation.navigate(ROUTES.EDIT_SPLIT_BILL_CURRENCY.getRoute(reportID, reportActionID, defaultCurrency, activeRoute));
+ }}
+ />
+ );
+ }
+
+ if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.MERCHANT) {
+ return (
+ {
+ setDraftSplitTransaction({merchant: transactionChanges.merchant.trim()});
+ }}
+ />
+ );
+ }
+
+ return ;
+}
+
+EditSplitBillPage.displayName = 'EditSplitBillPage';
+EditSplitBillPage.propTypes = propTypes;
+EditSplitBillPage.defaultProps = defaultProps;
+export default compose(
+ withOnyx({
+ reportActions: {
+ key: ({route}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${route.params.reportID}`,
+ canEvict: false,
+ },
+ }),
+ // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file
+ withOnyx({
+ transaction: {
+ key: ({route, reportActions}) => {
+ const reportAction = reportActions[`${route.params.reportActionID.toString()}`];
+ return `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(reportAction, 'originalMessage.IOUTransactionID', 0)}`;
+ },
+ },
+ draftTransaction: {
+ key: ({route, reportActions}) => {
+ const reportAction = reportActions[`${route.params.reportActionID.toString()}`];
+ return `${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${lodashGet(reportAction, 'originalMessage.IOUTransactionID', 0)}`;
+ },
+ },
+ }),
+)(EditSplitBillPage);
diff --git a/src/pages/EnablePayments/ActivateStep.js b/src/pages/EnablePayments/ActivateStep.js
index 268c2664e01d..2d23f39d25e5 100644
--- a/src/pages/EnablePayments/ActivateStep.js
+++ b/src/pages/EnablePayments/ActivateStep.js
@@ -1,3 +1,4 @@
+import _ from 'underscore';
import React from 'react';
import {withOnyx} from 'react-native-onyx';
import * as LottieAnimations from '../../components/LottieAnimations';
@@ -29,8 +30,8 @@ const defaultProps = {
};
function ActivateStep(props) {
- const isGoldWallet = props.userWallet.tierName === CONST.WALLET.TIER_NAME.GOLD;
- const animation = isGoldWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo;
+ const isActivatedWallet = _.contains([CONST.WALLET.TIER_NAME.GOLD, CONST.WALLET.TIER_NAME.PLATINUM], props.userWallet.tierName);
+ const animation = isActivatedWallet ? LottieAnimations.Fireworks : LottieAnimations.ReviewingBankInfo;
const continueButtonText = props.walletTerms.chatReportID ? props.translate('activateStep.continueToPayment') : props.translate('activateStep.continueToTransfer');
return (
@@ -38,9 +39,9 @@ function ActivateStep(props) {
diff --git a/src/pages/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 (
-