From 0c70673ec351428d8a096f37b6897853cf1c063f Mon Sep 17 00:00:00 2001
From: Adam Horodyski <ad.horodyski@gmail.com>
Date: Mon, 13 Jan 2025 21:57:46 +0100
Subject: [PATCH 01/14] feat: js payload parser for gzipped json strings

---
 package-lock.json                             | 14 ++++++++
 package.json                                  |  2 ++
 .../parsePushNotificationPayload.ts           | 35 +++++++++++++++----
 3 files changed, 45 insertions(+), 6 deletions(-)

diff --git a/package-lock.json b/package-lock.json
index d3820d4c582c..f8824b2fc568 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -43,6 +43,7 @@
         "@react-ng/bounds-observer": "^0.2.1",
         "@rnmapbox/maps": "10.1.33",
         "@shopify/flash-list": "1.7.1",
+        "@types/pako": "^2.0.3",
         "@ua/react-native-airship": "19.2.1",
         "awesome-phonenumber": "^5.4.0",
         "babel-polyfill": "^6.26.0",
@@ -68,6 +69,7 @@
         "lottie-react-native": "6.5.1",
         "mapbox-gl": "^2.15.0",
         "onfido-sdk-ui": "14.15.0",
+        "pako": "^2.1.0",
         "process": "^0.11.10",
         "pusher-js": "8.3.0",
         "react": "18.3.1",
@@ -13828,6 +13830,12 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/pako": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz",
+      "integrity": "sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==",
+      "license": "MIT"
+    },
     "node_modules/@types/parse-json": {
       "version": "4.0.0",
       "dev": true,
@@ -30560,6 +30568,12 @@
       "version": "1.0.0",
       "license": "BlueOak-1.0.0"
     },
+    "node_modules/pako": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+      "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+      "license": "(MIT AND Zlib)"
+    },
     "node_modules/param-case": {
       "version": "3.0.4",
       "dev": true,
diff --git a/package.json b/package.json
index 0978d03624ce..564320917f5a 100644
--- a/package.json
+++ b/package.json
@@ -108,6 +108,7 @@
     "@react-ng/bounds-observer": "^0.2.1",
     "@rnmapbox/maps": "10.1.33",
     "@shopify/flash-list": "1.7.1",
+    "@types/pako": "^2.0.3",
     "@ua/react-native-airship": "19.2.1",
     "awesome-phonenumber": "^5.4.0",
     "babel-polyfill": "^6.26.0",
@@ -133,6 +134,7 @@
     "lottie-react-native": "6.5.1",
     "mapbox-gl": "^2.15.0",
     "onfido-sdk-ui": "14.15.0",
+    "pako": "^2.1.0",
     "process": "^0.11.10",
     "pusher-js": "8.3.0",
     "react": "18.3.1",
diff --git a/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts b/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts
index 8ef20dd9f537..089c1b4ed124 100644
--- a/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts
+++ b/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts
@@ -1,20 +1,43 @@
 import type {JsonObject, JsonValue} from '@ua/react-native-airship';
+import pako from 'pako';
 import Log from '@libs/Log';
 import type {PushNotificationData} from './NotificationType';
 
+const GZIP_MAGIC_NUMBER = '\x1f\x8b';
+
 /**
  * Parse the payload of a push notification. On Android, some notification payloads are sent as a JSON string rather than an object
  */
 export default function parsePushNotificationPayload(payload: JsonValue | undefined): PushNotificationData | undefined {
-    let data = payload;
-    if (typeof payload === 'string') {
+    if (payload === undefined) {
+        return undefined;
+    }
+
+    // No need to parse if it's already an object
+    if (typeof payload !== 'string') {
+        return payload as PushNotificationData;
+    }
+
+    // Gzipped JSON String
+    if (payload.startsWith(GZIP_MAGIC_NUMBER)) {
         try {
-            data = JSON.parse(payload) as JsonObject;
+            Log.hmmm('[PushNotification] Dealing with a gzipped json string', payload);
+            const compressed = Buffer.from(payload, 'base64');
+            const decompressed = pako.inflate(compressed);
+            const jsonString = Buffer.from(decompressed).toString('utf-8');
+            const jsonObject = JSON.parse(jsonString) as JsonObject;
+            return jsonObject as PushNotificationData;
         } catch {
-            Log.hmmm(`[PushNotification] Failed to parse the payload`, payload);
-            data = undefined;
+            Log.hmmm('[PushNotification] Gzipped payload is not supported yet', payload);
+            return undefined;
         }
     }
 
-    return data ? (data as PushNotificationData) : undefined;
+    // JSON String
+    try {
+        return JSON.parse(payload) as JsonObject as PushNotificationData;
+    } catch {
+        Log.hmmm(`[PushNotification] Failed to parse the JSON payload`, payload);
+        return undefined;
+    }
 }

From 5850f727ffd494910683f4add09ec6bb9505e6f5 Mon Sep 17 00:00:00 2001
From: Adam Horodyski <ad.horodyski@gmail.com>
Date: Mon, 13 Jan 2025 22:00:36 +0100
Subject: [PATCH 02/14] chore: update logs

---
 .../PushNotification/parsePushNotificationPayload.ts          | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts b/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts
index 089c1b4ed124..942e3e31bd8c 100644
--- a/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts
+++ b/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts
@@ -28,7 +28,7 @@ export default function parsePushNotificationPayload(payload: JsonValue | undefi
             const jsonObject = JSON.parse(jsonString) as JsonObject;
             return jsonObject as PushNotificationData;
         } catch {
-            Log.hmmm('[PushNotification] Gzipped payload is not supported yet', payload);
+            Log.hmmm('[PushNotification] Failed to parse the payload as a Gzipped JSON string', payload);
             return undefined;
         }
     }
@@ -37,7 +37,7 @@ export default function parsePushNotificationPayload(payload: JsonValue | undefi
     try {
         return JSON.parse(payload) as JsonObject as PushNotificationData;
     } catch {
-        Log.hmmm(`[PushNotification] Failed to parse the JSON payload`, payload);
+        Log.hmmm(`[PushNotification] Failed to parse the payload as a JSON string`, payload);
         return undefined;
     }
 }

From 6a7db39fd3794449bee5f94389da52179e18c210 Mon Sep 17 00:00:00 2001
From: Adam Horodyski <ad.horodyski@gmail.com>
Date: Mon, 20 Jan 2025 19:53:21 +0100
Subject: [PATCH 03/14] feat: init android implementation

---
 .../CustomNotificationProvider.java           | 35 +++++++++------
 .../customairshipextender/PayloadHandler.kt   | 43 +++++++++++++++++++
 2 files changed, 65 insertions(+), 13 deletions(-)
 create mode 100644 android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt

diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
index b950921a0cd5..7a6de3edb9f5 100644
--- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
+++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
@@ -38,6 +38,7 @@
 
 import com.expensify.chat.R;
 import com.expensify.chat.shortcutManagerModule.ShortcutManagerUtils;
+import com.expensify.chat.customairshipextender.PayloadHandler;
 import com.urbanairship.AirshipConfigOptions;
 import com.urbanairship.json.JsonMap;
 import com.urbanairship.json.JsonValue;
@@ -119,18 +120,26 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @
         }
 
         // Attempt to parse data and apply custom notification styling
-        if (message.containsKey(PAYLOAD_KEY)) {
-            try {
-                JsonMap payload = JsonValue.parseString(message.getExtra(PAYLOAD_KEY)).optMap();
-                if (payload.containsKey(ONYX_DATA_KEY)) {
-                    Objects.requireNonNull(payload.get(ONYX_DATA_KEY)).isNull();
-                    Log.d(TAG, "payload contains onxyData");
-                    String alert = message.getExtra(PushMessage.EXTRA_ALERT);
-                    applyMessageStyle(context, builder, payload, arguments.getNotificationId(), alert);
-                }
-            } catch (Exception e) {
-                Log.e(TAG, "Failed to parse conversation, falling back to default notification style. SendID=" + message.getSendId(), e);
-            }
+        if (!message.containsKey(PAYLOAD_KEY)) {
+            return builder;
+        }
+
+        try {
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) throw new Exception();
+
+            String rawPayload = message.getExtra(PAYLOAD_KEY);
+            if (rawPayload == null) throw new Exception();
+
+            String processedPayload = PayloadHandler.processPayload(rawPayload);
+            JsonMap payload = JsonValue.parseString(processedPayload).optMap();
+            if (!payload.containsKey(ONYX_DATA_KEY)) throw new Exception();
+
+            Objects.requireNonNull(payload.get(ONYX_DATA_KEY)).isNull();
+            Log.d(TAG, "payload contains onxyData");
+            String alert = message.getExtra(PushMessage.EXTRA_ALERT);
+            applyMessageStyle(context, builder, payload, arguments.getNotificationId(), alert);
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to parse conversation, falling back to default notification style. SendID=" + message.getSendId(), e);
         }
 
         return builder;
@@ -207,7 +216,7 @@ private void applyMessageStyle(@NonNull Context context, NotificationCompat.Buil
             String name = messageData.get("person").getList().get(0).getMap().get("text").getString();
             String avatar = messageData.get("avatar").getString();
             String accountID = Integer.toString(messageData.get("actorAccountID").getInt(-1));
-            
+
             // Use the formatted alert message from the backend. Otherwise fallback on the message in the Onyx data.
             String message = alert != null ? alert : messageData.get("message").getList().get(0).getMap().get("text").getString();
             String roomName = payload.get("roomName") == null ? "" : payload.get("roomName").getString("");
diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt b/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
new file mode 100644
index 000000000000..6473f80c7877
--- /dev/null
+++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
@@ -0,0 +1,43 @@
+package com.expensify.chat.customairshipextender
+
+import android.os.Build
+import androidx.annotation.RequiresApi
+import java.io.ByteArrayInputStream
+import java.io.ByteArrayOutputStream
+import java.nio.charset.StandardCharsets
+import java.util.Base64
+import java.util.zip.GZIPInputStream
+
+object PayloadHandler {
+    private const val BUFFER_SIZE = 1024
+    private val GZIP_MAGIC = byteArrayOf(0x1f.toByte(), 0x8b.toByte())
+
+    @JvmStatic
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    fun processPayload(payloadString: String): String =
+            runCatching {
+                        val decoded = Base64.getDecoder().decode(payloadString)
+                        if (!isGzipped(decoded)) error("Input not gzipped")
+                        return decompressGzip(decoded)
+                    }
+                    .getOrDefault(payloadString)
+
+    private fun isGzipped(decoded: ByteArray) =
+            decoded[0] == GZIP_MAGIC[0] && decoded[1] == GZIP_MAGIC[1]
+
+    private fun decompressGzip(compressed: ByteArray): String {
+        ByteArrayInputStream(compressed).use { bis ->
+            GZIPInputStream(bis).use { gis ->
+                ByteArrayOutputStream().use { output ->
+                    val buffer = ByteArray(BUFFER_SIZE)
+                    var len: Int
+                    while (gis.read(buffer).also { len = it } > 0) {
+                        output.write(buffer, 0, len)
+                    }
+
+                    return output.toString(StandardCharsets.UTF_8.name())
+                }
+            }
+        }
+    }
+}

From 87815eefe448f54e80da81780b19cefcddb8c247 Mon Sep 17 00:00:00 2001
From: Adam Horodyski <ad.horodyski@gmail.com>
Date: Thu, 23 Jan 2025 18:11:41 +0100
Subject: [PATCH 04/14] fix: adjust the js implementation to work with RN, add
 tests for all covered paths

---
 .../parsePushNotificationPayload.ts           | 24 +++++++++---------
 .../unit/parsePushNotificationPayloadTest.ts  | 25 +++++++++++++++++++
 2 files changed, 37 insertions(+), 12 deletions(-)
 create mode 100644 tests/unit/parsePushNotificationPayloadTest.ts

diff --git a/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts b/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts
index 942e3e31bd8c..213a2d5f95f2 100644
--- a/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts
+++ b/src/libs/Notification/PushNotification/parsePushNotificationPayload.ts
@@ -19,18 +19,17 @@ export default function parsePushNotificationPayload(payload: JsonValue | undefi
     }
 
     // Gzipped JSON String
-    if (payload.startsWith(GZIP_MAGIC_NUMBER)) {
-        try {
-            Log.hmmm('[PushNotification] Dealing with a gzipped json string', payload);
-            const compressed = Buffer.from(payload, 'base64');
-            const decompressed = pako.inflate(compressed);
-            const jsonString = Buffer.from(decompressed).toString('utf-8');
-            const jsonObject = JSON.parse(jsonString) as JsonObject;
-            return jsonObject as PushNotificationData;
-        } catch {
-            Log.hmmm('[PushNotification] Failed to parse the payload as a Gzipped JSON string', payload);
-            return undefined;
+    try {
+        const binaryStringPayload = atob(payload);
+        if (!binaryStringPayload.startsWith(GZIP_MAGIC_NUMBER)) {
+            throw Error();
         }
+        const compressed = Uint8Array.from(binaryStringPayload, (x) => x.charCodeAt(0));
+        const decompressed = pako.inflate(compressed, {to: 'string'});
+        const jsonObject = JSON.parse(decompressed) as JsonObject;
+        return jsonObject as PushNotificationData;
+    } catch {
+        Log.hmmm('[PushNotification] Failed to parse the payload as a Gzipped JSON string', payload);
     }
 
     // JSON String
@@ -38,6 +37,7 @@ export default function parsePushNotificationPayload(payload: JsonValue | undefi
         return JSON.parse(payload) as JsonObject as PushNotificationData;
     } catch {
         Log.hmmm(`[PushNotification] Failed to parse the payload as a JSON string`, payload);
-        return undefined;
     }
+
+    return undefined;
 }
diff --git a/tests/unit/parsePushNotificationPayloadTest.ts b/tests/unit/parsePushNotificationPayloadTest.ts
new file mode 100644
index 000000000000..8277457afa3f
--- /dev/null
+++ b/tests/unit/parsePushNotificationPayloadTest.ts
@@ -0,0 +1,25 @@
+import parsePushNotificationPayload from '@libs/Notification/PushNotification/parsePushNotificationPayload';
+
+describe('parsePushNotificationPayload', () => {
+    it("returns 'undefined' for missing payload", () => {
+        expect(parsePushNotificationPayload(undefined)).toBeUndefined();
+    });
+
+    it('returns untouched input when provided with raw json', () => {
+        const payload = {key: 'value'};
+        expect(parsePushNotificationPayload(payload)).toStrictEqual(payload);
+    });
+
+    it('returns correct json when provided with stringified json', () => {
+        const json = {key: 'value'};
+        const payload = JSON.stringify(json);
+        expect(parsePushNotificationPayload(payload)).toStrictEqual(json);
+    });
+
+    it('returns correct json when provided with gzip compressed json string', () => {
+        const json = {key: 'value'};
+        // gzip compressed json string
+        const payload = 'H4sIAAAAAAAAA6tWyk6tVLJSKkvMKU1VqgUAv5wYPw8AAAA=';
+        expect(parsePushNotificationPayload(payload)).toStrictEqual(json);
+    });
+});

From 34ee570dec4597e8e8d52f655c45159b0817fc7f Mon Sep 17 00:00:00 2001
From: Adam Horodyski <ad.horodyski@gmail.com>
Date: Thu, 23 Jan 2025 18:59:32 +0100
Subject: [PATCH 05/14] fix: android implementation

---
 .../CustomNotificationProvider.java                  |  3 ++-
 .../chat/customairshipextender/PayloadHandler.kt     | 12 +++++-------
 2 files changed, 7 insertions(+), 8 deletions(-)

diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
index 7a6de3edb9f5..22362c256231 100644
--- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
+++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
@@ -130,7 +130,8 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @
             String rawPayload = message.getExtra(PAYLOAD_KEY);
             if (rawPayload == null) throw new Exception();
 
-            String processedPayload = PayloadHandler.processPayload(rawPayload);
+            PayloadHandler handler = new PayloadHandler();
+            String processedPayload = handler.processPayload(rawPayload);
             JsonMap payload = JsonValue.parseString(processedPayload).optMap();
             if (!payload.containsKey(ONYX_DATA_KEY)) throw new Exception();
 
diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt b/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
index 6473f80c7877..41e1ef60b332 100644
--- a/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
+++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
@@ -2,18 +2,16 @@ package com.expensify.chat.customairshipextender
 
 import android.os.Build
 import androidx.annotation.RequiresApi
+import java.util.Base64
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
-import java.nio.charset.StandardCharsets
-import java.util.Base64
 import java.util.zip.GZIPInputStream
 
-object PayloadHandler {
-    private const val BUFFER_SIZE = 1024
+class PayloadHandler {
+    private val BUFFER_SIZE = 1024
     private val GZIP_MAGIC = byteArrayOf(0x1f.toByte(), 0x8b.toByte())
 
-    @JvmStatic
-    @RequiresApi(api = Build.VERSION_CODES.O)
+    @RequiresApi(Build.VERSION_CODES.O)
     fun processPayload(payloadString: String): String =
             runCatching {
                         val decoded = Base64.getDecoder().decode(payloadString)
@@ -35,7 +33,7 @@ object PayloadHandler {
                         output.write(buffer, 0, len)
                     }
 
-                    return output.toString(StandardCharsets.UTF_8.name())
+                    return output.toString("UTF-8")
                 }
             }
         }

From 959478f754df2220a1b8d50842da71aece1fe195 Mon Sep 17 00:00:00 2001
From: Adam Horodyski <ad.horodyski@gmail.com>
Date: Tue, 28 Jan 2025 19:53:40 +0100
Subject: [PATCH 06/14] feat: initial implementation for ios

---
 .../NotificationService.swift                 | 36 +++++++++++++++++--
 ios/Podfile                                   |  3 +-
 ios/Podfile.lock                              | 10 ++++--
 3 files changed, 42 insertions(+), 7 deletions(-)

diff --git a/ios/NotificationServiceExtension/NotificationService.swift b/ios/NotificationServiceExtension/NotificationService.swift
index b588c6be1d0f..db999e0ec631 100644
--- a/ios/NotificationServiceExtension/NotificationService.swift
+++ b/ios/NotificationServiceExtension/NotificationService.swift
@@ -9,6 +9,7 @@ import AirshipServiceExtension
 import os.log
 import Intents
 import AppLogs
+import Gzip
 
 class NotificationService: UANotificationServiceExtension {
   
@@ -93,11 +94,40 @@ class NotificationService: UANotificationServiceExtension {
     }
   }
   
-  func parsePayload(notificationContent: UNMutableNotificationContent) throws -> NotificationData  {
-    guard let payload = notificationContent.userInfo["payload"] as? NSDictionary else {
-      throw ExpError.runtimeError("payload missing")
+  private func processPayload(rawPayload: Any) throws -> NSDictionary {
+    // Handle valid objects first
+    if let dictPayload = rawPayload as? NSDictionary {
+      return dictPayload
     }
     
+    guard let stringPayload = rawPayload as? String else {
+      throw ExpError.runtimeError("Failed to read payload as string")
+    }
+    
+    guard let decoded = Data(base64Encoded: stringPayload) else {
+      throw ExpError.runtimeError("Failed to decode payload string")
+    }
+    
+    guard decoded.isGzipped else {
+      throw ExpError.runtimeError("Decoded string not gzipped")
+    }
+    
+    let decompressedData = try decoded.gunzipped()
+    
+    guard let jsonDict = try JSONSerialization.jsonObject(with: decompressedData) as? NSDictionary else {
+      throw ExpError.runtimeError("Failed to parse JSON into dictionary")
+    }
+    
+    return jsonDict
+  }
+  
+  func parsePayload(notificationContent: UNMutableNotificationContent) throws -> NotificationData {
+    guard let rawPayload = notificationContent.userInfo["payload"] else {
+        throw ExpError.runtimeError("payload missing")
+    }
+  
+    let payload = try processPayload(rawPayload: rawPayload)
+    
     guard let reportID = payload["reportID"] as? Int64 else {
       throw ExpError.runtimeError("payload.reportID missing")
     }
diff --git a/ios/Podfile b/ios/Podfile
index b74584b839b4..245a52257af8 100644
--- a/ios/Podfile
+++ b/ios/Podfile
@@ -120,7 +120,8 @@ end
 
 target 'NotificationServiceExtension' do
   pod 'AirshipServiceExtension'
+  pod 'GzipSwift'
   pod 'AppLogs', :path => '../node_modules/react-native-app-logs/AppLogsPod'
 end
 
-pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz'
\ No newline at end of file
+pod 'FullStory', :http => 'https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz'
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index c674513b9f73..53e697696eec 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -252,6 +252,7 @@ PODS:
     - AppAuth/Core (~> 1.7)
     - GTMSessionFetcher/Core (< 4.0, >= 3.3)
   - GTMSessionFetcher/Core (3.5.0)
+  - GzipSwift (5.1.1)
   - hermes-engine (0.76.3):
     - hermes-engine/Pre-built (= 0.76.3)
   - hermes-engine/Pre-built (0.76.3)
@@ -2846,6 +2847,7 @@ DEPENDENCIES:
   - "FullStory (from `{:http=>\"https://ios-releases.fullstory.com/fullstory-1.52.0-xcframework.tar.gz\"}`)"
   - "fullstory_react-native (from `../node_modules/@fullstory/react-native`)"
   - glog (from `../node_modules/react-native/third-party-podspecs/glog.podspec`)
+  - GzipSwift
   - 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`)"
@@ -2974,6 +2976,7 @@ SPEC REPOS:
     - GoogleUtilities
     - GTMAppAuth
     - GTMSessionFetcher
+    - GzipSwift
     - libavif
     - libdav1d
     - libwebp
@@ -3279,6 +3282,7 @@ SPEC CHECKSUMS:
   GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
   GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
   GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
+  GzipSwift: 893f3e48e597a1a4f62fafcb6514220fcf8287fa
   hermes-engine: 0555a84ea495e8e3b4bde71b597cd87fbb382888
   libavif: 84bbb62fb232c3018d6f1bab79beea87e35de7b7
   libdav1d: 23581a4d8ec811ff171ed5e2e05cd27bad64c39f
@@ -3400,8 +3404,8 @@ SPEC CHECKSUMS:
   SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
   Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e
   VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c
-  Yoga: 3deb2471faa9916c8a82dda2a22d3fba2620ad37
+  Yoga: f6dc1b6029519815d5516a1241821c6a9074af6d
 
-PODFILE CHECKSUM: 6fc95cc1e80a55665a376c27ca23105e1eda8c64
+PODFILE CHECKSUM: a5d166a2e749f6a67badc7a7c90ac596a94c2099
 
-COCOAPODS: 1.15.2
+COCOAPODS: 1.13.0

From c8511be708befb9d886641b5183c006f8b3198c7 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Fri, 31 Jan 2025 13:51:15 +0100
Subject: [PATCH 07/14] chore: Podfile.lock update

---
 ios/Podfile.lock | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/ios/Podfile.lock b/ios/Podfile.lock
index 723b3a99358b..0958f465480d 100644
--- a/ios/Podfile.lock
+++ b/ios/Podfile.lock
@@ -3404,8 +3404,8 @@ SPEC CHECKSUMS:
   SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748
   Turf: aa2ede4298009639d10db36aba1a7ebaad072a5e
   VisionCamera: c95a8ad535f527562be1fb05fb2fd324578e769c
-  Yoga: f6dc1b6029519815d5516a1241821c6a9074af6d
+  Yoga: 3deb2471faa9916c8a82dda2a22d3fba2620ad37
 
 PODFILE CHECKSUM: a5d166a2e749f6a67badc7a7c90ac596a94c2099
 
-COCOAPODS: 1.13.0
+COCOAPODS: 1.15.2

From 7c0fde2d0e352dbc20bcfdb8990bcb53f11c6f06 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Fri, 31 Jan 2025 14:19:48 +0100
Subject: [PATCH 08/14] chore: update unit test to use more realistic data

---
 .../unit/parsePushNotificationPayloadTest.ts  | 48 +++++++++++++++----
 1 file changed, 39 insertions(+), 9 deletions(-)

diff --git a/tests/unit/parsePushNotificationPayloadTest.ts b/tests/unit/parsePushNotificationPayloadTest.ts
index 8277457afa3f..1566ebbe51d2 100644
--- a/tests/unit/parsePushNotificationPayloadTest.ts
+++ b/tests/unit/parsePushNotificationPayloadTest.ts
@@ -1,25 +1,55 @@
 import parsePushNotificationPayload from '@libs/Notification/PushNotification/parsePushNotificationPayload';
 
+const payloadWithOnyxData = {
+    type: 'reportComment',
+    title: 'Test Payload',
+    onyxData: [
+        {
+            key: 'reportActions_2170976176751360',
+            onyxMethod: 'merge',
+            value: {
+                // eslint-disable-next-line @typescript-eslint/naming-convention
+                '2463291366241014308': {
+                    actionName: 'ADDCOMMENT',
+                    reportID: '2170976176751360',
+                    reportActionID: '2463291366241014308',
+                    message: [
+                        {
+                            type: 'COMMENT',
+                            html: 'Hello world!',
+                            text: 'Hello world!',
+                        },
+                    ],
+                },
+            },
+        },
+    ],
+    lastUpdateID: 4024059044,
+    previousUpdateID: 4024059043,
+    reportID: 2170976176751360,
+    reportActionID: '2463291366241014308',
+    roomName: '',
+    app: 'new',
+};
+// gzip compressed json string
+const compressedPayloadWithOnyxData =
+    'H4sIAAAAAAAAA5WQT0/DMAzFv0rJeYe0zVrKbVqR4NDBYZzQhCxqbRVJEyXen6rqd6fJmBjQC7f4/ew8P/eMOoPsLmIWjba01EphS2wWMWpIBrJGR9EzdFJD7YFuu1MJBCN77dkHdt/ji3dqdOvekjjnRZ7FeZbP4zTjl7EKaadr36/QbtHLB5B7b9OzRGRpUoztWSJiHouU3wYdwqcrUGGbRVkun6rqfrX202fbx9KTKdPrtb66Jlxmfh/nYIvnTJebXBntSEkvPaCUOjpqK+ubcCU80R992AzDsBmpBEcvpgbCYC54Ivi84EKMzFg8NHrvpnj6M9rvZP8JZrVWl9v5GozxzxaPbPgE0MCh7v4BAAA=';
+
 describe('parsePushNotificationPayload', () => {
     it("returns 'undefined' for missing payload", () => {
         expect(parsePushNotificationPayload(undefined)).toBeUndefined();
     });
 
     it('returns untouched input when provided with raw json', () => {
-        const payload = {key: 'value'};
-        expect(parsePushNotificationPayload(payload)).toStrictEqual(payload);
+        expect(parsePushNotificationPayload(payloadWithOnyxData)).toStrictEqual(payloadWithOnyxData);
     });
 
     it('returns correct json when provided with stringified json', () => {
-        const json = {key: 'value'};
-        const payload = JSON.stringify(json);
-        expect(parsePushNotificationPayload(payload)).toStrictEqual(json);
+        const stringifiedPayload = JSON.stringify(payloadWithOnyxData);
+        expect(parsePushNotificationPayload(stringifiedPayload)).toStrictEqual(payloadWithOnyxData);
     });
 
     it('returns correct json when provided with gzip compressed json string', () => {
-        const json = {key: 'value'};
-        // gzip compressed json string
-        const payload = 'H4sIAAAAAAAAA6tWyk6tVLJSKkvMKU1VqgUAv5wYPw8AAAA=';
-        expect(parsePushNotificationPayload(payload)).toStrictEqual(json);
+        expect(parsePushNotificationPayload(compressedPayloadWithOnyxData)).toStrictEqual(payloadWithOnyxData);
     });
 });

From 9a61c8dfabdf1a582bee54933dad9c51ebfa6379 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Fri, 31 Jan 2025 15:14:16 +0100
Subject: [PATCH 09/14] fix: android implementation improvements

---
 .../CustomNotificationProvider.java                    |  2 --
 .../chat/customairshipextender/PayloadHandler.kt       | 10 ++++------
 2 files changed, 4 insertions(+), 8 deletions(-)

diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
index 22362c256231..6d4a596bfdaa 100644
--- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
+++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
@@ -125,8 +125,6 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @
         }
 
         try {
-            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) throw new Exception();
-
             String rawPayload = message.getExtra(PAYLOAD_KEY);
             if (rawPayload == null) throw new Exception();
 
diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt b/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
index 41e1ef60b332..e402f2c70d3c 100644
--- a/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
+++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
@@ -1,27 +1,25 @@
 package com.expensify.chat.customairshipextender
 
 import android.os.Build
-import androidx.annotation.RequiresApi
-import java.util.Base64
+import android.util.Base64
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
 import java.util.zip.GZIPInputStream
 
 class PayloadHandler {
-    private val BUFFER_SIZE = 1024
+    private val BUFFER_SIZE = 4096
     private val GZIP_MAGIC = byteArrayOf(0x1f.toByte(), 0x8b.toByte())
 
-    @RequiresApi(Build.VERSION_CODES.O)
     fun processPayload(payloadString: String): String =
             runCatching {
-                        val decoded = Base64.getDecoder().decode(payloadString)
+                        val decoded = Base64.decode(payloadString, Base64.DEFAULT)
                         if (!isGzipped(decoded)) error("Input not gzipped")
                         return decompressGzip(decoded)
                     }
                     .getOrDefault(payloadString)
 
     private fun isGzipped(decoded: ByteArray) =
-            decoded[0] == GZIP_MAGIC[0] && decoded[1] == GZIP_MAGIC[1]
+            decoded.size >= 2 && decoded[0] == GZIP_MAGIC[0] && decoded[1] == GZIP_MAGIC[1]
 
     private fun decompressGzip(compressed: ByteArray): String {
         ByteArrayInputStream(compressed).use { bis ->

From 1e50439d4b7a5c4feb19d680a3d56e5bdad42617 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Thu, 6 Feb 2025 09:27:54 +0100
Subject: [PATCH 10/14] chore: minor code updates

---
 .../CustomNotificationProvider.java                | 10 ++++++++--
 .../chat/customairshipextender/PayloadHandler.kt   | 14 ++++++--------
 2 files changed, 14 insertions(+), 10 deletions(-)

diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
index 6d4a596bfdaa..fc2ba9db3624 100644
--- a/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
+++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/CustomNotificationProvider.java
@@ -126,12 +126,18 @@ protected NotificationCompat.Builder onExtendBuilder(@NonNull Context context, @
 
         try {
             String rawPayload = message.getExtra(PAYLOAD_KEY);
-            if (rawPayload == null) throw new Exception();
+            if (rawPayload == null) {
+              Log.d(TAG, "Failed to parse payload - payload is empty. SendID=" + message.getSendId());
+              return builder;
+            }
 
             PayloadHandler handler = new PayloadHandler();
             String processedPayload = handler.processPayload(rawPayload);
             JsonMap payload = JsonValue.parseString(processedPayload).optMap();
-            if (!payload.containsKey(ONYX_DATA_KEY)) throw new Exception();
+            if (!payload.containsKey(ONYX_DATA_KEY)) {
+                Log.d(TAG, "Failed to process payload - no onyx data. SendID=" + message.getSendId());
+                return builder;
+            }
 
             Objects.requireNonNull(payload.get(ONYX_DATA_KEY)).isNull();
             Log.d(TAG, "payload contains onxyData");
diff --git a/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt b/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
index e402f2c70d3c..e9ade0eff5e5 100644
--- a/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
+++ b/android/app/src/main/java/com/expensify/chat/customairshipextender/PayloadHandler.kt
@@ -1,6 +1,5 @@
 package com.expensify.chat.customairshipextender
 
-import android.os.Build
 import android.util.Base64
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
@@ -11,15 +10,14 @@ class PayloadHandler {
     private val GZIP_MAGIC = byteArrayOf(0x1f.toByte(), 0x8b.toByte())
 
     fun processPayload(payloadString: String): String =
-            runCatching {
-                        val decoded = Base64.decode(payloadString, Base64.DEFAULT)
-                        if (!isGzipped(decoded)) error("Input not gzipped")
-                        return decompressGzip(decoded)
-                    }
-                    .getOrDefault(payloadString)
+        runCatching {
+            val decoded = Base64.decode(payloadString, Base64.DEFAULT)
+            if (!isGzipped(decoded)) error("Input not gzipped")
+            return decompressGzip(decoded)
+        }.getOrDefault(payloadString)
 
     private fun isGzipped(decoded: ByteArray) =
-            decoded.size >= 2 && decoded[0] == GZIP_MAGIC[0] && decoded[1] == GZIP_MAGIC[1]
+        decoded.size >= 2 && decoded[0] == GZIP_MAGIC[0] && decoded[1] == GZIP_MAGIC[1]
 
     private fun decompressGzip(compressed: ByteArray): String {
         ByteArrayInputStream(compressed).use { bis ->

From bad3241f1c3b64595ce43ee450ee89653c739780 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Thu, 6 Feb 2025 09:47:31 +0100
Subject: [PATCH 11/14] re-run tests


From 799b1495547f646605e08ff3234763012501add2 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Wed, 19 Feb 2025 12:48:54 +0100
Subject: [PATCH 12/14] Minor improvement

---
 tests/unit/parsePushNotificationPayloadTest.ts | 1 +
 1 file changed, 1 insertion(+)

diff --git a/tests/unit/parsePushNotificationPayloadTest.ts b/tests/unit/parsePushNotificationPayloadTest.ts
index 1566ebbe51d2..d2c0265e9836 100644
--- a/tests/unit/parsePushNotificationPayloadTest.ts
+++ b/tests/unit/parsePushNotificationPayloadTest.ts
@@ -31,6 +31,7 @@ const payloadWithOnyxData = {
     roomName: '',
     app: 'new',
 };
+
 // gzip compressed json string
 const compressedPayloadWithOnyxData =
     'H4sIAAAAAAAAA5WQT0/DMAzFv0rJeYe0zVrKbVqR4NDBYZzQhCxqbRVJEyXen6rqd6fJmBjQC7f4/ew8P/eMOoPsLmIWjba01EphS2wWMWpIBrJGR9EzdFJD7YFuu1MJBCN77dkHdt/ji3dqdOvekjjnRZ7FeZbP4zTjl7EKaadr36/QbtHLB5B7b9OzRGRpUoztWSJiHouU3wYdwqcrUGGbRVkun6rqfrX202fbx9KTKdPrtb66Jlxmfh/nYIvnTJebXBntSEkvPaCUOjpqK+ubcCU80R992AzDsBmpBEcvpgbCYC54Ivi84EKMzFg8NHrvpnj6M9rvZP8JZrVWl9v5GozxzxaPbPgE0MCh7v4BAAA=';

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


From 1fc6c25dd43ebec3bc8457a299df22c9e5f7df98 Mon Sep 17 00:00:00 2001
From: VickyStash <vikstash@gmail.com>
Date: Fri, 21 Feb 2025 09:31:19 +0100
Subject: [PATCH 14/14] package-lock.json updates

---
 package-lock.json | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/package-lock.json b/package-lock.json
index 74b9c136c4f6..1be890a95786 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,6 +44,7 @@
         "@react-ng/bounds-observer": "^0.2.1",
         "@rnmapbox/maps": "10.1.33",
         "@shopify/flash-list": "1.7.1",
+        "@types/pako": "^2.0.3",
         "@ua/react-native-airship": "19.2.1",
         "awesome-phonenumber": "^5.4.0",
         "babel-polyfill": "^6.26.0",
@@ -69,6 +70,7 @@
         "lottie-react-native": "6.5.1",
         "mapbox-gl": "^2.15.0",
         "onfido-sdk-ui": "14.42.0",
+        "pako": "^2.1.0",
         "process": "^0.11.10",
         "pusher-js": "8.3.0",
         "react": "18.3.1",
@@ -13846,6 +13848,12 @@
         "@types/node": "*"
       }
     },
+    "node_modules/@types/pako": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.3.tgz",
+      "integrity": "sha512-bq0hMV9opAcrmE0Byyo0fY3Ew4tgOevJmQ9grUhpXQhYfyLJ1Kqg3P33JT5fdbT2AjeAjR51zqqVjAL/HMkx7Q==",
+      "license": "MIT"
+    },
     "node_modules/@types/parse-json": {
       "version": "4.0.0",
       "dev": true,
@@ -30634,6 +30642,12 @@
       "version": "1.0.0",
       "license": "BlueOak-1.0.0"
     },
+    "node_modules/pako": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz",
+      "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==",
+      "license": "(MIT AND Zlib)"
+    },
     "node_modules/param-case": {
       "version": "3.0.4",
       "dev": true,
