mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 07:47:28 +00:00
iOS: add APNs registration and notification signing config (#20308)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 614180020e
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
@@ -62,6 +62,7 @@
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict/>
|
||||
|
||||
@@ -127,6 +127,8 @@ final class NodeAppModel {
|
||||
private var operatorConnected = false
|
||||
private var shareDeliveryChannel: String?
|
||||
private var shareDeliveryTo: String?
|
||||
private var apnsDeviceTokenHex: String?
|
||||
private var apnsLastRegisteredTokenHex: String?
|
||||
var gatewaySession: GatewayNodeSession { self.nodeGateway }
|
||||
var operatorSession: GatewayNodeSession { self.operatorGateway }
|
||||
private(set) var activeGatewayConnectConfig: GatewayConnectConfig?
|
||||
@@ -164,6 +166,7 @@ final class NodeAppModel {
|
||||
self.motionService = motionService
|
||||
self.watchMessagingService = watchMessagingService
|
||||
self.talkMode = talkMode
|
||||
self.apnsDeviceTokenHex = UserDefaults.standard.string(forKey: Self.apnsDeviceTokenUserDefaultsKey)
|
||||
GatewayDiagnostics.bootstrap()
|
||||
|
||||
self.voiceWake.configure { [weak self] cmd in
|
||||
@@ -409,6 +412,14 @@ final class NodeAppModel {
|
||||
}
|
||||
|
||||
private static let defaultSeamColor = Color(red: 79 / 255.0, green: 122 / 255.0, blue: 154 / 255.0)
|
||||
private static let apnsDeviceTokenUserDefaultsKey = "push.apns.deviceTokenHex"
|
||||
private static var apnsEnvironment: String {
|
||||
#if DEBUG
|
||||
"sandbox"
|
||||
#else
|
||||
"production"
|
||||
#endif
|
||||
}
|
||||
|
||||
private static func color(fromHex raw: String?) -> Color? {
|
||||
let trimmed = (raw ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
@@ -1702,6 +1713,7 @@ private extension NodeAppModel {
|
||||
self.gatewayDefaultAgentId = nil
|
||||
self.gatewayAgents = []
|
||||
self.selectedAgentId = GatewaySettingsStore.loadGatewaySelectedAgentId(stableID: stableID)
|
||||
self.apnsLastRegisteredTokenHex = nil
|
||||
}
|
||||
|
||||
func startOperatorGatewayLoop(
|
||||
@@ -2109,7 +2121,55 @@ extension NodeAppModel {
|
||||
}
|
||||
|
||||
/// Back-compat hook retained for older gateway-connect flows.
|
||||
func onNodeGatewayConnected() async {}
|
||||
func onNodeGatewayConnected() async {
|
||||
await self.registerAPNsTokenIfNeeded()
|
||||
}
|
||||
|
||||
func updateAPNsDeviceToken(_ tokenData: Data) {
|
||||
let tokenHex = tokenData.map { String(format: "%02x", $0) }.joined()
|
||||
let trimmed = tokenHex.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
self.apnsDeviceTokenHex = trimmed
|
||||
UserDefaults.standard.set(trimmed, forKey: Self.apnsDeviceTokenUserDefaultsKey)
|
||||
Task { [weak self] in
|
||||
await self?.registerAPNsTokenIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private func registerAPNsTokenIfNeeded() async {
|
||||
guard self.gatewayConnected else { return }
|
||||
guard let token = self.apnsDeviceTokenHex?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!token.isEmpty
|
||||
else {
|
||||
return
|
||||
}
|
||||
if token == self.apnsLastRegisteredTokenHex {
|
||||
return
|
||||
}
|
||||
guard let topic = Bundle.main.bundleIdentifier?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!topic.isEmpty
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
struct PushRegistrationPayload: Codable {
|
||||
var token: String
|
||||
var topic: String
|
||||
var environment: String
|
||||
}
|
||||
|
||||
let payload = PushRegistrationPayload(
|
||||
token: token,
|
||||
topic: topic,
|
||||
environment: Self.apnsEnvironment)
|
||||
do {
|
||||
let json = try Self.encodePayload(payload)
|
||||
await self.nodeGateway.sendEvent(event: "push.apns.register", payloadJSON: json)
|
||||
self.apnsLastRegisteredTokenHex = token
|
||||
} catch {
|
||||
// Best-effort only.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#if DEBUG
|
||||
|
||||
9
apps/ios/Sources/OpenClaw.entitlements
Normal file
9
apps/ios/Sources/OpenClaw.entitlements
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,10 +1,51 @@
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
import os
|
||||
import UIKit
|
||||
|
||||
final class OpenClawAppDelegate: NSObject, UIApplicationDelegate {
|
||||
private let logger = Logger(subsystem: "ai.openclaw.ios", category: "Push")
|
||||
private var pendingAPNsDeviceToken: Data?
|
||||
weak var appModel: NodeAppModel? {
|
||||
didSet {
|
||||
guard let model = self.appModel, let token = self.pendingAPNsDeviceToken else { return }
|
||||
self.pendingAPNsDeviceToken = nil
|
||||
Task { @MainActor in
|
||||
model.updateAPNsDeviceToken(token)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil
|
||||
) -> Bool
|
||||
{
|
||||
application.registerForRemoteNotifications()
|
||||
return true
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
if let appModel = self.appModel {
|
||||
Task { @MainActor in
|
||||
appModel.updateAPNsDeviceToken(deviceToken)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.pendingAPNsDeviceToken = deviceToken
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: any Error) {
|
||||
self.logger.error("APNs registration failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
@main
|
||||
struct OpenClawApp: App {
|
||||
@State private var appModel: NodeAppModel
|
||||
@State private var gatewayController: GatewayConnectionController
|
||||
@UIApplicationDelegateAdaptor(OpenClawAppDelegate.self) private var appDelegate
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
init() {
|
||||
@@ -21,6 +62,9 @@ struct OpenClawApp: App {
|
||||
.environment(self.appModel)
|
||||
.environment(self.appModel.voiceWake)
|
||||
.environment(self.gatewayController)
|
||||
.task {
|
||||
self.appDelegate.appModel = self.appModel
|
||||
}
|
||||
.onOpenURL { url in
|
||||
Task { await self.appModel.handleDeepLink(url: url) }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user