| index 0e66d91..a1ee0e9 100644 |
| --- a/ios/LChat/PushManager.swift |
| +++ b/ios/LChat/PushManager.swift |
| @@ -108,6 +108,34 @@ final class PushManager: NSObject, ObservableObject, UIApplicationDelegate, UNUs |
| URLSession.shared.dataTask(with: req).resume() // best-effort |
| } |
| |
| + /// Tear this device's subscription down on every instance — the sign-out |
| + /// path. Without it a signed-out phone keeps getting pushed sealed |
| + /// payloads forever: the wipe removed the keys, so the NSE can neither |
| + /// decrypt them nor drop them (dropping needs the filtering entitlement |
| + /// we don't hold yet). Must run BEFORE the wipe — these calls authenticate |
| + /// with the very instance tokens the wipe erases — and is awaited so the |
| + /// wipe can't race it; a dead instance only costs its own short timeout. |
| + func unsubscribeAll() async { |
| + guard let hex = UserDefaults.standard.string(forKey: Self.tokenKey) else { return } |
| + await withTaskGroup(of: Void.self) { group in |
| + for inst in InstancesStore.load() { |
| + group.addTask { await Self.unsubscribe(inst, apnsToken: hex) } |
| + } |
| + } |
| + } |
| + |
| + private static func unsubscribe(_ inst: SavedInstance, apnsToken: String) async { |
| + guard let base = inst.baseURL else { return } |
| + var req = URLRequest(url: base.appendingPathComponent("api/apns/unsubscribe")) |
| + req.httpMethod = "POST" |
| + req.setValue("application/json", forHTTPHeaderField: "Content-Type") |
| + req.timeoutInterval = 5 // sign-out must not hang on an unreachable space |
| + req.httpBody = try? JSONSerialization.data(withJSONObject: [ |
| + "token": inst.deviceToken, "apns_token": apnsToken, |
| + ]) |
| + _ = try? await URLSession.shared.data(for: req) |
| + } |
| + |
| // MARK: Presentation + taps |
| |
| // Which chat is on-screen right now (set by the pushed ChatView) — a banner |