diff --git a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNavigationTarget.swift b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNavigationTarget.swift index 926283a3b0..9218655021 100644 --- a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNavigationTarget.swift +++ b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNavigationTarget.swift @@ -10,9 +10,9 @@ public struct BuzzPushNavigationTarget: Codable, Equatable, Sendable { public let channelID: String public init(eventID: String, communityID: String, channelID: String) { - self.eventID = eventID.lowercased() + self.eventID = eventID self.communityID = communityID - self.channelID = channelID.lowercased() + self.channelID = channelID } public var userInfoValue: [String: String] { @@ -32,9 +32,9 @@ public struct BuzzPushNavigationTarget: Codable, Equatable, Sendable { let eventID = raw["event_id"] as? String, let communityID = raw["community_id"] as? String, let channelID = raw["channel_id"] as? String, + !eventID.isEmpty, !communityID.isEmpty, - isLowercaseHex64(eventID.lowercased()), - isChannelID(channelID.lowercased()) + !channelID.isEmpty else { return nil } @@ -45,24 +45,6 @@ public struct BuzzPushNavigationTarget: Codable, Equatable, Sendable { ) } - private static func isLowercaseHex64(_ value: String) -> Bool { - value.utf8.count == 64 - && value.utf8.allSatisfy { - ($0 >= 48 && $0 <= 57) || ($0 >= 97 && $0 <= 102) - } - } - - private static func isChannelID(_ value: String) -> Bool { - let bytes = Array(value.utf8) - guard bytes.count == 36, - bytes[8] == 45, bytes[13] == 45, bytes[18] == 45, bytes[23] == 45, - [56, 57, 97, 98].contains(bytes[19]) - else { return false } - return bytes.enumerated().allSatisfy { index, byte in - if [8, 13, 18, 23].contains(index) { return byte == 45 } - return (byte >= 48 && byte <= 57) || (byte >= 97 && byte <= 102) - } - } } /// Thread-safe one-item buffer spanning notification delivery and Flutter diff --git a/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNavigationTargetTests.swift b/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNavigationTargetTests.swift index 4b9853e2c9..414e9446a2 100644 --- a/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNavigationTargetTests.swift +++ b/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNavigationTargetTests.swift @@ -3,11 +3,11 @@ import Testing @testable import BuzzPushKit -@Test func `Round-trip UUIDv4 navigation target through notification user info`() { +@Test func `Round-trip opaque navigation target through notification user info`() { let target = BuzzPushNavigationTarget( - eventID: String(repeating: "A", count: 64), + eventID: "MESSAGE-ID", communityID: "community-id", - channelID: "123E4567-E89B-42D3-A456-426614174000" + channelID: "CHANNEL/GENERAL" ) #expect( @@ -15,23 +15,8 @@ import Testing from: [BuzzPushNavigationTarget.userInfoKey: target.userInfoValue] ) == target ) - #expect(target.eventID == String(repeating: "a", count: 64)) - #expect(target.channelID == "123e4567-e89b-42d3-a456-426614174000") -} - -@Test func `Decode UUIDv5 navigation target`() { - let target = BuzzPushNavigationTarget( - eventID: String(repeating: "B", count: 64), - communityID: "community-id", - channelID: "9A1657AC-F7AA-5DB0-B632-D8BBEB6DFB50" - ) - - #expect( - BuzzPushNavigationTarget.decodeIfPresent( - from: [BuzzPushNavigationTarget.userInfoKey: target.userInfoValue] - ) == target - ) - #expect(target.channelID == "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50") + #expect(target.eventID == "MESSAGE-ID") + #expect(target.channelID == "CHANNEL/GENERAL") } @Test func `Reject incomplete or malformed navigation target`() { @@ -39,7 +24,7 @@ import Testing BuzzPushNavigationTarget.decodeIfPresent( from: [ BuzzPushNavigationTarget.userInfoKey: [ - "event_id": "event-id", + "event_id": "message-id", "community_id": "community-id", ] ] @@ -50,9 +35,9 @@ import Testing BuzzPushNavigationTarget.decodeIfPresent( from: [ BuzzPushNavigationTarget.userInfoKey: [ - "event_id": String(repeating: "a", count: 64), + "event_id": "", "community_id": "community-id", - "channel_id": "not-a-channel", + "channel_id": "channel-id", ] ] ) == nil @@ -62,9 +47,9 @@ import Testing BuzzPushNavigationTarget.decodeIfPresent( from: [ BuzzPushNavigationTarget.userInfoKey: [ - "event_id": String(repeating: "a", count: 64), + "event_id": "message-id", "community_id": "community-id", - "channel_id": "9a1657ac-f7aa-5db0-7632-d8bbeb6dfb50", + "channel_id": "", ] ] ) == nil diff --git a/mobile/lib/shared/push/push_bridge.dart b/mobile/lib/shared/push/push_bridge.dart index 3d5a818092..4e3fc6f887 100644 --- a/mobile/lib/shared/push/push_bridge.dart +++ b/mobile/lib/shared/push/push_bridge.dart @@ -24,32 +24,23 @@ final pushEndpointGrantError = ValueNotifier(null); /// notifier also carries warm responses into the existing deep-link pipeline. final pendingPushNotificationLink = ValueNotifier(null); -final _pushEventIdPattern = RegExp(r'^[0-9a-f]{64}$'); -final _pushChannelIdPattern = RegExp( - r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', -); - MessageDeepLink? _pushNotificationLink(Object? arguments) { if (arguments is! Map) return null; final eventId = arguments['eventId']; final communityId = arguments['communityId']; final channelId = arguments['channelId']; - final normalizedEventId = eventId is String ? eventId.toLowerCase() : ''; - final normalizedChannelId = channelId is String - ? channelId.toLowerCase() - : ''; if (eventId is! String || - !_pushEventIdPattern.hasMatch(normalizedEventId) || + eventId.isEmpty || communityId is! String || communityId.isEmpty || channelId is! String || - !_pushChannelIdPattern.hasMatch(normalizedChannelId)) { + channelId.isEmpty) { return null; } return MessageDeepLink( communityId: communityId, - channelId: normalizedChannelId, - messageId: normalizedEventId, + channelId: channelId, + messageId: eventId, ); } diff --git a/mobile/test/shared/push/push_bridge_test.dart b/mobile/test/shared/push/push_bridge_test.dart index f59dd65660..730e011d6e 100644 --- a/mobile/test/shared/push/push_bridge_test.dart +++ b/mobile/test/shared/push/push_bridge_test.dart @@ -150,16 +150,16 @@ void main() { expect(apnsRegistrationError.value, 'denied'); }); - test('routes a warm notification response with a UUIDv4 channel', () async { + test('routes a warm notification response with opaque IDs', () async { final response = Completer(); await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .handlePlatformMessage( _channel.name, _channel.codec.encodeMethodCall( MethodCall('notificationOpened', { - 'eventId': 'a' * 64, + 'eventId': 'MESSAGE-ID', 'communityId': 'community-id', - 'channelId': '123e4567-e89b-42d3-a456-426614174000', + 'channelId': 'CHANNEL/GENERAL', }), ), response.complete, @@ -172,22 +172,22 @@ void main() { pendingPushNotificationLink.value, MessageDeepLink( communityId: 'community-id', - channelId: '123e4567-e89b-42d3-a456-426614174000', - messageId: 'a' * 64, + channelId: 'CHANNEL/GENERAL', + messageId: 'MESSAGE-ID', ), ); }); - test('routes a warm notification response with a UUIDv5 channel', () async { + test('rejects a notification response with an empty channel ID', () async { final response = Completer(); await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .handlePlatformMessage( _channel.name, _channel.codec.encodeMethodCall( MethodCall('notificationOpened', { - 'eventId': 'b' * 64, + 'eventId': 'message-id', 'communityId': 'community-id', - 'channelId': '9A1657AC-F7AA-5DB0-B632-D8BBEB6DFB50', + 'channelId': '', }), ), response.complete, @@ -195,19 +195,12 @@ void main() { final envelope = await response.future; expect(envelope, isNotNull); - expect(_channel.codec.decodeEnvelope(envelope!), 'handled'); - expect( - pendingPushNotificationLink.value, - MessageDeepLink( - communityId: 'community-id', - channelId: '9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50', - messageId: 'b' * 64, - ), - ); + expect(_channel.codec.decodeEnvelope(envelope!), 'ignored'); + expect(pendingPushNotificationLink.value, isNull); }); test( - 'rejects a notification response with a malformed channel UUID', + 'rejects a notification response with a non-string message ID', () async { final response = Completer(); await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger @@ -215,9 +208,9 @@ void main() { _channel.name, _channel.codec.encodeMethodCall( MethodCall('notificationOpened', { - 'eventId': 'c' * 64, + 'eventId': 42, 'communityId': 'community-id', - 'channelId': '9a1657ac-f7aa-5db0-7632-d8bbeb6dfb50', + 'channelId': 'channel-id', }), ), response.complete, @@ -230,6 +223,26 @@ void main() { }, ); + test('rejects a notification response with an absent message ID', () async { + final response = Completer(); + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + _channel.name, + _channel.codec.encodeMethodCall( + const MethodCall('notificationOpened', { + 'communityId': 'community-id', + 'channelId': 'channel-id', + }), + ), + response.complete, + ); + + final envelope = await response.future; + expect(envelope, isNotNull); + expect(_channel.codec.decodeEnvelope(envelope!), 'ignored'); + expect(pendingPushNotificationLink.value, isNull); + }); + test('pulls a cold-start notification response from native iOS', () async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger