Commit 81e97f81a5

81e97f81a573a28f4e270b0b4f85b34b03a706f6

parent: f5ab7cf868

Verified · cmc

cmc <hello@cleberg.net> · 2026-09-20 21:36 UTC

push: decode a notification payload

instance and user name the account a notice belongs to; path alone is
ambiguous when one install is registered against several.

Ref krz/gitbay#89

Layout: unified · split

gitbay/Push/PushPayload.swift added +28
@@ -0,0 +1,28 @@
1import Foundation
2
3/// A push notification as the app reads it.
4///
5/// `instance` and `user` name the account the notice belongs to. One
6/// device token is one install, and an install registers against every
7/// account signed in on it, so `path` alone is ambiguous — two
8/// instances can hold the same `owner/name`.
9nonisolated struct PushPayload: Equatable, Sendable {
10 let instance: URL
11 let user: String
12 let path: String
13 /// The recipient's unread count, for the app icon badge. Absent on
14 /// a notification from a server that predates it.
15 let badge: Int
16
17 init?(userInfo: [AnyHashable: Any]) {
18 guard let raw = userInfo["instance"] as? String,
19 let instance = URL(string: raw), instance.scheme != nil,
20 let user = userInfo["user"] as? String, !user.isEmpty,
21 let path = userInfo["path"] as? String, !path.isEmpty
22 else { return nil }
23 self.instance = instance
24 self.user = user
25 self.path = path
26 self.badge = (userInfo["aps"] as? [AnyHashable: Any])?["badge"] as? Int ?? 0
27 }
28}
gitbayTests/PushPayloadTests.swift added +34
@@ -0,0 +1,34 @@
1import Foundation
2import Testing
3@testable import gitbay
4
5@Test func payloadDecodesTheAccountAndPath() throws {
6 let userInfo: [AnyHashable: Any] = [
7 "aps": ["alert": ["title": "krz/gitbay", "body": "cmc opened issue #12"], "badge": 3],
8 "instance": "https://gitbay.org",
9 "user": "cmc",
10 "path": "krz/gitbay/issues/12",
11 ]
12 let payload = try #require(PushPayload(userInfo: userInfo))
13 #expect(payload.instance == URL(string: "https://gitbay.org"))
14 #expect(payload.user == "cmc")
15 #expect(payload.path == "krz/gitbay/issues/12")
16 #expect(payload.badge == 3)
17}
18
19@Test func payloadTreatsAMissingBadgeAsZero() throws {
20 let userInfo: [AnyHashable: Any] = [
21 "aps": ["alert": ["title": "t", "body": "b"]],
22 "instance": "https://gitbay.org", "user": "cmc", "path": "a/b/issues/1",
23 ]
24 let payload = try #require(PushPayload(userInfo: userInfo))
25 #expect(payload.badge == 0)
26}
27
28// A payload from a server that predates the account fields, or one
29// mangled in transit, is dropped rather than guessed at.
30@Test func payloadRejectsAnIncompleteNotification() {
31 #expect(PushPayload(userInfo: ["aps": ["alert": "x"], "path": "a/b/issues/1"]) == nil)
32 #expect(PushPayload(userInfo: ["instance": "https://gitbay.org", "user": "cmc"]) == nil)
33 #expect(PushPayload(userInfo: ["instance": "not a url at all", "user": "cmc", "path": "a/b/issues/1"]) == nil)
34}