mobiler 0.39.0

Build mobile apps in Rust — one core, native UI on Android, iOS, and the web (CLI)
import Foundation
import SharedTypes
import UIKit
import UserNotifications

// Remote push (free, bundled). Native APNs via system frameworks — no third-party SDK. Two surfaces:
//   cx.plugin("push", "register", "", |r| Msg::PushToken(r))          // → {"token":"…","platform":"apns"}
//   cx.subscribe("push", "push", "events", "", |r| Msg::PushEvent(r)) // received/tapped payloads
//
// The actual APNs plumbing lives in the shell's AppDelegate + `PushBridge` (in App.swift) — only the
// AppDelegate receives the OS token + notification callbacks. This plugin is the thin cx adapter:
// `register` asks for authorization + registers for remote notifications, then awaits the token the
// AppDelegate forwards to PushBridge; `subscribe` attaches an emit sink to PushBridge (which buffers
// any notification that arrived before the core subscribed — e.g. a tap that launched the app).
//
// Needs the `aps-environment` entitlement + Push Notifications enabled on the App ID (the plugin
// manifest adds the entitlement; the dev-portal capability is the one manual iOS step).
enum PushPlugin {
    static func handle(op: String, input: String) async -> PluginResponse {
        switch op {
        case "register": return await register()
        default: return PluginResponse(ok: false, output: "unknown op '\(op)'")
        }
    }

    // Streaming entrypoint (cx.subscribe): attach to PushBridge and stay parked until the
    // subscription's Task is cancelled (cx.unsubscribe), then detach.
    static func subscribe(op: String, input: String, emit: @escaping @Sendable (PluginResponse) -> Void) async {
        let sink: @Sendable (String) -> Void = { emit(PluginResponse(ok: true, output: $0)) }
        await MainActor.run { PushBridge.shared.attach(sink) }
        await withTaskCancellationHandler {
            while !Task.isCancelled {
                try? await Task.sleep(nanoseconds: 1_000_000_000)
            }
        } onCancel: {
            Task { @MainActor in PushBridge.shared.detach() }
        }
    }

    private static func register() async -> PluginResponse {
        do {
            let granted = try await UNUserNotificationCenter.current()
                .requestAuthorization(options: [.alert, .sound, .badge])
            guard granted else { return PluginResponse(ok: false, output: "denied") }
            await MainActor.run { UIApplication.shared.registerForRemoteNotifications() }
            // The AppDelegate's didRegister…DeviceToken resolves this with the hex APNs token.
            let token = try await PushBridge.shared.awaitToken()
            return PluginResponse(ok: true, output: "{\"token\":\"\(token)\",\"platform\":\"apns\"}")
        } catch {
            return PluginResponse(ok: false, output: error.localizedDescription)
        }
    }
}