From 82b4613342abb246393f78840614120cbc9e3ce3 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Mon, 10 Feb 2025 14:22:56 +0100
Subject: [PATCH 01/16] Initial updates to migrate mobile platforms to
 pusher-websocket-react-native lib

---
 ios/Podfile.lock                              |  28 +-
 package-lock.json                             |  11 +
 package.json                                  |   1 +
 src/CONST.ts                                  |   7 +
 src/libs/Pusher/pusher/index.native.ts        | 406 ++++++++++++++++++
 .../Pusher/{pusher.ts => pusher/index.ts}     |  17 +-
 src/libs/PusherConnectionManager.ts           |   2 +-
 src/libs/actions/Session/index.ts             |  20 +-
 8 files changed, 472 insertions(+), 20 deletions(-)
 create mode 100644 src/libs/Pusher/pusher/index.native.ts
 rename src/libs/Pusher/{pusher.ts => pusher/index.ts} (95%)

diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index a7347a4c509..714f187cea3 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -310,6 +310,7 @@ PODS:
     - nanopb/encode (= 2.30908.0)
   - nanopb/decode (2.30908.0)
   - nanopb/encode (2.30908.0)
+  - NWWebSocket (0.5.4)
   - Onfido (29.7.2)
   - onfido-react-native-sdk (10.6.0):
     - DoubleConversion
@@ -335,6 +336,12 @@ PODS:
     - Yoga
   - Plaid (5.6.0)
   - PromisesObjC (2.4.0)
+  - pusher-websocket-react-native (1.3.1):
+    - PusherSwift (~> 10.1.5)
+    - React
+  - PusherSwift (10.1.5):
+    - NWWebSocket (~> 0.5.4)
+    - TweetNacl (~> 1.0.0)
   - RCT-Folly (2024.01.01.00):
     - boost
     - DoubleConversion
@@ -2842,6 +2849,7 @@ PODS:
     - SDWebImage/Core (~> 5.17)
   - SocketRocket (0.7.1)
   - Turf (2.8.0)
+  - TweetNacl (1.0.2)
   - VisionCamera (4.6.1):
     - VisionCamera/Core (= 4.6.1)
     - VisionCamera/React (= 4.6.1)
@@ -2872,6 +2880,7 @@ DEPENDENCIES:
   - hermes-engine (from `../node_modules/react-native/sdks/hermes-engine/hermes-engine.podspec`)
   - lottie-react-native (from `../node_modules/lottie-react-native`)
   - "onfido-react-native-sdk (from `../node_modules/@onfido/react-native-sdk`)"
+  - "pusher-websocket-react-native (from `../node_modules/@pusher/pusher-websocket-react-native`)"
   - RCT-Folly (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
   - RCT-Folly/Fabric (from `../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec`)
   - RCTDeprecation (from `../node_modules/react-native/ReactApple/Libraries/RCTFoundation/RCTDeprecation`)
@@ -3008,15 +3017,18 @@ SPEC REPOS:
     - MapboxMaps
     - MapboxMobileEvents
     - nanopb
+    - NWWebSocket
     - Onfido
     - Plaid
     - PromisesObjC
+    - PusherSwift
     - SDWebImage
     - SDWebImageAVIFCoder
     - SDWebImageSVGCoder
     - SDWebImageWebPCoder
     - SocketRocket
     - Turf
+    - TweetNacl
 
 EXTERNAL SOURCES:
   AppLogs:
@@ -3060,6 +3072,8 @@ EXTERNAL SOURCES:
     :path: "../node_modules/lottie-react-native"
   onfido-react-native-sdk:
     :path: "../node_modules/@onfido/react-native-sdk"
+  pusher-websocket-react-native:
+    :path: "../node_modules/@pusher/pusher-websocket-react-native"
   RCT-Folly:
     :podspec: "../node_modules/react-native/third-party-podspecs/RCT-Folly.podspec"
   RCTDeprecation:
@@ -3275,8 +3289,8 @@ SPEC CHECKSUMS:
   AirshipServiceExtension: 9c73369f426396d9fb9ff222d86d842fac76ba46
   AppAuth: 501c04eda8a8d11f179dbe8637b7a91bb7e5d2fa
   AppLogs: 3bc4e9b141dbf265b9464409caaa40416a9ee0e0
-  boost: d7090b1a93a9798c029277a8288114f2948f471c
-  DoubleConversion: f16ae600a246532c4020132d54af21d0ddb2a385
+  boost: 26992d1adf73c1c7676360643e687aee6dda994b
+  DoubleConversion: 76ab83afb40bddeeee456813d9c04f67f78771b5
   EXAV: 9773c9799767c9925547b05e41a26a0240bb8ef2
   EXImageLoader: 759063a65ab016b836f73972d3bb25404888713d
   expensify-react-native-background-task: 6f797cf470b627912c246514b1631a205794775d
@@ -3296,11 +3310,11 @@ SPEC CHECKSUMS:
   FirebaseInstallations: 40bd9054049b2eae9a2c38ef1c3dd213df3605cd
   FirebasePerformance: 0c01a7a496657d7cea86d40c0b1725259d164c6c
   FirebaseRemoteConfig: 2d6e2cfdb49af79535c8af8a80a4a5009038ec2b
-  fmt: 10c6e61f4be25dc963c36bd73fc7b1705fe975be
+  fmt: 4c2741a687cc09f0634a2e2c72a838b99f1ff120
   ForkInputMask: 55e3fbab504b22da98483e9f9a6514b98fdd2f3c
   FullStory: c8a10b2358c0d33c57be84d16e4c440b0434b33d
   fullstory_react-native: 63a803cca04b0447a71daa73e4df3f7b56e1919d
-  glog: 08b301085f15bcbb6ff8632a8ebaf239aae04e6a
+  glog: 69ef571f3de08433d766d614c73a9838a06bf7eb
   GoogleAppMeasurement: 5ba1164e3c844ba84272555e916d0a6d3d977e91
   GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
   GoogleSignIn: d4281ab6cf21542b1cfaff85c191f230b399d2db
@@ -3318,11 +3332,14 @@ SPEC CHECKSUMS:
   MapboxMaps: 05822ab0ee74f7d626e6471572439afe35c1c116
   MapboxMobileEvents: d044b9edbe0ec7df60f6c2c9634fe9a7f449266b
   nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96
+  NWWebSocket: 040d22f23438cc09aaeabf537beff67699c3c76d
   Onfido: f3af62ea1c9a419589c133e3e511e5d2c4f3f8af
   onfido-react-native-sdk: 4ccfdeb10f9ccb4a5799d2555cdbc2a068a42c0d
   Plaid: c32f22ffce5ec67c9e6147eaf6c4d7d5f8086d89
   PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
-  RCT-Folly: bf5c0376ffe4dd2cf438dcf86db385df9fdce648
+  pusher-websocket-react-native: e40c49a1e4ec96d4157375aebcf44943f0f8f62f
+  PusherSwift: cad631bad86cfff4b8458dce1310a7774e469b1f
+  RCT-Folly: 4464f4d875961fce86008d45f4ecf6cef6de0740
   RCTDeprecation: 2c5e1000b04ab70b53956aa498bf7442c3c6e497
   RCTRequired: 5f785a001cf68a551c5f5040fb4c415672dbb481
   RCTTypeSafety: 6b98db8965005d32449605c0d005ecb4fee8a0f7
@@ -3428,6 +3445,7 @@ SPEC CHECKSUMS:
   SDWebImageWebPCoder: e38c0a70396191361d60c092933e22c20d5b1380
   SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
   Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e
+  TweetNacl: 3abf4d1d2082b0114e7a67410e300892448951e6
   VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c
   Yoga: 3deb2471faa9916c8a82dda2a22d3fba2620ad37
 
diff --git a/package-lock.json b/package-lock.json
index 79c088f2740..af3257de8f0 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -26,6 +26,7 @@
         "@gorhom/portal": "^1.0.14",
         "@invertase/react-native-apple-authentication": "^2.2.2",
         "@onfido/react-native-sdk": "10.6.0",
+        "@pusher/pusher-websocket-react-native": "^1.3.1",
         "@react-native-camera-roll/camera-roll": "7.4.0",
         "@react-native-clipboard/clipboard": "^1.15.0",
         "@react-native-community/geolocation": "3.3.0",
@@ -7561,6 +7562,16 @@
       "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==",
       "dev": true
     },
+    "node_modules/@pusher/pusher-websocket-react-native": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/@pusher/pusher-websocket-react-native/-/pusher-websocket-react-native-1.3.1.tgz",
+      "integrity": "sha512-NUarJuOW79b9DBjH/ena0pOutBR0uXnWLg5mIbzUYIl0A7gASC1dgBd8fJ7s5fJXQgQQXNlrqQ9E1SKz6pIvuA==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/@radix-ui/primitive": {
       "version": "1.1.0",
       "dev": true,
diff --git a/package.json b/package.json
index b79a9e8318e..a5286e115de 100644
--- a/package.json
+++ b/package.json
@@ -93,6 +93,7 @@
     "@gorhom/portal": "^1.0.14",
     "@invertase/react-native-apple-authentication": "^2.2.2",
     "@onfido/react-native-sdk": "10.6.0",
+    "@pusher/pusher-websocket-react-native": "^1.3.1",
     "@react-native-camera-roll/camera-roll": "7.4.0",
     "@react-native-clipboard/clipboard": "^1.15.0",
     "@react-native-community/geolocation": "3.3.0",
diff --git a/src/CONST.ts b/src/CONST.ts
index 285d3ed2e13..3f12dba5146 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1588,6 +1588,13 @@ const CONST = {
         PRIVATE_USER_CHANNEL_PREFIX: 'private-encrypted-user-accountID-',
         PRIVATE_REPORT_CHANNEL_PREFIX: 'private-report-reportID-',
         PRESENCE_ACTIVE_GUIDES: 'presence-activeGuides',
+        STATE: {
+            CONNECTING: 'CONNECTING',
+            CONNECTED: 'CONNECTED',
+            DISCONNECTING: 'DISCONNECTING',
+            DISCONNECTED: 'DISCONNECTED',
+            RECONNECTING: 'RECONNECTING',
+        },
     },
 
     EMOJI_SPACER: 'SPACER',
diff --git a/src/libs/Pusher/pusher/index.native.ts b/src/libs/Pusher/pusher/index.native.ts
new file mode 100644
index 00000000000..ee617e2d589
--- /dev/null
+++ b/src/libs/Pusher/pusher/index.native.ts
@@ -0,0 +1,406 @@
+import type {PusherChannel} from '@pusher/pusher-websocket-react-native';
+import {Pusher} from '@pusher/pusher-websocket-react-native';
+import isObject from 'lodash/isObject';
+import {InteractionManager} from 'react-native';
+import Onyx from 'react-native-onyx';
+import type {LiteralUnion, ValueOf} from 'type-fest';
+import Log from '@libs/Log';
+import TYPE from '@libs/Pusher/EventType';
+import type {SocketEventName} from '@libs/Pusher/library/types';
+import {onAuthorizer} from '@userActions/Session';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
+import type DeepValueOf from '@src/types/utils/DeepValueOf';
+
+type States = {
+    previous: string;
+    current: string;
+};
+
+type Args = {
+    appKey: string;
+    cluster: string;
+    authEndpoint: string;
+};
+
+type UserIsTypingEvent = ReportUserIsTyping & {
+    userLogin?: string;
+};
+
+type UserIsLeavingRoomEvent = Record<string, boolean> & {
+    userLogin?: string;
+};
+
+type PingPongEvent = Record<string, string | number> & {
+    pingID: string;
+    pingTimestamp: number;
+};
+
+type PusherEventMap = {
+    [TYPE.USER_IS_TYPING]: UserIsTypingEvent;
+    [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent;
+    [TYPE.PONG]: PingPongEvent;
+};
+
+type EventData<EventName extends string> = {chunk?: string; id?: string; index?: number; final?: boolean} & (EventName extends keyof PusherEventMap
+    ? PusherEventMap[EventName]
+    : OnyxUpdatesFromServer);
+
+type EventCallbackError = {type?: ValueOf<typeof CONST.ERROR>; data: {code: number; message?: string}};
+
+type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean};
+
+type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void;
+
+type PusherEventName = LiteralUnion<DeepValueOf<typeof TYPE>, string>;
+
+let shouldForceOffline = false;
+Onyx.connect({
+    key: ONYXKEYS.NETWORK,
+    callback: (network) => {
+        if (!network) {
+            return;
+        }
+        shouldForceOffline = !!network.shouldForceOffline;
+    },
+});
+
+let socket: Pusher | null;
+let pusherSocketID: string | undefined;
+const socketEventCallbacks: SocketEventCallback[] = [];
+
+let resolveInitPromise: () => void;
+let initPromise = new Promise<void>((resolve) => {
+    resolveInitPromise = resolve;
+});
+
+const eventsBoundToChannels = new Map<string, Map<PusherEventName, (eventData: EventData<string>) => void>>();
+
+/**
+ * Trigger each of the socket event callbacks with the event information
+ */
+function callSocketEventCallbacks(eventName: SocketEventName, data?: EventCallbackError | States) {
+    socketEventCallbacks.forEach((cb) => cb(eventName, data));
+}
+
+/**
+ * Initialize our pusher lib
+ * @returns resolves when Pusher has connected
+ */
+function init(args: Args): Promise<void> {
+    // eslint-disable-next-line @lwc/lwc/no-async-await
+    return new Promise<void>(async (resolve) => {
+        if (socket) {
+            resolve();
+            return;
+        }
+
+        socket = Pusher.getInstance();
+        await socket.init({
+            apiKey: args.appKey,
+            cluster: args.cluster,
+            onConnectionStateChange: (currentState: string, previousState: string) => {
+                if (currentState === CONST.PUSHER.STATE.CONNECTED) {
+                    socket?.getSocketId().then((id: string) => {
+                        pusherSocketID = id;
+                        callSocketEventCallbacks('connected');
+                        resolve();
+                    });
+                }
+                if (currentState === CONST.PUSHER.STATE.DISCONNECTED) {
+                    callSocketEventCallbacks('disconnected');
+                }
+                callSocketEventCallbacks('state_change', {previous: previousState, current: currentState});
+            },
+            onError: (message: string, code: number) => callSocketEventCallbacks('error', {data: {code, message}}),
+            onAuthorizer: async (channelName: string, socketId: string) => {
+                const res = await onAuthorizer(channelName, socketId);
+                return {
+                    auth: res.auth,
+                    shared_secret: res.shared_secret,
+                };
+            },
+        });
+        await socket.connect();
+    }).then(resolveInitPromise);
+}
+
+/**
+ * Returns a Pusher channel for a channel name
+ */
+function getChannel(channelName: string): PusherChannel | undefined {
+    if (!socket) {
+        return;
+    }
+
+    return socket.getChannel(channelName);
+}
+
+/**
+ * Binds an event callback to a channel + eventName
+ */
+function bindEventToChannel<EventName extends PusherEventName>(channel: string, eventName?: EventName, eventCallback: (data: EventData<EventName>) => void = () => {}) {
+    if (!eventName) {
+        return;
+    }
+
+    const chunkedDataEvents: Record<string, ChunkedDataEvents> = {};
+    const callback = (eventData: EventData<EventName>) => {
+        if (shouldForceOffline) {
+            Log.info('[Pusher] Ignoring a Push event because shouldForceOffline = true');
+            return;
+        }
+
+        let data: EventData<EventName>;
+        try {
+            data = isObject(eventData) ? eventData : (JSON.parse(eventData) as EventData<EventName>);
+        } catch (err) {
+            Log.alert('[Pusher] Unable to parse single JSON event data from Pusher', {error: err, eventData});
+            return;
+        }
+        if (data.id === undefined || data.chunk === undefined || data.final === undefined) {
+            eventCallback(data);
+            return;
+        }
+
+        // If we are chunking the requests, we need to construct a rolling list of all packets that have come through
+        // Pusher. If we've completed one of these full packets, we'll combine the data and act on the event that it's
+        // assigned to.
+
+        // If we haven't seen this eventID yet, initialize it into our rolling list of packets.
+        if (!chunkedDataEvents[data.id]) {
+            chunkedDataEvents[data.id] = {chunks: [], receivedFinal: false};
+        }
+
+        // Add it to the rolling list.
+        const chunkedEvent = chunkedDataEvents[data.id];
+        if (data.index !== undefined) {
+            chunkedEvent.chunks[data.index] = data.chunk;
+        }
+
+        // If this is the last packet, mark that we've hit the end.
+        if (data.final) {
+            chunkedEvent.receivedFinal = true;
+        }
+
+        // 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 === Object.keys(chunkedEvent.chunks).length) {
+            try {
+                eventCallback(JSON.parse(chunkedEvent.chunks.join('')) as EventData<EventName>);
+            } catch (err) {
+                Log.alert('[Pusher] Unable to parse chunked JSON response from Pusher', {
+                    error: err,
+                    eventData: chunkedEvent.chunks.join(''),
+                });
+
+                // Using console.error is helpful here because it will print a usable stack trace to the console to debug where the error comes from
+                console.error(err);
+            }
+
+            delete chunkedDataEvents[data.id];
+        }
+    };
+
+    if (!eventsBoundToChannels.has(channel)) {
+        eventsBoundToChannels.set(channel, new Map());
+    }
+
+    eventsBoundToChannels.get(channel)?.set(eventName, callback);
+}
+
+/**
+ * Subscribe to a channel and an event
+ */
+function subscribe<EventName extends PusherEventName>(
+    channelName: string,
+    eventName?: EventName,
+    eventCallback: (data: EventData<EventName>) => void = () => {},
+    onResubscribe = () => {},
+): Promise<void> {
+    return initPromise.then(
+        () =>
+            new Promise((resolve, reject) => {
+                InteractionManager.runAfterInteractions(async () => {
+                    // We cannot call subscribe() before init(). Prevent any attempt to do this on dev.
+                    if (!socket) {
+                        throw new Error(`[Pusher] instance not found. Pusher.subscribe()
+            most likely has been called before Pusher.init()`);
+                    }
+
+                    Log.info('[Pusher] Attempting to subscribe to channel', false, {channelName, eventName});
+                    const channel = getChannel(channelName);
+
+                    if (!channel) {
+                        await socket.subscribe({
+                            channelName,
+                            onEvent: (event) => {
+                                const callback = eventsBoundToChannels.get(event.channelName)?.get(event.eventName);
+                                callback?.(event.data);
+                            },
+                            onSubscriptionSucceeded: () => {
+                                bindEventToChannel(channelName, eventName, eventCallback);
+                                resolve();
+                                // When subscribing for the first time we register a success callback that can be
+                                // called multiple times when the subscription succeeds again in the future
+                                // e.g. as a result of Pusher disconnecting and reconnecting. This callback does
+                                // not fire on the first subscription_succeeded event.
+                                onResubscribe();
+                            },
+                            onSubscriptionError: (name: string, message: string) => {
+                                Log.hmmm('[Pusher] Issue authenticating with Pusher during subscribe attempt.', {
+                                    channelName,
+                                    message,
+                                });
+                                reject(message);
+                            },
+                        });
+                    } else {
+                        bindEventToChannel(channelName, eventName, eventCallback);
+                        resolve();
+                    }
+                });
+            }),
+    );
+}
+
+/**
+ * Unsubscribe from a channel and optionally a specific event
+ */
+function unsubscribe(channelName: string, eventName: PusherEventName = '') {
+    const channel = getChannel(channelName);
+
+    if (!channel) {
+        Log.hmmm('[Pusher] Attempted to unsubscribe or unbind from a channel, but Pusher-JS has no knowledge of it', {channelName, eventName});
+        return;
+    }
+
+    if (eventName) {
+        Log.info('[Pusher] Unbinding event', false, {eventName, channelName});
+        eventsBoundToChannels.get(channelName)?.delete(eventName);
+        if (eventsBoundToChannels.get(channelName)?.size === 0) {
+            Log.info(`[Pusher] After unbinding ${eventName} from channel ${channelName}, no other events were bound to that channel. Unsubscribing...`, false);
+            eventsBoundToChannels.delete(channelName);
+            socket?.unsubscribe({channelName});
+        }
+    } else {
+        Log.info('[Pusher] Unsubscribing from channel', false, {channelName});
+        eventsBoundToChannels.delete(channelName);
+        socket?.unsubscribe({channelName});
+    }
+}
+
+/**
+ * Are we already in the process of subscribing to this channel?
+ */
+function isAlreadySubscribing(channelName: string): boolean {
+    if (!socket) {
+        return false;
+    }
+
+    // const channel = getChannel(channelName);
+    // return channel ? channel.subscriptionPending : false;
+    return false;
+}
+
+/**
+ * Are we already subscribed to this channel?
+ */
+function isSubscribed(channelName: string): boolean {
+    if (!socket) {
+        return false;
+    }
+
+    const channel = getChannel(channelName);
+    return !!channel;
+}
+
+/**
+ * Sends an event over a specific event/channel in pusher.
+ */
+function sendEvent<EventName extends PusherEventName>(channelName: string, eventName: EventName, payload: EventData<EventName>) {
+    // 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.
+    if (!isSubscribed(channelName)) {
+        return;
+    }
+
+    if (shouldForceOffline) {
+        Log.info('[Pusher] Ignoring a Send event because shouldForceOffline = true');
+        return;
+    }
+
+    socket?.trigger({channelName, eventName, data: payload});
+}
+
+/**
+ * Register a method that will be triggered when a socket event happens (like disconnecting)
+ */
+function registerSocketEventCallback(cb: SocketEventCallback) {
+    socketEventCallbacks.push(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.
+ */
+// function registerCustomAuthorizer(authorizer: ChannelAuthorizerGenerator) {
+//     customAuthorizer = authorizer;
+// }
+
+/**
+ * Disconnect from Pusher
+ */
+function disconnect() {
+    if (!socket) {
+        Log.info('[Pusher] Attempting to disconnect from Pusher before initialisation has occurred, ignoring.');
+        return;
+    }
+
+    socket.disconnect();
+    socket = null;
+    pusherSocketID = '';
+    eventsBoundToChannels.clear();
+    initPromise = new Promise((resolve) => {
+        resolveInitPromise = resolve;
+    });
+}
+
+/**
+ * Disconnect and Re-Connect Pusher
+ */
+function reconnect() {
+    if (!socket) {
+        Log.info('[Pusher] Unable to reconnect since Pusher instance does not yet exist.');
+        return;
+    }
+
+    Log.info('[Pusher] Reconnecting to Pusher');
+    socket.disconnect();
+    socket.connect();
+}
+
+function getPusherSocketID(): string | undefined {
+    return pusherSocketID;
+}
+
+export {
+    init,
+    subscribe,
+    unsubscribe,
+    getChannel,
+    isSubscribed,
+    isAlreadySubscribing,
+    sendEvent,
+    disconnect,
+    reconnect,
+    registerSocketEventCallback,
+    // registerCustomAuthorizer,
+    TYPE,
+    getPusherSocketID,
+};
+
+export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent};
diff --git a/src/libs/Pusher/pusher.ts b/src/libs/Pusher/pusher/index.ts
similarity index 95%
rename from src/libs/Pusher/pusher.ts
rename to src/libs/Pusher/pusher/index.ts
index cb18bc7f63d..6ca05e78818 100644
--- a/src/libs/Pusher/pusher.ts
+++ b/src/libs/Pusher/pusher/index.ts
@@ -4,13 +4,13 @@ import {InteractionManager} from 'react-native';
 import Onyx from 'react-native-onyx';
 import type {LiteralUnion, ValueOf} from 'type-fest';
 import Log from '@libs/Log';
+import TYPE from '@libs/Pusher/EventType';
+import Pusher from '@libs/Pusher/library';
+import type {SocketEventName} from '@libs/Pusher/library/types';
 import type CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
 import type DeepValueOf from '@src/types/utils/DeepValueOf';
-import TYPE from './EventType';
-import Pusher from './library';
-import type {SocketEventName} from './library/types';
 
 type States = {
     previous: string;
@@ -98,7 +98,7 @@ function callSocketEventCallbacks(eventName: SocketEventName, data?: EventCallba
  * Initialize our pusher lib
  * @returns resolves when Pusher has connected
  */
-function init(args: Args, params?: unknown): Promise<void> {
+function init(args: Args): Promise<void> {
     return new Promise<void>((resolve) => {
         if (socket) {
             resolve();
@@ -122,15 +122,6 @@ function init(args: Args, params?: unknown): Promise<void> {
         }
 
         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).
-        // Any param mentioned here will show up in $_REQUEST when we call "AuthenticatePusher". Params passed here need
-        // to pass our inputRules to show up in the request.
-        if (params) {
-            socket.config.auth = {};
-            socket.config.auth.params = params;
-        }
 
         // Listen for connection errors and log them
         socket?.connection.bind('error', (error: EventCallbackError) => {
diff --git a/src/libs/PusherConnectionManager.ts b/src/libs/PusherConnectionManager.ts
index 69ffa8339f5..fbe6e3c5faf 100644
--- a/src/libs/PusherConnectionManager.ts
+++ b/src/libs/PusherConnectionManager.ts
@@ -13,7 +13,7 @@ function init() {
      * current valid token to generate the signed auth response
      * needed to subscribe to Pusher channels.
      */
-    registerCustomAuthorizer((channel) => ({
+    registerCustomAuthorizer?.((channel) => ({
         authorize: (socketId: string, callback: ChannelAuthorizationCallback) => {
             authenticatePusher(socketId, channel.name, callback);
         },
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 512019b6db6..da1c310f919 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -895,7 +895,7 @@ const reauthenticatePusher = throttle(
     {trailing: false},
 );
 
-function authenticatePusher(socketID: string, channelName: string, callback: ChannelAuthorizationCallback) {
+function authenticatePusher(socketID: string, channelName: string, callback?: ChannelAuthorizationCallback) {
     Log.info('[PusherAuthorizer] Attempting to authorize Pusher', false, {channelName});
 
     const params: AuthenticatePusherParams = {
@@ -935,6 +935,23 @@ function authenticatePusher(socketID: string, channelName: string, callback: Cha
         });
 }
 
+async function onAuthorizer(channelName: string, socketId: string) {
+    Log.info('[PusherAuthorizer] Attempting to authorize Pusher', false, {channelName});
+
+    const params: AuthenticatePusherParams = {
+        // eslint-disable-next-line @typescript-eslint/naming-convention
+        socket_id: socketId,
+        // eslint-disable-next-line @typescript-eslint/naming-convention
+        channel_name: channelName,
+        shouldRetry: false,
+        forceNetworkRequest: true,
+    };
+
+    const response = await API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params);
+
+    return response;
+}
+
 /**
  * Request a new validation link / magic code to unlink an unvalidated secondary login from a primary login
  */
@@ -1336,4 +1353,5 @@ export {
     validateUserAndGetAccessiblePolicies,
     isUserOnPrivateDomain,
     resetSMSDeliveryFailureStatus,
+    onAuthorizer,
 };

From 2a56b1c90c228a9ad50b04d6814924d98adf74d8 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Mon, 10 Feb 2025 16:06:25 +0100
Subject: [PATCH 02/16] Reuse existing authenticatePusher function

---
 src/libs/Pusher/pusher/index.native.ts | 10 ++-----
 src/libs/actions/Session/index.ts      | 36 ++++++++++----------------
 2 files changed, 15 insertions(+), 31 deletions(-)

diff --git a/src/libs/Pusher/pusher/index.native.ts b/src/libs/Pusher/pusher/index.native.ts
index ee617e2d589..66e0399bf44 100644
--- a/src/libs/Pusher/pusher/index.native.ts
+++ b/src/libs/Pusher/pusher/index.native.ts
@@ -7,7 +7,7 @@ import type {LiteralUnion, ValueOf} from 'type-fest';
 import Log from '@libs/Log';
 import TYPE from '@libs/Pusher/EventType';
 import type {SocketEventName} from '@libs/Pusher/library/types';
-import {onAuthorizer} from '@userActions/Session';
+import {authenticatePusher} from '@userActions/Session';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
@@ -114,13 +114,7 @@ function init(args: Args): Promise<void> {
                 callSocketEventCallbacks('state_change', {previous: previousState, current: currentState});
             },
             onError: (message: string, code: number) => callSocketEventCallbacks('error', {data: {code, message}}),
-            onAuthorizer: async (channelName: string, socketId: string) => {
-                const res = await onAuthorizer(channelName, socketId);
-                return {
-                    auth: res.auth,
-                    shared_secret: res.shared_secret,
-                };
-            },
+            onAuthorizer: (channelName: string, socketId: string) => authenticatePusher(socketId, channelName),
         });
         await socket.connect();
     }).then(resolveInitPromise);
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index da1c310f919..5d775752412 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -909,11 +909,11 @@ function authenticatePusher(socketID: string, channelName: string, callback?: Ch
 
     // We use makeRequestWithSideEffects here because we need to authorize to Pusher (an external service) each time a user connects to any channel.
     // eslint-disable-next-line rulesdir/no-api-side-effects-method
-    API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params)
+    return API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params)
         .then((response) => {
             if (response?.jsonCode === CONST.JSON_CODE.NOT_AUTHENTICATED) {
                 Log.hmmm('[PusherAuthorizer] Unable to authenticate Pusher because authToken is expired');
-                callback(new Error('Pusher failed to authenticate because authToken is expired'), {auth: ''});
+                callback?.(new Error('Pusher failed to authenticate because authToken is expired'), {auth: ''});
 
                 // Attempt to refresh the authToken then reconnect to Pusher
                 reauthenticatePusher();
@@ -922,36 +922,27 @@ function authenticatePusher(socketID: string, channelName: string, callback?: Ch
 
             if (response?.jsonCode !== CONST.JSON_CODE.SUCCESS) {
                 Log.hmmm('[PusherAuthorizer] Unable to authenticate Pusher for reason other than expired session');
-                callback(new Error(`Pusher failed to authenticate because code: ${response?.jsonCode} message: ${response?.message}`), {auth: ''});
+                callback?.(new Error(`Pusher failed to authenticate because code: ${response?.jsonCode} message: ${response?.message}`), {auth: ''});
                 return;
             }
 
             Log.info('[PusherAuthorizer] Pusher authenticated successfully', false, {channelName});
-            callback(null, response as ChannelAuthorizationData);
+            if (callback) {
+                callback(null, response as ChannelAuthorizationData);
+            } else {
+                return {
+                    auth: response.auth,
+                    // eslint-disable-next-line @typescript-eslint/naming-convention
+                    shared_secret: response.shared_secret,
+                };
+            }
         })
         .catch((error: unknown) => {
             Log.hmmm('[PusherAuthorizer] Unhandled error: ', {channelName, error});
-            callback(new Error('AuthenticatePusher request failed'), {auth: ''});
+            callback?.(new Error('AuthenticatePusher request failed'), {auth: ''});
         });
 }
 
-async function onAuthorizer(channelName: string, socketId: string) {
-    Log.info('[PusherAuthorizer] Attempting to authorize Pusher', false, {channelName});
-
-    const params: AuthenticatePusherParams = {
-        // eslint-disable-next-line @typescript-eslint/naming-convention
-        socket_id: socketId,
-        // eslint-disable-next-line @typescript-eslint/naming-convention
-        channel_name: channelName,
-        shouldRetry: false,
-        forceNetworkRequest: true,
-    };
-
-    const response = await API.makeRequestWithSideEffects(SIDE_EFFECT_REQUEST_COMMANDS.AUTHENTICATE_PUSHER, params);
-
-    return response;
-}
-
 /**
  * Request a new validation link / magic code to unlink an unvalidated secondary login from a primary login
  */
@@ -1353,5 +1344,4 @@ export {
     validateUserAndGetAccessiblePolicies,
     isUserOnPrivateDomain,
     resetSMSDeliveryFailureStatus,
-    onAuthorizer,
 };

From 879da1323d389bf352c8561a27c803d85ec8ece5 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Mon, 10 Feb 2025 16:39:04 +0100
Subject: [PATCH 03/16] Fix channel resubscribing

---
 src/CONST.ts                           |  4 ++++
 src/libs/Pusher/pusher/index.native.ts | 12 +++++++-----
 2 files changed, 11 insertions(+), 5 deletions(-)

diff --git a/src/CONST.ts b/src/CONST.ts
index 3f12dba5146..f8f65f67bd5 100755
--- a/src/CONST.ts
+++ b/src/CONST.ts
@@ -1595,6 +1595,10 @@ const CONST = {
             DISCONNECTED: 'DISCONNECTED',
             RECONNECTING: 'RECONNECTING',
         },
+        CHANNEL_STATUS: {
+            SUBSCRIBING: 'SUBSCRIBING',
+            SUBSCRIBED: 'SUBSCRIBED',
+        },
     },
 
     EMOJI_SPACER: 'SPACER',
diff --git a/src/libs/Pusher/pusher/index.native.ts b/src/libs/Pusher/pusher/index.native.ts
index 66e0399bf44..f5873842d00 100644
--- a/src/libs/Pusher/pusher/index.native.ts
+++ b/src/libs/Pusher/pusher/index.native.ts
@@ -76,6 +76,7 @@ let initPromise = new Promise<void>((resolve) => {
 });
 
 const eventsBoundToChannels = new Map<string, Map<PusherEventName, (eventData: EventData<string>) => void>>();
+const channels: Record<string, ValueOf<typeof CONST.PUSHER.CHANNEL_STATUS>> = {};
 
 /**
  * Trigger each of the socket event callbacks with the event information
@@ -224,9 +225,9 @@ function subscribe<EventName extends PusherEventName>(
                     }
 
                     Log.info('[Pusher] Attempting to subscribe to channel', false, {channelName, eventName});
-                    const channel = getChannel(channelName);
 
-                    if (!channel) {
+                    if (!channels[channelName]) {
+                        channels[channelName] = CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBING;
                         await socket.subscribe({
                             channelName,
                             onEvent: (event) => {
@@ -234,6 +235,7 @@ function subscribe<EventName extends PusherEventName>(
                                 callback?.(event.data);
                             },
                             onSubscriptionSucceeded: () => {
+                                channels[channelName] = CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBED;
                                 bindEventToChannel(channelName, eventName, eventCallback);
                                 resolve();
                                 // When subscribing for the first time we register a success callback that can be
@@ -243,6 +245,7 @@ function subscribe<EventName extends PusherEventName>(
                                 onResubscribe();
                             },
                             onSubscriptionError: (name: string, message: string) => {
+                                delete channels[channelName];
                                 Log.hmmm('[Pusher] Issue authenticating with Pusher during subscribe attempt.', {
                                     channelName,
                                     message,
@@ -281,6 +284,7 @@ function unsubscribe(channelName: string, eventName: PusherEventName = '') {
     } else {
         Log.info('[Pusher] Unsubscribing from channel', false, {channelName});
         eventsBoundToChannels.delete(channelName);
+        delete channels[channelName];
         socket?.unsubscribe({channelName});
     }
 }
@@ -293,9 +297,7 @@ function isAlreadySubscribing(channelName: string): boolean {
         return false;
     }
 
-    // const channel = getChannel(channelName);
-    // return channel ? channel.subscriptionPending : false;
-    return false;
+    return channels[channelName] === CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBING;
 }
 
 /**

From 5e95a4d0bfc0a262e8c5f93ec6da57d82895f4f6 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Mon, 10 Feb 2025 17:32:54 +0100
Subject: [PATCH 04/16] Update Pusher folder structure

---
 src/libs/API/index.ts                         |   2 +-
 .../Navigation/AppNavigator/AuthScreens.tsx   |   2 +-
 src/libs/Pusher/{pusher => }/index.native.ts  |  87 +++------------
 src/libs/Pusher/{pusher => }/index.ts         |  76 +++----------
 src/libs/Pusher/library/index.native.ts       |  10 --
 src/libs/Pusher/library/index.ts              |  10 --
 src/libs/Pusher/library/types.ts              |  10 --
 src/libs/Pusher/types.ts                      | 101 ++++++++++++++++++
 src/libs/PusherConnectionManager.ts           |   6 +-
 src/libs/PusherUtils.ts                       |   4 +-
 src/libs/actions/Report.ts                    |   2 +-
 src/libs/actions/Session/index.ts             |   2 +-
 src/libs/actions/User.ts                      |   2 +-
 tests/utils/PusherHelper.ts                   |   2 +-
 tests/utils/TestHelper.ts                     |   2 +-
 15 files changed, 146 insertions(+), 172 deletions(-)
 rename src/libs/Pusher/{pusher => }/index.native.ts (85%)
 rename src/libs/Pusher/{pusher => }/index.ts (88%)
 delete mode 100644 src/libs/Pusher/library/index.native.ts
 delete mode 100644 src/libs/Pusher/library/index.ts
 delete mode 100644 src/libs/Pusher/library/types.ts
 create mode 100644 src/libs/Pusher/types.ts

diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index 1723d5cd55a..3fa4ba64c37 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -5,7 +5,7 @@ import Log from '@libs/Log';
 import {HandleUnusedOptimisticID, Logging, Pagination, Reauthentication, RecheckConnection, SaveResponseInOnyx} from '@libs/Middleware';
 import {isOffline} from '@libs/Network/NetworkStore';
 import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue';
-import {getPusherSocketID} from '@libs/Pusher/pusher';
+import {getPusherSocketID} from '@libs/Pusher';
 import {processWithMiddleware, use} from '@libs/Request';
 import {getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests';
 import CONST from '@src/CONST';
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 9968251e226..8120770f52d 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -34,7 +34,7 @@ import type {AuthScreensParamList, CentralPaneName, CentralPaneScreensParamList}
 import {isOnboardingFlowName} from '@libs/NavigationUtils';
 import NetworkConnection from '@libs/NetworkConnection';
 import onyxSubscribe from '@libs/onyxSubscribe';
-import * as Pusher from '@libs/Pusher/pusher';
+import * as Pusher from '@libs/Pusher';
 import PusherConnectionManager from '@libs/PusherConnectionManager';
 import * as ReportUtils from '@libs/ReportUtils';
 import * as SearchQueryUtils from '@libs/SearchQueryUtils';
diff --git a/src/libs/Pusher/pusher/index.native.ts b/src/libs/Pusher/index.native.ts
similarity index 85%
rename from src/libs/Pusher/pusher/index.native.ts
rename to src/libs/Pusher/index.native.ts
index f5873842d00..00c60726c34 100644
--- a/src/libs/Pusher/pusher/index.native.ts
+++ b/src/libs/Pusher/index.native.ts
@@ -3,57 +3,25 @@ import {Pusher} from '@pusher/pusher-websocket-react-native';
 import isObject from 'lodash/isObject';
 import {InteractionManager} from 'react-native';
 import Onyx from 'react-native-onyx';
-import type {LiteralUnion, ValueOf} from 'type-fest';
+import type {ValueOf} from 'type-fest';
 import Log from '@libs/Log';
-import TYPE from '@libs/Pusher/EventType';
-import type {SocketEventName} from '@libs/Pusher/library/types';
 import {authenticatePusher} from '@userActions/Session';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
-import type DeepValueOf from '@src/types/utils/DeepValueOf';
-
-type States = {
-    previous: string;
-    current: string;
-};
-
-type Args = {
-    appKey: string;
-    cluster: string;
-    authEndpoint: string;
-};
-
-type UserIsTypingEvent = ReportUserIsTyping & {
-    userLogin?: string;
-};
-
-type UserIsLeavingRoomEvent = Record<string, boolean> & {
-    userLogin?: string;
-};
-
-type PingPongEvent = Record<string, string | number> & {
-    pingID: string;
-    pingTimestamp: number;
-};
-
-type PusherEventMap = {
-    [TYPE.USER_IS_TYPING]: UserIsTypingEvent;
-    [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent;
-    [TYPE.PONG]: PingPongEvent;
-};
-
-type EventData<EventName extends string> = {chunk?: string; id?: string; index?: number; final?: boolean} & (EventName extends keyof PusherEventMap
-    ? PusherEventMap[EventName]
-    : OnyxUpdatesFromServer);
-
-type EventCallbackError = {type?: ValueOf<typeof CONST.ERROR>; data: {code: number; message?: string}};
-
-type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean};
-
-type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void;
-
-type PusherEventName = LiteralUnion<DeepValueOf<typeof TYPE>, string>;
+import TYPE from './EventType';
+import type {
+    Args,
+    ChunkedDataEvents,
+    EventCallbackError,
+    EventData,
+    PingPongEvent,
+    PusherEventName,
+    SocketEventCallback,
+    SocketEventName,
+    States,
+    UserIsLeavingRoomEvent,
+    UserIsTypingEvent,
+} from './types';
 
 let shouldForceOffline = false;
 Onyx.connect({
@@ -338,15 +306,6 @@ function registerSocketEventCallback(cb: SocketEventCallback) {
     socketEventCallbacks.push(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.
- */
-// function registerCustomAuthorizer(authorizer: ChannelAuthorizerGenerator) {
-//     customAuthorizer = authorizer;
-// }
-
 /**
  * Disconnect from Pusher
  */
@@ -383,20 +342,6 @@ function getPusherSocketID(): string | undefined {
     return pusherSocketID;
 }
 
-export {
-    init,
-    subscribe,
-    unsubscribe,
-    getChannel,
-    isSubscribed,
-    isAlreadySubscribing,
-    sendEvent,
-    disconnect,
-    reconnect,
-    registerSocketEventCallback,
-    // registerCustomAuthorizer,
-    TYPE,
-    getPusherSocketID,
-};
+export {init, subscribe, unsubscribe, getChannel, isSubscribed, isAlreadySubscribing, sendEvent, disconnect, reconnect, registerSocketEventCallback, TYPE, getPusherSocketID};
 
 export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent};
diff --git a/src/libs/Pusher/pusher/index.ts b/src/libs/Pusher/index.ts
similarity index 88%
rename from src/libs/Pusher/pusher/index.ts
rename to src/libs/Pusher/index.ts
index 6ca05e78818..22cf7935231 100644
--- a/src/libs/Pusher/pusher/index.ts
+++ b/src/libs/Pusher/index.ts
@@ -1,68 +1,26 @@
 import isObject from 'lodash/isObject';
 import type {Channel, ChannelAuthorizerGenerator, Options} from 'pusher-js/with-encryption';
+import Pusher from 'pusher-js/with-encryption';
 import {InteractionManager} from 'react-native';
 import Onyx from 'react-native-onyx';
-import type {LiteralUnion, ValueOf} from 'type-fest';
 import Log from '@libs/Log';
-import TYPE from '@libs/Pusher/EventType';
-import Pusher from '@libs/Pusher/library';
-import type {SocketEventName} from '@libs/Pusher/library/types';
-import type CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
-import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
-import type DeepValueOf from '@src/types/utils/DeepValueOf';
-
-type States = {
-    previous: string;
-    current: string;
-};
-
-type Args = {
-    appKey: string;
-    cluster: string;
-    authEndpoint: string;
-};
-
-type UserIsTypingEvent = ReportUserIsTyping & {
-    userLogin?: string;
-};
-
-type UserIsLeavingRoomEvent = Record<string, boolean> & {
-    userLogin?: string;
-};
-
-type PingPongEvent = Record<string, string | number> & {
-    pingID: string;
-    pingTimestamp: number;
-};
-
-type PusherEventMap = {
-    [TYPE.USER_IS_TYPING]: UserIsTypingEvent;
-    [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent;
-    [TYPE.PONG]: PingPongEvent;
-};
-
-type EventData<EventName extends string> = {chunk?: string; id?: string; index?: number; final?: boolean} & (EventName extends keyof PusherEventMap
-    ? PusherEventMap[EventName]
-    : OnyxUpdatesFromServer);
-
-type EventCallbackError = {type: ValueOf<typeof CONST.ERROR>; data: {code: number; message?: string}};
-
-type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean};
-
-type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void;
-
-type PusherWithAuthParams = InstanceType<typeof Pusher> & {
-    config: {
-        auth?: {
-            params?: unknown;
-        };
-    };
-};
-
-type PusherEventName = LiteralUnion<DeepValueOf<typeof TYPE>, string>;
-
-type PusherSubscribtionErrorData = {type?: string; error?: string; status?: string};
+import TYPE from './EventType';
+import type {
+    Args,
+    ChunkedDataEvents,
+    EventCallbackError,
+    EventData,
+    PingPongEvent,
+    PusherEventName,
+    PusherSubscribtionErrorData,
+    PusherWithAuthParams,
+    SocketEventCallback,
+    SocketEventName,
+    States,
+    UserIsLeavingRoomEvent,
+    UserIsTypingEvent,
+} from './types';
 
 let shouldForceOffline = false;
 Onyx.connect({
diff --git a/src/libs/Pusher/library/index.native.ts b/src/libs/Pusher/library/index.native.ts
deleted file mode 100644
index 4f11506f10f..00000000000
--- a/src/libs/Pusher/library/index.native.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * 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 type 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
deleted file mode 100644
index 6a7104a1d2a..00000000000
--- a/src/libs/Pusher/library/index.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * 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
deleted file mode 100644
index 566fd8a7277..00000000000
--- a/src/libs/Pusher/library/types.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import type PusherClass from 'pusher-js/with-encryption';
-import type {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/types.ts b/src/libs/Pusher/types.ts
new file mode 100644
index 00000000000..e07b0186f71
--- /dev/null
+++ b/src/libs/Pusher/types.ts
@@ -0,0 +1,101 @@
+import type {PusherChannel} from '@pusher/pusher-websocket-react-native';
+import type PusherClass from 'pusher-js/with-encryption';
+import type {Channel, ChannelAuthorizerGenerator} from 'pusher-js/with-encryption';
+import type {LiteralUnion, ValueOf} from 'type-fest';
+import type CONST from '@src/CONST';
+import type {OnyxUpdatesFromServer, ReportUserIsTyping} from '@src/types/onyx';
+import type DeepValueOf from '@src/types/utils/DeepValueOf';
+import type TYPE from './EventType';
+
+type SocketEventName = LiteralUnion<'error' | 'connected' | 'disconnected' | 'state_change', string>;
+
+type States = {
+    previous: string;
+    current: string;
+};
+
+type Args = {
+    appKey: string;
+    cluster: string;
+    authEndpoint: string;
+};
+
+type UserIsTypingEvent = ReportUserIsTyping & {
+    userLogin?: string;
+};
+
+type UserIsLeavingRoomEvent = Record<string, boolean> & {
+    userLogin?: string;
+};
+
+type PingPongEvent = Record<string, string | number> & {
+    pingID: string;
+    pingTimestamp: number;
+};
+
+type PusherEventMap = {
+    [TYPE.USER_IS_TYPING]: UserIsTypingEvent;
+    [TYPE.USER_IS_LEAVING_ROOM]: UserIsLeavingRoomEvent;
+    [TYPE.PONG]: PingPongEvent;
+};
+
+type EventData<EventName extends string> = {chunk?: string; id?: string; index?: number; final?: boolean} & (EventName extends keyof PusherEventMap
+    ? PusherEventMap[EventName]
+    : OnyxUpdatesFromServer);
+
+type EventCallbackError = {type: ValueOf<typeof CONST.ERROR>; data: {code: number; message?: string}};
+
+type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean};
+
+type SocketEventCallback = (eventName: SocketEventName, data?: States | EventCallbackError) => void;
+
+type PusherWithAuthParams = InstanceType<typeof PusherClass> & {
+    config: {
+        auth?: {
+            params?: unknown;
+        };
+    };
+};
+
+type PusherEventName = LiteralUnion<DeepValueOf<typeof TYPE>, string>;
+
+type PusherSubscribtionErrorData = {type?: string; error?: string; status?: string};
+
+type PusherModule = {
+    init: (args: Args) => Promise<void>;
+    subscribe: <EventName extends PusherEventName>(
+        channelName: string,
+        eventName?: EventName,
+        eventCallback?: (data: EventData<EventName>) => void,
+        onResubscribe?: () => void,
+    ) => Promise<void>;
+    unsubscribe: (channelName: string, eventName?: PusherEventName) => void;
+    getChannel: (channelName: string) => Channel | PusherChannel | undefined;
+    isSubscribed: (channelName: string) => boolean;
+    isAlreadySubscribing: (channelName: string) => boolean;
+    sendEvent: <EventName extends PusherEventName>(channelName: string, eventName: EventName, payload: EventData<EventName>) => void;
+    disconnect: () => void;
+    reconnect: () => void;
+    registerSocketEventCallback: (cb: SocketEventCallback) => void;
+    registerCustomAuthorizer?: (authorizer: ChannelAuthorizerGenerator) => void;
+    getPusherSocketID: () => string | undefined;
+};
+
+export default PusherModule;
+
+export type {
+    SocketEventName,
+    States,
+    Args,
+    UserIsTypingEvent,
+    UserIsLeavingRoomEvent,
+    PingPongEvent,
+    PusherEventMap,
+    EventData,
+    EventCallbackError,
+    ChunkedDataEvents,
+    SocketEventCallback,
+    PusherWithAuthParams,
+    PusherEventName,
+    PusherSubscribtionErrorData,
+};
diff --git a/src/libs/PusherConnectionManager.ts b/src/libs/PusherConnectionManager.ts
index fbe6e3c5faf..ee31c892045 100644
--- a/src/libs/PusherConnectionManager.ts
+++ b/src/libs/PusherConnectionManager.ts
@@ -2,9 +2,9 @@ import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption';
 import CONST from '@src/CONST';
 import {authenticatePusher} from './actions/Session';
 import Log from './Log';
-import type {SocketEventName} from './Pusher/library/types';
-import {reconnect, registerCustomAuthorizer, registerSocketEventCallback} from './Pusher/pusher';
-import type {EventCallbackError, States} from './Pusher/pusher';
+import {reconnect, registerCustomAuthorizer, registerSocketEventCallback} from './Pusher';
+import type {EventCallbackError, States} from './Pusher';
+import type {SocketEventName} from './Pusher/types';
 
 function init() {
     /**
diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts
index 547aa06e770..50778160dbf 100644
--- a/src/libs/PusherUtils.ts
+++ b/src/libs/PusherUtils.ts
@@ -4,8 +4,8 @@ import CONST from '@src/CONST';
 import type {OnyxUpdatesFromServer} from '@src/types/onyx';
 import Log from './Log';
 import NetworkConnection from './NetworkConnection';
-import {subscribe} from './Pusher/pusher';
-import type {PingPongEvent} from './Pusher/pusher';
+import {subscribe} from './Pusher';
+import type {PingPongEvent} from './Pusher';
 
 type Callback = (data: OnyxUpdate[]) => Promise<void>;
 
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index bc051f1472b..9de0c193124 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -79,7 +79,7 @@ import * as PhoneNumber from '@libs/PhoneNumber';
 import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
 import {extractPolicyIDFromPath, getPolicy} from '@libs/PolicyUtils';
 import processReportIDDeeplink from '@libs/processReportIDDeeplink';
-import * as Pusher from '@libs/Pusher/pusher';
+import * as Pusher from '@libs/Pusher';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import type {OptimisticAddCommentReportAction, OptimisticChatReport} from '@libs/ReportUtils';
 import {
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 5d775752412..60c169df433 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -36,7 +36,7 @@ import * as MainQueue from '@libs/Network/MainQueue';
 import * as NetworkStore from '@libs/Network/NetworkStore';
 import {getCurrentUserEmail} from '@libs/Network/NetworkStore';
 import NetworkConnection from '@libs/NetworkConnection';
-import * as Pusher from '@libs/Pusher/pusher';
+import * as Pusher from '@libs/Pusher';
 import {getReportIDFromLink, parseReportRouteParams as parseReportRouteParamsReportUtils} from '@libs/ReportUtils';
 import * as SessionUtils from '@libs/SessionUtils';
 import {clearSoundAssetsCache} from '@libs/Sound';
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index ffcbc813477..8ed2506336b 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -32,7 +32,7 @@ import {isOffline} from '@libs/Network/NetworkStore';
 import * as SequentialQueue from '@libs/Network/SequentialQueue';
 import * as NumberUtils from '@libs/NumberUtils';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
-import * as Pusher from '@libs/Pusher/pusher';
+import * as Pusher from '@libs/Pusher';
 import PusherUtils from '@libs/PusherUtils';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import * as ReportUtils from '@libs/ReportUtils';
diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts
index 8547c25b123..3fe49b979c3 100644
--- a/tests/utils/PusherHelper.ts
+++ b/tests/utils/PusherHelper.ts
@@ -1,6 +1,6 @@
+import * as Pusher from '@libs/Pusher';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
-import * as Pusher from '@src/libs/Pusher/pusher';
 import PusherConnectionManager from '@src/libs/PusherConnectionManager';
 import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
 import asMutable from '@src/types/utils/asMutable';
diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts
index f3814d4b91c..42763a3741b 100644
--- a/tests/utils/TestHelper.ts
+++ b/tests/utils/TestHelper.ts
@@ -5,7 +5,7 @@ import Onyx from 'react-native-onyx';
 import type {ConnectOptions} from 'react-native-onyx/dist/types';
 import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types';
 import * as Localize from '@libs/Localize';
-import * as Pusher from '@libs/Pusher/pusher';
+import * as Pusher from '@libs/Pusher';
 import PusherConnectionManager from '@libs/PusherConnectionManager';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';

From 0efbb8f9292e6514b3b22f2fcbc79965a8e80d57 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Tue, 11 Feb 2025 10:29:12 +0100
Subject: [PATCH 05/16] TS fixes

---
 src/libs/Pusher/index.native.ts | 10 +++++-----
 src/libs/Pusher/types.ts        |  2 +-
 2 files changed, 6 insertions(+), 6 deletions(-)

diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts
index 00c60726c34..2b4dd15efc3 100644
--- a/src/libs/Pusher/index.native.ts
+++ b/src/libs/Pusher/index.native.ts
@@ -1,4 +1,4 @@
-import type {PusherChannel} from '@pusher/pusher-websocket-react-native';
+import type {PusherAuthorizerResult, PusherChannel} from '@pusher/pusher-websocket-react-native';
 import {Pusher} from '@pusher/pusher-websocket-react-native';
 import isObject from 'lodash/isObject';
 import {InteractionManager} from 'react-native';
@@ -43,7 +43,7 @@ let initPromise = new Promise<void>((resolve) => {
     resolveInitPromise = resolve;
 });
 
-const eventsBoundToChannels = new Map<string, Map<PusherEventName, (eventData: EventData<string>) => void>>();
+const eventsBoundToChannels = new Map<string, Map<PusherEventName, (eventData: EventData<PusherEventName>) => void>>();
 const channels: Record<string, ValueOf<typeof CONST.PUSHER.CHANNEL_STATUS>> = {};
 
 /**
@@ -82,8 +82,8 @@ function init(args: Args): Promise<void> {
                 }
                 callSocketEventCallbacks('state_change', {previous: previousState, current: currentState});
             },
-            onError: (message: string, code: number) => callSocketEventCallbacks('error', {data: {code, message}}),
-            onAuthorizer: (channelName: string, socketId: string) => authenticatePusher(socketId, channelName),
+            onError: (message: string) => callSocketEventCallbacks('error', {data: {message}}),
+            onAuthorizer: (channelName: string, socketId: string) => authenticatePusher(socketId, channelName) as Promise<PusherAuthorizerResult>,
         });
         await socket.connect();
     }).then(resolveInitPromise);
@@ -200,7 +200,7 @@ function subscribe<EventName extends PusherEventName>(
                             channelName,
                             onEvent: (event) => {
                                 const callback = eventsBoundToChannels.get(event.channelName)?.get(event.eventName);
-                                callback?.(event.data);
+                                callback?.(event.data as EventData<PusherEventName>);
                             },
                             onSubscriptionSucceeded: () => {
                                 channels[channelName] = CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBED;
diff --git a/src/libs/Pusher/types.ts b/src/libs/Pusher/types.ts
index e07b0186f71..88348e1c4ba 100644
--- a/src/libs/Pusher/types.ts
+++ b/src/libs/Pusher/types.ts
@@ -43,7 +43,7 @@ type EventData<EventName extends string> = {chunk?: string; id?: string; index?:
     ? PusherEventMap[EventName]
     : OnyxUpdatesFromServer);
 
-type EventCallbackError = {type: ValueOf<typeof CONST.ERROR>; data: {code: number; message?: string}};
+type EventCallbackError = {type?: ValueOf<typeof CONST.ERROR>; data: {code?: number; message?: string}};
 
 type ChunkedDataEvents = {chunks: unknown[]; receivedFinal: boolean};
 

From 8385b859b8da3b6499c166eb1d579504e7225ee7 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Tue, 11 Feb 2025 11:27:42 +0100
Subject: [PATCH 06/16] Code improvements

---
 src/libs/Pusher/index.native.ts | 13 ++++++-------
 1 file changed, 6 insertions(+), 7 deletions(-)

diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts
index 2b4dd15efc3..93da14987e2 100644
--- a/src/libs/Pusher/index.native.ts
+++ b/src/libs/Pusher/index.native.ts
@@ -58,15 +58,14 @@ function callSocketEventCallbacks(eventName: SocketEventName, data?: EventCallba
  * @returns resolves when Pusher has connected
  */
 function init(args: Args): Promise<void> {
-    // eslint-disable-next-line @lwc/lwc/no-async-await
-    return new Promise<void>(async (resolve) => {
+    return new Promise<void>((resolve) => {
         if (socket) {
             resolve();
             return;
         }
 
         socket = Pusher.getInstance();
-        await socket.init({
+        socket.init({
             apiKey: args.appKey,
             cluster: args.cluster,
             onConnectionStateChange: (currentState: string, previousState: string) => {
@@ -85,7 +84,7 @@ function init(args: Args): Promise<void> {
             onError: (message: string) => callSocketEventCallbacks('error', {data: {message}}),
             onAuthorizer: (channelName: string, socketId: string) => authenticatePusher(socketId, channelName) as Promise<PusherAuthorizerResult>,
         });
-        await socket.connect();
+        socket.connect();
     }).then(resolveInitPromise);
 }
 
@@ -170,7 +169,7 @@ function bindEventToChannel<EventName extends PusherEventName>(channel: string,
         eventsBoundToChannels.set(channel, new Map());
     }
 
-    eventsBoundToChannels.get(channel)?.set(eventName, callback);
+    eventsBoundToChannels.get(channel)?.set(eventName, callback as (eventData: EventData<PusherEventName>) => void);
 }
 
 /**
@@ -185,7 +184,7 @@ function subscribe<EventName extends PusherEventName>(
     return initPromise.then(
         () =>
             new Promise((resolve, reject) => {
-                InteractionManager.runAfterInteractions(async () => {
+                InteractionManager.runAfterInteractions(() => {
                     // We cannot call subscribe() before init(). Prevent any attempt to do this on dev.
                     if (!socket) {
                         throw new Error(`[Pusher] instance not found. Pusher.subscribe()
@@ -196,7 +195,7 @@ function subscribe<EventName extends PusherEventName>(
 
                     if (!channels[channelName]) {
                         channels[channelName] = CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBING;
-                        await socket.subscribe({
+                        socket.subscribe({
                             channelName,
                             onEvent: (event) => {
                                 const callback = eventsBoundToChannels.get(event.channelName)?.get(event.eventName);

From 0a6be56796a3b9614c0b48ad3ded30eef6ab7a88 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Tue, 11 Feb 2025 11:54:24 +0100
Subject: [PATCH 07/16] Lint fixes

---
 tests/utils/PusherHelper.ts | 19 ++++++++++---------
 tests/utils/TestHelper.ts   |  8 ++++----
 2 files changed, 14 insertions(+), 13 deletions(-)

diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts
index 3fe49b979c3..553ded9010f 100644
--- a/tests/utils/PusherHelper.ts
+++ b/tests/utils/PusherHelper.ts
@@ -1,9 +1,8 @@
-import * as Pusher from '@libs/Pusher';
+import {getChannel, init, TYPE, unsubscribe} from '@libs/Pusher';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
 import PusherConnectionManager from '@src/libs/PusherConnectionManager';
 import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
-import asMutable from '@src/types/utils/asMutable';
 
 const CHANNEL_NAME = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}1${CONFIG.PUSHER.SUFFIX}`;
 
@@ -12,12 +11,14 @@ function setup() {
     // channel already in a subscribed state. These methods are normally used to prevent
     // duplicated subscriptions, but we don't need them for this test so forcing them to
     // return false will make the testing less complex.
-    asMutable(Pusher).isSubscribed = jest.fn().mockReturnValue(false);
-    asMutable(Pusher).isAlreadySubscribing = jest.fn().mockReturnValue(false);
+    jest.mock('@libs/Pusher', () => ({
+        isSubscribed: jest.fn().mockReturnValue(false),
+        isAlreadySubscribing: jest.fn().mockReturnValue(false),
+    }));
 
     // Connect to Pusher
     PusherConnectionManager.init();
-    Pusher.init({
+    init({
         appKey: CONFIG.PUSHER.APP_KEY,
         cluster: CONFIG.PUSHER.CLUSTER,
         authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`,
@@ -27,14 +28,14 @@ function setup() {
 }
 
 function emitOnyxUpdate(args: OnyxServerUpdate[]) {
-    const channel = Pusher.getChannel(CHANNEL_NAME);
-    channel?.emit(Pusher.TYPE.MULTIPLE_EVENTS, {
+    const channel = getChannel(CHANNEL_NAME);
+    channel?.emit(TYPE.MULTIPLE_EVENTS, {
         type: 'pusher',
         lastUpdateID: null,
         previousUpdateID: null,
         updates: [
             {
-                eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
+                eventType: TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
                 data: args,
             },
         ],
@@ -44,7 +45,7 @@ function emitOnyxUpdate(args: OnyxServerUpdate[]) {
 function teardown() {
     // Unsubscribe from account channel after each test since we subscribe in the function
     // subscribeToUserEvents and we don't want duplicate event subscriptions.
-    Pusher.unsubscribe(CHANNEL_NAME);
+    unsubscribe(CHANNEL_NAME);
 }
 
 export default {
diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts
index 42763a3741b..edc06433e06 100644
--- a/tests/utils/TestHelper.ts
+++ b/tests/utils/TestHelper.ts
@@ -4,8 +4,8 @@ import {Linking} from 'react-native';
 import Onyx from 'react-native-onyx';
 import type {ConnectOptions} from 'react-native-onyx/dist/types';
 import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types';
-import * as Localize from '@libs/Localize';
-import * as Pusher from '@libs/Pusher';
+import {translateLocal} from '@libs/Localize';
+import {init} from '@libs/Pusher';
 import PusherConnectionManager from '@libs/PusherConnectionManager';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
@@ -47,7 +47,7 @@ function setupApp() {
 
         // Connect to Pusher
         PusherConnectionManager.init();
-        Pusher.init({
+        init({
             appKey: CONFIG.PUSHER.APP_KEY,
             cluster: CONFIG.PUSHER.CLUSTER,
             authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`,
@@ -330,7 +330,7 @@ function assertFormDataMatchesObject(obj: Report, formData?: FormData) {
 }
 
 async function navigateToSidebarOption(index: number): Promise<void> {
-    const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat');
+    const hintText = translateLocal('accessibilityHints.navigatesToChat');
     const optionRow = screen.queryAllByAccessibilityHint(hintText).at(index);
     if (!optionRow) {
         return;

From 625df9ac8b85aba34bc4a692f00227b4a1d342f9 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 12 Feb 2025 12:27:42 +0100
Subject: [PATCH 08/16] Add pusher-websocket-react-native mock to fix tests

---
 .../pusher-websocket-react-native/index.ts    | 72 +++++++++++++++++++
 src/libs/Pusher/index.native.ts               | 11 ++-
 tests/utils/PusherHelper.ts                   | 19 +++--
 3 files changed, 90 insertions(+), 12 deletions(-)
 create mode 100644 __mocks__/@pusher/pusher-websocket-react-native/index.ts

diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
new file mode 100644
index 00000000000..093cf7c0ece
--- /dev/null
+++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
@@ -0,0 +1,72 @@
+import type {PusherEvent} from '@pusher/pusher-websocket-react-native';
+import {EventEmitter} from 'events';
+import CONST from '@src/CONST';
+
+type OnSubscriptionSucceeded = () => void;
+type OnEvent = (event: PusherEvent) => void;
+
+type ChannelCallbacks = {
+    onSubscriptionSucceeded: OnSubscriptionSucceeded;
+    onEvent: OnEvent;
+};
+
+class MockPusher extends EventEmitter {
+    static instance: MockPusher | null = null;
+
+    channels = new Map<string, ChannelCallbacks>();
+
+    socketId = 'mock-socket-id';
+
+    connectionState = 'DISCONNECTED';
+
+    static getInstance() {
+        if (!MockPusher.instance) {
+            MockPusher.instance = new MockPusher();
+        }
+        return MockPusher.instance;
+    }
+
+    init({onConnectionStateChange}: {onConnectionStateChange: (currentState: string, previousState: string) => void}) {
+        onConnectionStateChange(CONST.PUSHER.STATE.CONNECTED, CONST.PUSHER.STATE.DISCONNECTED);
+        return Promise.resolve();
+    }
+
+    connect() {
+        this.connectionState = CONST.PUSHER.STATE.CONNECTED;
+        return Promise.resolve();
+    }
+
+    disconnect() {
+        this.connectionState = CONST.PUSHER.STATE.DISCONNECTED;
+        return Promise.resolve();
+    }
+
+    subscribe({channelName, onEvent, onSubscriptionSucceeded}: {channelName: string; onEvent: OnEvent; onSubscriptionSucceeded: OnSubscriptionSucceeded}) {
+        if (!this.channels.has(channelName)) {
+            this.channels.set(channelName, {onEvent, onSubscriptionSucceeded});
+            onSubscriptionSucceeded();
+        }
+        return Promise.resolve();
+    }
+
+    unsubscribe({channelName}: {channelName: string}) {
+        this.channels.delete(channelName);
+    }
+
+    trigger({channelName, eventName, data}: PusherEvent) {
+        this.channels.get(channelName)?.onSubscriptionSucceeded();
+        this.channels.get(channelName)?.onEvent({channelName, eventName, data: JSON.stringify(data)});
+    }
+
+    getChannel(channelName: string) {
+        return this.channels.get(channelName);
+    }
+
+    // eslint-disable-next-line @typescript-eslint/require-await,@lwc/lwc/no-async-await
+    async getSocketId() {
+        return this.socketId;
+    }
+}
+
+// eslint-disable-next-line import/prefer-default-export
+export {MockPusher as Pusher};
diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts
index 93da14987e2..c95bf9e7691 100644
--- a/src/libs/Pusher/index.native.ts
+++ b/src/libs/Pusher/index.native.ts
@@ -275,8 +275,7 @@ function isSubscribed(channelName: string): boolean {
         return false;
     }
 
-    const channel = getChannel(channelName);
-    return !!channel;
+    return channels[channelName] === CONST.PUSHER.CHANNEL_STATUS.SUBSCRIBED;
 }
 
 /**
@@ -341,6 +340,14 @@ function getPusherSocketID(): string | undefined {
     return pusherSocketID;
 }
 
+if (window) {
+    /**
+     * Pusher socket for debugging purposes
+     */
+    // @ts-expect-error type mismatch to be fixed
+    window.getPusherInstance = () => socket;
+}
+
 export {init, subscribe, unsubscribe, getChannel, isSubscribed, isAlreadySubscribing, sendEvent, disconnect, reconnect, registerSocketEventCallback, TYPE, getPusherSocketID};
 
 export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent};
diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts
index 553ded9010f..5590acca75f 100644
--- a/tests/utils/PusherHelper.ts
+++ b/tests/utils/PusherHelper.ts
@@ -1,8 +1,10 @@
-import {getChannel, init, TYPE, unsubscribe} from '@libs/Pusher';
+import {init, sendEvent, TYPE, unsubscribe} from '@libs/Pusher';
+import * as Pusher from '@libs/Pusher';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
 import PusherConnectionManager from '@src/libs/PusherConnectionManager';
 import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
+import asMutable from '@src/types/utils/asMutable';
 
 const CHANNEL_NAME = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}1${CONFIG.PUSHER.SUFFIX}`;
 
@@ -11,10 +13,8 @@ function setup() {
     // channel already in a subscribed state. These methods are normally used to prevent
     // duplicated subscriptions, but we don't need them for this test so forcing them to
     // return false will make the testing less complex.
-    jest.mock('@libs/Pusher', () => ({
-        isSubscribed: jest.fn().mockReturnValue(false),
-        isAlreadySubscribing: jest.fn().mockReturnValue(false),
-    }));
+    asMutable(Pusher).isSubscribed = jest.fn().mockReturnValue(false);
+    asMutable(Pusher).isAlreadySubscribing = jest.fn().mockReturnValue(false);
 
     // Connect to Pusher
     PusherConnectionManager.init();
@@ -24,15 +24,14 @@ function setup() {
         authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`,
     });
 
-    window.getPusherInstance()?.connection.emit('connected');
+    window.getPusherInstance()?.connection?.emit('connected');
 }
 
 function emitOnyxUpdate(args: OnyxServerUpdate[]) {
-    const channel = getChannel(CHANNEL_NAME);
-    channel?.emit(TYPE.MULTIPLE_EVENTS, {
+    sendEvent(CHANNEL_NAME, TYPE.MULTIPLE_EVENTS, {
         type: 'pusher',
-        lastUpdateID: null,
-        previousUpdateID: null,
+        lastUpdateID: 0,
+        previousUpdateID: 0,
         updates: [
             {
                 eventType: TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,

From 5cd43fa970e1b738535f06b4d6d2b5ff0b6020c1 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 12 Feb 2025 14:36:21 +0100
Subject: [PATCH 09/16] Code clean-up

---
 .../pusher-websocket-react-native/index.ts    | 39 ++++++++++++-------
 tests/utils/PusherHelper.ts                   |  6 +--
 2 files changed, 27 insertions(+), 18 deletions(-)

diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
index 093cf7c0ece..e7b4ccad34e 100644
--- a/__mocks__/@pusher/pusher-websocket-react-native/index.ts
+++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
@@ -3,6 +3,7 @@ import {EventEmitter} from 'events';
 import CONST from '@src/CONST';
 
 type OnSubscriptionSucceeded = () => void;
+
 type OnEvent = (event: PusherEvent) => void;
 
 type ChannelCallbacks = {
@@ -10,23 +11,35 @@ type ChannelCallbacks = {
     onEvent: OnEvent;
 };
 
-class MockPusher extends EventEmitter {
-    static instance: MockPusher | null = null;
+type InitProps = {
+    onConnectionStateChange: (currentState: string, previousState: string) => void;
+};
+
+type SubscribeProps = {
+    channelName: string;
+    onEvent: OnEvent;
+    onSubscriptionSucceeded: OnSubscriptionSucceeded;
+};
+
+type UnsubscribeProps = {channelName: string};
+
+class MockedPusher extends EventEmitter {
+    static instance: MockedPusher | null = null;
 
     channels = new Map<string, ChannelCallbacks>();
 
     socketId = 'mock-socket-id';
 
-    connectionState = 'DISCONNECTED';
+    connectionState: string = CONST.PUSHER.STATE.DISCONNECTED;
 
     static getInstance() {
-        if (!MockPusher.instance) {
-            MockPusher.instance = new MockPusher();
+        if (!MockedPusher.instance) {
+            MockedPusher.instance = new MockedPusher();
         }
-        return MockPusher.instance;
+        return MockedPusher.instance;
     }
 
-    init({onConnectionStateChange}: {onConnectionStateChange: (currentState: string, previousState: string) => void}) {
+    init({onConnectionStateChange}: InitProps) {
         onConnectionStateChange(CONST.PUSHER.STATE.CONNECTED, CONST.PUSHER.STATE.DISCONNECTED);
         return Promise.resolve();
     }
@@ -41,7 +54,7 @@ class MockPusher extends EventEmitter {
         return Promise.resolve();
     }
 
-    subscribe({channelName, onEvent, onSubscriptionSucceeded}: {channelName: string; onEvent: OnEvent; onSubscriptionSucceeded: OnSubscriptionSucceeded}) {
+    subscribe({channelName, onEvent, onSubscriptionSucceeded}: SubscribeProps) {
         if (!this.channels.has(channelName)) {
             this.channels.set(channelName, {onEvent, onSubscriptionSucceeded});
             onSubscriptionSucceeded();
@@ -49,12 +62,11 @@ class MockPusher extends EventEmitter {
         return Promise.resolve();
     }
 
-    unsubscribe({channelName}: {channelName: string}) {
+    unsubscribe({channelName}: UnsubscribeProps) {
         this.channels.delete(channelName);
     }
 
     trigger({channelName, eventName, data}: PusherEvent) {
-        this.channels.get(channelName)?.onSubscriptionSucceeded();
         this.channels.get(channelName)?.onEvent({channelName, eventName, data: JSON.stringify(data)});
     }
 
@@ -62,11 +74,10 @@ class MockPusher extends EventEmitter {
         return this.channels.get(channelName);
     }
 
-    // eslint-disable-next-line @typescript-eslint/require-await,@lwc/lwc/no-async-await
-    async getSocketId() {
-        return this.socketId;
+    getSocketId() {
+        return Promise.resolve(this.socketId);
     }
 }
 
 // eslint-disable-next-line import/prefer-default-export
-export {MockPusher as Pusher};
+export {MockedPusher as Pusher};
diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts
index 5590acca75f..3ffc7e5fb74 100644
--- a/tests/utils/PusherHelper.ts
+++ b/tests/utils/PusherHelper.ts
@@ -1,10 +1,8 @@
 import {init, sendEvent, TYPE, unsubscribe} from '@libs/Pusher';
-import * as Pusher from '@libs/Pusher';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
 import PusherConnectionManager from '@src/libs/PusherConnectionManager';
 import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer';
-import asMutable from '@src/types/utils/asMutable';
 
 const CHANNEL_NAME = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}1${CONFIG.PUSHER.SUFFIX}`;
 
@@ -13,8 +11,8 @@ function setup() {
     // channel already in a subscribed state. These methods are normally used to prevent
     // duplicated subscriptions, but we don't need them for this test so forcing them to
     // return false will make the testing less complex.
-    asMutable(Pusher).isSubscribed = jest.fn().mockReturnValue(false);
-    asMutable(Pusher).isAlreadySubscribing = jest.fn().mockReturnValue(false);
+    jest.spyOn(require('@libs/Pusher'), 'isSubscribed').mockReturnValue(false);
+    jest.spyOn(require('@libs/Pusher'), 'isAlreadySubscribing').mockReturnValue(false);
 
     // Connect to Pusher
     PusherConnectionManager.init();

From ea403e036d2d9bea0e331a7c916368bf0442e42f Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 12 Feb 2025 15:04:45 +0100
Subject: [PATCH 10/16] Minor fixes

---
 __mocks__/@pusher/pusher-websocket-react-native/index.ts | 4 ++--
 src/libs/Pusher/index.native.ts                          | 5 +++--
 src/types/modules/pusher.d.ts                            | 3 ++-
 tests/utils/PusherHelper.ts                              | 5 ++++-
 4 files changed, 11 insertions(+), 6 deletions(-)

diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
index e7b4ccad34e..dd0ea1592de 100644
--- a/__mocks__/@pusher/pusher-websocket-react-native/index.ts
+++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
@@ -1,5 +1,4 @@
 import type {PusherEvent} from '@pusher/pusher-websocket-react-native';
-import {EventEmitter} from 'events';
 import CONST from '@src/CONST';
 
 type OnSubscriptionSucceeded = () => void;
@@ -23,7 +22,7 @@ type SubscribeProps = {
 
 type UnsubscribeProps = {channelName: string};
 
-class MockedPusher extends EventEmitter {
+class MockedPusher {
     static instance: MockedPusher | null = null;
 
     channels = new Map<string, ChannelCallbacks>();
@@ -51,6 +50,7 @@ class MockedPusher extends EventEmitter {
 
     disconnect() {
         this.connectionState = CONST.PUSHER.STATE.DISCONNECTED;
+        this.channels.clear();
         return Promise.resolve();
     }
 
diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts
index c95bf9e7691..4348e81964f 100644
--- a/src/libs/Pusher/index.native.ts
+++ b/src/libs/Pusher/index.native.ts
@@ -44,7 +44,7 @@ let initPromise = new Promise<void>((resolve) => {
 });
 
 const eventsBoundToChannels = new Map<string, Map<PusherEventName, (eventData: EventData<PusherEventName>) => void>>();
-const channels: Record<string, ValueOf<typeof CONST.PUSHER.CHANNEL_STATUS>> = {};
+let channels: Record<string, ValueOf<typeof CONST.PUSHER.CHANNEL_STATUS>> = {};
 
 /**
  * Trigger each of the socket event callbacks with the event information
@@ -246,6 +246,7 @@ function unsubscribe(channelName: string, eventName: PusherEventName = '') {
         if (eventsBoundToChannels.get(channelName)?.size === 0) {
             Log.info(`[Pusher] After unbinding ${eventName} from channel ${channelName}, no other events were bound to that channel. Unsubscribing...`, false);
             eventsBoundToChannels.delete(channelName);
+            delete channels[channelName];
             socket?.unsubscribe({channelName});
         }
     } else {
@@ -316,6 +317,7 @@ function disconnect() {
     socket.disconnect();
     socket = null;
     pusherSocketID = '';
+    channels = {};
     eventsBoundToChannels.clear();
     initPromise = new Promise((resolve) => {
         resolveInitPromise = resolve;
@@ -344,7 +346,6 @@ if (window) {
     /**
      * Pusher socket for debugging purposes
      */
-    // @ts-expect-error type mismatch to be fixed
     window.getPusherInstance = () => socket;
 }
 
diff --git a/src/types/modules/pusher.d.ts b/src/types/modules/pusher.d.ts
index ffcf7744773..e0070ac2878 100644
--- a/src/types/modules/pusher.d.ts
+++ b/src/types/modules/pusher.d.ts
@@ -1,9 +1,10 @@
+import type {Pusher as MobilePusher} from '@pusher/pusher-websocket-react-native';
 import type Pusher from 'pusher-js/types/src/core/pusher';
 
 declare global {
     // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
     interface Window {
-        getPusherInstance: () => Pusher | null;
+        getPusherInstance: () => Pusher | MobilePusher | null;
     }
 
     // eslint-disable-next-line @typescript-eslint/consistent-type-definitions
diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts
index 3ffc7e5fb74..43e5e30f54a 100644
--- a/tests/utils/PusherHelper.ts
+++ b/tests/utils/PusherHelper.ts
@@ -22,7 +22,10 @@ function setup() {
         authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`,
     });
 
-    window.getPusherInstance()?.connection?.emit('connected');
+    const pusher = window.getPusherInstance();
+    if (pusher && 'connection' in pusher) {
+        pusher.connection?.emit('connected');
+    }
 }
 
 function emitOnyxUpdate(args: OnyxServerUpdate[]) {

From 7cf37cd5f65afb40aff153d7645bdff50e149663 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 12 Feb 2025 15:07:00 +0100
Subject: [PATCH 11/16] Remove unnecessary mock

---
 __mocks__/pusher-js/react-native.ts | 9 ---------
 1 file changed, 9 deletions(-)
 delete mode 100644 __mocks__/pusher-js/react-native.ts

diff --git a/__mocks__/pusher-js/react-native.ts b/__mocks__/pusher-js/react-native.ts
deleted file mode 100644
index 1edec34ffb1..00000000000
--- a/__mocks__/pusher-js/react-native.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import {PusherMock} from 'pusher-js-mock';
-
-class PusherMockWithDisconnect extends PusherMock {
-    disconnect() {
-        return jest.fn();
-    }
-}
-
-export default PusherMockWithDisconnect;

From 863a59a6d702a124a7b880b6068c2744c3d3b3c4 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 12 Feb 2025 15:18:34 +0100
Subject: [PATCH 12/16] Remove unnecessary pusher-js-mock lib

---
 package-lock.json | 9 ---------
 package.json      | 1 -
 2 files changed, 10 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index af3257de8f0..3158e267d2a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -255,7 +255,6 @@
         "peggy": "^4.0.3",
         "portfinder": "^1.0.28",
         "prettier": "^2.8.8",
-        "pusher-js-mock": "^0.3.3",
         "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020",
         "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020",
         "react-is": "^18.3.1",
@@ -31460,14 +31459,6 @@
         "tweetnacl": "^1.0.3"
       }
     },
-    "node_modules/pusher-js-mock": {
-      "version": "0.3.8",
-      "dev": true,
-      "license": "MIT",
-      "engines": {
-        "node": ">=4.2.4"
-      }
-    },
     "node_modules/qrcode": {
       "version": "1.5.4",
       "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz",
diff --git a/package.json b/package.json
index a5286e115de..1256b13a7b2 100644
--- a/package.json
+++ b/package.json
@@ -322,7 +322,6 @@
     "peggy": "^4.0.3",
     "portfinder": "^1.0.28",
     "prettier": "^2.8.8",
-    "pusher-js-mock": "^0.3.3",
     "react-compiler-healthcheck": "^19.0.0-beta-8a03594-20241020",
     "react-compiler-runtime": "^19.0.0-beta-8a03594-20241020",
     "react-is": "^18.3.1",

From dbd04210fc8b59441dcf9b2ef1d42deecbe0f529 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 12 Feb 2025 15:51:32 +0100
Subject: [PATCH 13/16] Update Pusher module export since now it's
 platform-specific

---
 src/libs/API/index.ts                         |  4 +--
 .../Navigation/AppNavigator/AuthScreens.tsx   |  2 +-
 src/libs/Pusher/index.native.ts               | 34 ++++++++++---------
 src/libs/Pusher/index.ts                      |  8 ++---
 src/libs/Pusher/types.ts                      |  1 +
 src/libs/PusherConnectionManager.ts           | 11 +++---
 src/libs/PusherUtils.ts                       |  6 ++--
 src/libs/actions/Report.ts                    | 11 +++---
 src/libs/actions/Session/index.ts             |  2 +-
 src/libs/actions/User.ts                      |  5 +--
 tests/utils/PusherHelper.ts                   | 14 ++++----
 tests/utils/TestHelper.ts                     |  4 +--
 12 files changed, 52 insertions(+), 50 deletions(-)

diff --git a/src/libs/API/index.ts b/src/libs/API/index.ts
index 3fa4ba64c37..92f23260cc1 100644
--- a/src/libs/API/index.ts
+++ b/src/libs/API/index.ts
@@ -5,7 +5,7 @@ import Log from '@libs/Log';
 import {HandleUnusedOptimisticID, Logging, Pagination, Reauthentication, RecheckConnection, SaveResponseInOnyx} from '@libs/Middleware';
 import {isOffline} from '@libs/Network/NetworkStore';
 import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue';
-import {getPusherSocketID} from '@libs/Pusher';
+import Pusher from '@libs/Pusher';
 import {processWithMiddleware, use} from '@libs/Request';
 import {getLength as getPersistedRequestsLength} from '@userActions/PersistedRequests';
 import CONST from '@src/CONST';
@@ -70,7 +70,7 @@ function prepareRequest<TCommand extends ApiCommand>(
 
         // We send the pusherSocketID with all write requests so that the api can include it in push events to prevent Pusher from sending the events to the requesting client. The push event
         // is sent back to the requesting client in the response data instead, which prevents a replay effect in the UI. See https://github.com/Expensify/App/issues/12775.
-        pusherSocketID: isWriteRequest ? getPusherSocketID() : undefined,
+        pusherSocketID: isWriteRequest ? Pusher.getPusherSocketID() : undefined,
     };
 
     // Assemble all request metadata (used by middlewares, and for persisted requests stored in Onyx)
diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
index 8120770f52d..336e99c54c8 100644
--- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx
+++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx
@@ -34,7 +34,7 @@ import type {AuthScreensParamList, CentralPaneName, CentralPaneScreensParamList}
 import {isOnboardingFlowName} from '@libs/NavigationUtils';
 import NetworkConnection from '@libs/NetworkConnection';
 import onyxSubscribe from '@libs/onyxSubscribe';
-import * as Pusher from '@libs/Pusher';
+import Pusher from '@libs/Pusher';
 import PusherConnectionManager from '@libs/PusherConnectionManager';
 import * as ReportUtils from '@libs/ReportUtils';
 import * as SearchQueryUtils from '@libs/SearchQueryUtils';
diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts
index 4348e81964f..6da8c7a3053 100644
--- a/src/libs/Pusher/index.native.ts
+++ b/src/libs/Pusher/index.native.ts
@@ -9,19 +9,8 @@ import {authenticatePusher} from '@userActions/Session';
 import CONST from '@src/CONST';
 import ONYXKEYS from '@src/ONYXKEYS';
 import TYPE from './EventType';
-import type {
-    Args,
-    ChunkedDataEvents,
-    EventCallbackError,
-    EventData,
-    PingPongEvent,
-    PusherEventName,
-    SocketEventCallback,
-    SocketEventName,
-    States,
-    UserIsLeavingRoomEvent,
-    UserIsTypingEvent,
-} from './types';
+import type {Args, ChunkedDataEvents, EventCallbackError, EventData, PusherEventName, SocketEventCallback, SocketEventName, States} from './types';
+import type PusherModule from './types';
 
 let shouldForceOffline = false;
 Onyx.connect({
@@ -349,6 +338,19 @@ if (window) {
     window.getPusherInstance = () => socket;
 }
 
-export {init, subscribe, unsubscribe, getChannel, isSubscribed, isAlreadySubscribing, sendEvent, disconnect, reconnect, registerSocketEventCallback, TYPE, getPusherSocketID};
-
-export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent};
+const MobilePusher: PusherModule = {
+    init,
+    subscribe,
+    unsubscribe,
+    getChannel,
+    isSubscribed,
+    isAlreadySubscribing,
+    sendEvent,
+    disconnect,
+    reconnect,
+    registerSocketEventCallback,
+    TYPE,
+    getPusherSocketID,
+};
+
+export default MobilePusher;
diff --git a/src/libs/Pusher/index.ts b/src/libs/Pusher/index.ts
index 22cf7935231..282ac54b6f1 100644
--- a/src/libs/Pusher/index.ts
+++ b/src/libs/Pusher/index.ts
@@ -11,16 +11,14 @@ import type {
     ChunkedDataEvents,
     EventCallbackError,
     EventData,
-    PingPongEvent,
     PusherEventName,
     PusherSubscribtionErrorData,
     PusherWithAuthParams,
     SocketEventCallback,
     SocketEventName,
     States,
-    UserIsLeavingRoomEvent,
-    UserIsTypingEvent,
 } from './types';
+import type PusherModule from './types';
 
 let shouldForceOffline = false;
 Onyx.connect({
@@ -380,7 +378,7 @@ if (window) {
     window.getPusherInstance = () => socket;
 }
 
-export {
+const WebPusher: PusherModule = {
     init,
     subscribe,
     unsubscribe,
@@ -396,4 +394,4 @@ export {
     getPusherSocketID,
 };
 
-export type {EventCallbackError, States, UserIsTypingEvent, UserIsLeavingRoomEvent, PingPongEvent};
+export default WebPusher;
diff --git a/src/libs/Pusher/types.ts b/src/libs/Pusher/types.ts
index 88348e1c4ba..d6d1da3297d 100644
--- a/src/libs/Pusher/types.ts
+++ b/src/libs/Pusher/types.ts
@@ -79,6 +79,7 @@ type PusherModule = {
     registerSocketEventCallback: (cb: SocketEventCallback) => void;
     registerCustomAuthorizer?: (authorizer: ChannelAuthorizerGenerator) => void;
     getPusherSocketID: () => string | undefined;
+    TYPE: typeof TYPE;
 };
 
 export default PusherModule;
diff --git a/src/libs/PusherConnectionManager.ts b/src/libs/PusherConnectionManager.ts
index ee31c892045..bc3ef45d859 100644
--- a/src/libs/PusherConnectionManager.ts
+++ b/src/libs/PusherConnectionManager.ts
@@ -2,9 +2,8 @@ import type {ChannelAuthorizationCallback} from 'pusher-js/with-encryption';
 import CONST from '@src/CONST';
 import {authenticatePusher} from './actions/Session';
 import Log from './Log';
-import {reconnect, registerCustomAuthorizer, registerSocketEventCallback} from './Pusher';
-import type {EventCallbackError, States} from './Pusher';
-import type {SocketEventName} from './Pusher/types';
+import Pusher from './Pusher';
+import type {EventCallbackError, SocketEventName, States} from './Pusher/types';
 
 function init() {
     /**
@@ -13,13 +12,13 @@ function init() {
      * current valid token to generate the signed auth response
      * needed to subscribe to Pusher channels.
      */
-    registerCustomAuthorizer?.((channel) => ({
+    Pusher.registerCustomAuthorizer?.((channel) => ({
         authorize: (socketId: string, callback: ChannelAuthorizationCallback) => {
             authenticatePusher(socketId, channel.name, callback);
         },
     }));
 
-    registerSocketEventCallback((eventName: SocketEventName, error?: EventCallbackError | States) => {
+    Pusher.registerSocketEventCallback((eventName: SocketEventName, error?: EventCallbackError | States) => {
         switch (eventName) {
             case 'error': {
                 if (error && 'type' in error) {
@@ -35,7 +34,7 @@ function init() {
                         // On the advice from Pusher directly, they suggested to manually reconnect in those scenarios.
                         if (errorMessage) {
                             Log.hmmm('[PusherConnectionManager] Channels Error 1006 message', {errorMessage});
-                            reconnect();
+                            Pusher.reconnect();
                         }
                     } 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
diff --git a/src/libs/PusherUtils.ts b/src/libs/PusherUtils.ts
index 50778160dbf..703b5bc4a44 100644
--- a/src/libs/PusherUtils.ts
+++ b/src/libs/PusherUtils.ts
@@ -4,8 +4,8 @@ import CONST from '@src/CONST';
 import type {OnyxUpdatesFromServer} from '@src/types/onyx';
 import Log from './Log';
 import NetworkConnection from './NetworkConnection';
-import {subscribe} from './Pusher';
-import type {PingPongEvent} from './Pusher';
+import Pusher from './Pusher';
+import type {PingPongEvent} from './Pusher/types';
 
 type Callback = (data: OnyxUpdate[]) => Promise<void>;
 
@@ -50,7 +50,7 @@ function subscribeToPrivateUserChannelEvent(eventName: string, accountID: string
     function onSubscriptionFailed(error: Error) {
         Log.hmmm('Failed to subscribe to Pusher channel', {error, pusherChannelName, eventName});
     }
-    subscribe(pusherChannelName, eventName, onEventPush, onPusherResubscribeToPrivateUserChannel).catch(onSubscriptionFailed);
+    Pusher.subscribe(pusherChannelName, eventName, onEventPush, onPusherResubscribeToPrivateUserChannel).catch(onSubscriptionFailed);
 }
 
 export default {
diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts
index 9de0c193124..63a372f4ea9 100644
--- a/src/libs/actions/Report.ts
+++ b/src/libs/actions/Report.ts
@@ -79,7 +79,8 @@ import * as PhoneNumber from '@libs/PhoneNumber';
 import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
 import {extractPolicyIDFromPath, getPolicy} from '@libs/PolicyUtils';
 import processReportIDDeeplink from '@libs/processReportIDDeeplink';
-import * as Pusher from '@libs/Pusher';
+import Pusher from '@libs/Pusher';
+import type {UserIsLeavingRoomEvent, UserIsTypingEvent} from '@libs/Pusher/types';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import type {OptimisticAddCommentReportAction, OptimisticChatReport} from '@libs/ReportUtils';
 import {
@@ -398,7 +399,7 @@ function getReportChannelName(reportID: string): string {
  *
  * This method makes sure that no matter which we get, we return the "new" format
  */
-function getNormalizedStatus(typingStatus: Pusher.UserIsTypingEvent | Pusher.UserIsLeavingRoomEvent): ReportUserIsTyping {
+function getNormalizedStatus(typingStatus: UserIsTypingEvent | UserIsLeavingRoomEvent): ReportUserIsTyping {
     let normalizedStatus: ReportUserIsTyping;
 
     if (typingStatus.userLogin) {
@@ -463,7 +464,7 @@ function subscribeToReportLeavingEvents(reportID: string | undefined) {
     Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportID}`, false);
 
     const pusherChannelName = getReportChannelName(reportID);
-    Pusher.subscribe(pusherChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, (leavingStatus: Pusher.UserIsLeavingRoomEvent) => {
+    Pusher.subscribe(pusherChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, (leavingStatus: UserIsLeavingRoomEvent) => {
         // If the pusher message comes from OldDot, we expect the leaving status to be keyed by user
         // login OR by 'Concierge'. If the pusher message comes from NewDot, it is keyed by accountID
         // since personal details are keyed by accountID.
@@ -1499,7 +1500,7 @@ function saveReportDraftComment(reportID: string, comment: string | null, callba
 /** Broadcasts whether or not a user is typing on a report over the report's private pusher channel. */
 function broadcastUserIsTyping(reportID: string) {
     const privateReportChannelName = getReportChannelName(reportID);
-    const typingStatus: Pusher.UserIsTypingEvent = {
+    const typingStatus: UserIsTypingEvent = {
         [currentUserAccountID]: true,
     };
     Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_TYPING, typingStatus);
@@ -1508,7 +1509,7 @@ function broadcastUserIsTyping(reportID: string) {
 /** Broadcasts to the report's private pusher channel whether a user is leaving a report */
 function broadcastUserIsLeavingRoom(reportID: string) {
     const privateReportChannelName = getReportChannelName(reportID);
-    const leavingStatus: Pusher.UserIsLeavingRoomEvent = {
+    const leavingStatus: UserIsLeavingRoomEvent = {
         [currentUserAccountID]: true,
     };
     Pusher.sendEvent(privateReportChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM, leavingStatus);
diff --git a/src/libs/actions/Session/index.ts b/src/libs/actions/Session/index.ts
index 60c169df433..d28d3b77180 100644
--- a/src/libs/actions/Session/index.ts
+++ b/src/libs/actions/Session/index.ts
@@ -36,7 +36,7 @@ import * as MainQueue from '@libs/Network/MainQueue';
 import * as NetworkStore from '@libs/Network/NetworkStore';
 import {getCurrentUserEmail} from '@libs/Network/NetworkStore';
 import NetworkConnection from '@libs/NetworkConnection';
-import * as Pusher from '@libs/Pusher';
+import Pusher from '@libs/Pusher';
 import {getReportIDFromLink, parseReportRouteParams as parseReportRouteParamsReportUtils} from '@libs/ReportUtils';
 import * as SessionUtils from '@libs/SessionUtils';
 import {clearSoundAssetsCache} from '@libs/Sound';
diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts
index 8ed2506336b..0397a06326d 100644
--- a/src/libs/actions/User.ts
+++ b/src/libs/actions/User.ts
@@ -32,7 +32,8 @@ import {isOffline} from '@libs/Network/NetworkStore';
 import * as SequentialQueue from '@libs/Network/SequentialQueue';
 import * as NumberUtils from '@libs/NumberUtils';
 import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
-import * as Pusher from '@libs/Pusher';
+import Pusher from '@libs/Pusher';
+import type {PingPongEvent} from '@libs/Pusher/types';
 import PusherUtils from '@libs/PusherUtils';
 import * as ReportActionsUtils from '@libs/ReportActionsUtils';
 import * as ReportUtils from '@libs/ReportUtils';
@@ -905,7 +906,7 @@ function subscribeToPusherPong() {
         lastPongReceivedTimestamp = Date.now();
 
         // Calculate the latency between the client and the server
-        const pongEvent = pushJSON as Pusher.PingPongEvent;
+        const pongEvent = pushJSON as PingPongEvent;
         const latency = Date.now() - Number(pongEvent.pingTimestamp);
         Log.info(`[Pusher PINGPONG] The event took ${latency} ms`);
 
diff --git a/tests/utils/PusherHelper.ts b/tests/utils/PusherHelper.ts
index 43e5e30f54a..fe84cc9344d 100644
--- a/tests/utils/PusherHelper.ts
+++ b/tests/utils/PusherHelper.ts
@@ -1,4 +1,4 @@
-import {init, sendEvent, TYPE, unsubscribe} from '@libs/Pusher';
+import Pusher from '@libs/Pusher';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
 import PusherConnectionManager from '@src/libs/PusherConnectionManager';
@@ -11,12 +11,12 @@ function setup() {
     // channel already in a subscribed state. These methods are normally used to prevent
     // duplicated subscriptions, but we don't need them for this test so forcing them to
     // return false will make the testing less complex.
-    jest.spyOn(require('@libs/Pusher'), 'isSubscribed').mockReturnValue(false);
-    jest.spyOn(require('@libs/Pusher'), 'isAlreadySubscribing').mockReturnValue(false);
+    jest.spyOn(Pusher, 'isSubscribed').mockReturnValue(false);
+    jest.spyOn(Pusher, 'isAlreadySubscribing').mockReturnValue(false);
 
     // Connect to Pusher
     PusherConnectionManager.init();
-    init({
+    Pusher.init({
         appKey: CONFIG.PUSHER.APP_KEY,
         cluster: CONFIG.PUSHER.CLUSTER,
         authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`,
@@ -29,13 +29,13 @@ function setup() {
 }
 
 function emitOnyxUpdate(args: OnyxServerUpdate[]) {
-    sendEvent(CHANNEL_NAME, TYPE.MULTIPLE_EVENTS, {
+    Pusher.sendEvent(CHANNEL_NAME, Pusher.TYPE.MULTIPLE_EVENTS, {
         type: 'pusher',
         lastUpdateID: 0,
         previousUpdateID: 0,
         updates: [
             {
-                eventType: TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
+                eventType: Pusher.TYPE.MULTIPLE_EVENT_TYPE.ONYX_API_UPDATE,
                 data: args,
             },
         ],
@@ -45,7 +45,7 @@ function emitOnyxUpdate(args: OnyxServerUpdate[]) {
 function teardown() {
     // Unsubscribe from account channel after each test since we subscribe in the function
     // subscribeToUserEvents and we don't want duplicate event subscriptions.
-    unsubscribe(CHANNEL_NAME);
+    Pusher.unsubscribe(CHANNEL_NAME);
 }
 
 export default {
diff --git a/tests/utils/TestHelper.ts b/tests/utils/TestHelper.ts
index edc06433e06..24c15aa274d 100644
--- a/tests/utils/TestHelper.ts
+++ b/tests/utils/TestHelper.ts
@@ -5,7 +5,7 @@ import Onyx from 'react-native-onyx';
 import type {ConnectOptions} from 'react-native-onyx/dist/types';
 import type {ApiCommand, ApiRequestCommandParameters} from '@libs/API/types';
 import {translateLocal} from '@libs/Localize';
-import {init} from '@libs/Pusher';
+import Pusher from '@libs/Pusher';
 import PusherConnectionManager from '@libs/PusherConnectionManager';
 import CONFIG from '@src/CONFIG';
 import CONST from '@src/CONST';
@@ -47,7 +47,7 @@ function setupApp() {
 
         // Connect to Pusher
         PusherConnectionManager.init();
-        init({
+        Pusher.init({
             appKey: CONFIG.PUSHER.APP_KEY,
             cluster: CONFIG.PUSHER.CLUSTER,
             authEndpoint: `${CONFIG.EXPENSIFY.DEFAULT_API_ROOT}api/AuthenticatePusher?`,

From 416b9bfd809d65f119fe3182a4ae21f46fd76963 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 12 Feb 2025 16:19:30 +0100
Subject: [PATCH 14/16] Re-run checks


From 4a6e373c6c80c7a5f365b282dfa4b18cedd20db6 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Thu, 13 Feb 2025 12:17:23 +0100
Subject: [PATCH 15/16] Android: fix event trigger

---
 __mocks__/@pusher/pusher-websocket-react-native/index.ts | 2 +-
 src/libs/Pusher/index.native.ts                          | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
index dd0ea1592de..07a7a06b4ba 100644
--- a/__mocks__/@pusher/pusher-websocket-react-native/index.ts
+++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
@@ -67,7 +67,7 @@ class MockedPusher {
     }
 
     trigger({channelName, eventName, data}: PusherEvent) {
-        this.channels.get(channelName)?.onEvent({channelName, eventName, data: JSON.stringify(data)});
+        this.channels.get(channelName)?.onEvent({channelName, eventName, data: data as Record<string, unknown>});
     }
 
     getChannel(channelName: string) {
diff --git a/src/libs/Pusher/index.native.ts b/src/libs/Pusher/index.native.ts
index 6da8c7a3053..aef541baf69 100644
--- a/src/libs/Pusher/index.native.ts
+++ b/src/libs/Pusher/index.native.ts
@@ -284,7 +284,7 @@ function sendEvent<EventName extends PusherEventName>(channelName: string, event
         return;
     }
 
-    socket?.trigger({channelName, eventName, data: payload});
+    socket?.trigger({channelName, eventName, data: JSON.stringify(payload)});
 }
 
 /**

From 0b975b1e5f4afdbb4f2a54f163c6ab6d751888d1 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Fri, 14 Feb 2025 12:20:29 +0100
Subject: [PATCH 16/16] Minor update

---
 __mocks__/@pusher/pusher-websocket-react-native/index.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/__mocks__/@pusher/pusher-websocket-react-native/index.ts b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
index 07a7a06b4ba..b2b59d30325 100644
--- a/__mocks__/@pusher/pusher-websocket-react-native/index.ts
+++ b/__mocks__/@pusher/pusher-websocket-react-native/index.ts
@@ -20,7 +20,9 @@ type SubscribeProps = {
     onSubscriptionSucceeded: OnSubscriptionSucceeded;
 };
 
-type UnsubscribeProps = {channelName: string};
+type UnsubscribeProps = {
+    channelName: string;
+};
 
 class MockedPusher {
     static instance: MockedPusher | null = null;
