diff --git a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNavigationTarget.swift b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNavigationTarget.swift index bde4a4494c..926283a3b0 100644 --- a/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNavigationTarget.swift +++ b/mobile/ios/BuzzPushKit/Sources/BuzzPushKit/BuzzPushNavigationTarget.swift @@ -56,7 +56,6 @@ public struct BuzzPushNavigationTarget: Codable, Equatable, Sendable { let bytes = Array(value.utf8) guard bytes.count == 36, bytes[8] == 45, bytes[13] == 45, bytes[18] == 45, bytes[23] == 45, - bytes[14] == 52, [56, 57, 97, 98].contains(bytes[19]) else { return false } return bytes.enumerated().allSatisfy { index, byte in diff --git a/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNavigationTargetTests.swift b/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNavigationTargetTests.swift index 7acd9f5c53..4b9853e2c9 100644 --- a/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNavigationTargetTests.swift +++ b/mobile/ios/BuzzPushKit/Tests/BuzzPushKitTests/BuzzPushNavigationTargetTests.swift @@ -3,7 +3,7 @@ import Testing @testable import BuzzPushKit -@Test func `Round-trip navigation target through notification user info`() { +@Test func `Round-trip UUIDv4 navigation target through notification user info`() { let target = BuzzPushNavigationTarget( eventID: String(repeating: "A", count: 64), communityID: "community-id", @@ -19,7 +19,22 @@ import Testing #expect(target.channelID == "123e4567-e89b-42d3-a456-426614174000") } -@Test func `Reject incomplete navigation target`() { +@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") +} + +@Test func `Reject incomplete or malformed navigation target`() { #expect( BuzzPushNavigationTarget.decodeIfPresent( from: [ @@ -42,6 +57,18 @@ import Testing ] ) == nil ) + + #expect( + BuzzPushNavigationTarget.decodeIfPresent( + from: [ + BuzzPushNavigationTarget.userInfoKey: [ + "event_id": String(repeating: "a", count: 64), + "community_id": "community-id", + "channel_id": "9a1657ac-f7aa-5db0-7632-d8bbeb6dfb50", + ] + ] + ) == nil + ) } @Test func `Buffer preserves cold-start target until consumed`() { diff --git a/mobile/lib/shared/push/push_bridge.dart b/mobile/lib/shared/push/push_bridge.dart index c91c24ed87..3d5a818092 100644 --- a/mobile/lib/shared/push/push_bridge.dart +++ b/mobile/lib/shared/push/push_bridge.dart @@ -26,7 +26,7 @@ final pendingPushNotificationLink = ValueNotifier(null); final _pushEventIdPattern = RegExp(r'^[0-9a-f]{64}$'); final _pushChannelIdPattern = RegExp( - r'^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$', + 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) { diff --git a/mobile/test/shared/push/push_bridge_test.dart b/mobile/test/shared/push/push_bridge_test.dart index e9445e88ca..f59dd65660 100644 --- a/mobile/test/shared/push/push_bridge_test.dart +++ b/mobile/test/shared/push/push_bridge_test.dart @@ -150,7 +150,7 @@ void main() { expect(apnsRegistrationError.value, 'denied'); }); - test('turns a warm notification response into a message link', () async { + test('routes a warm notification response with a UUIDv4 channel', () async { final response = Completer(); await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .handlePlatformMessage( @@ -178,6 +178,58 @@ void main() { ); }); + test('routes a warm notification response with a UUIDv5 channel', () async { + final response = Completer(); + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + _channel.name, + _channel.codec.encodeMethodCall( + MethodCall('notificationOpened', { + 'eventId': 'b' * 64, + 'communityId': 'community-id', + 'channelId': '9A1657AC-F7AA-5DB0-B632-D8BBEB6DFB50', + }), + ), + response.complete, + ); + + 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, + ), + ); + }); + + test( + 'rejects a notification response with a malformed channel UUID', + () async { + final response = Completer(); + await TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + _channel.name, + _channel.codec.encodeMethodCall( + MethodCall('notificationOpened', { + 'eventId': 'c' * 64, + 'communityId': 'community-id', + 'channelId': '9a1657ac-f7aa-5db0-7632-d8bbeb6dfb50', + }), + ), + 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