From 0c70673ec351428d8a096f37b6897853cf1c063f Mon Sep 17 00:00:00 2001 From: Adam Horodyski 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 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 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 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 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 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 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 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 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 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 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 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 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 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,