@@ -127,6 +127,8 @@ final class NodeAppModel {
127127 private var operatorConnected = false
128128 private var shareDeliveryChannel : String ?
129129 private var shareDeliveryTo : String ?
130+ private var apnsDeviceTokenHex : String ?
131+ private var apnsLastRegisteredTokenHex : String ?
130132 var gatewaySession : GatewayNodeSession { self . nodeGateway }
131133 var operatorSession : GatewayNodeSession { self . operatorGateway }
132134 private( set) var activeGatewayConnectConfig : GatewayConnectConfig ?
@@ -164,6 +166,7 @@ final class NodeAppModel {
164166 self . motionService = motionService
165167 self . watchMessagingService = watchMessagingService
166168 self . talkMode = talkMode
169+ self . apnsDeviceTokenHex = UserDefaults . standard. string ( forKey: Self . apnsDeviceTokenUserDefaultsKey)
167170 GatewayDiagnostics . bootstrap ( )
168171
169172 self . voiceWake. configure { [ weak self] cmd in
@@ -409,6 +412,14 @@ final class NodeAppModel {
409412 }
410413
411414 private static let defaultSeamColor = Color ( red: 79 / 255.0 , green: 122 / 255.0 , blue: 154 / 255.0 )
415+ private static let apnsDeviceTokenUserDefaultsKey = " push.apns.deviceTokenHex "
416+ private static var apnsEnvironment : String {
417+ #if DEBUG
418+ " sandbox "
419+ #else
420+ " production "
421+ #endif
422+ }
412423
413424 private static func color( fromHex raw: String ? ) -> Color ? {
414425 let trimmed = ( raw ?? " " ) . trimmingCharacters ( in: . whitespacesAndNewlines)
@@ -1702,6 +1713,7 @@ private extension NodeAppModel {
17021713 self . gatewayDefaultAgentId = nil
17031714 self . gatewayAgents = [ ]
17041715 self . selectedAgentId = GatewaySettingsStore . loadGatewaySelectedAgentId ( stableID: stableID)
1716+ self . apnsLastRegisteredTokenHex = nil
17051717 }
17061718
17071719 func startOperatorGatewayLoop(
@@ -2109,7 +2121,55 @@ extension NodeAppModel {
21092121 }
21102122
21112123 /// Back-compat hook retained for older gateway-connect flows.
2112- func onNodeGatewayConnected( ) async { }
2124+ func onNodeGatewayConnected( ) async {
2125+ await self . registerAPNsTokenIfNeeded ( )
2126+ }
2127+
2128+ func updateAPNsDeviceToken( _ tokenData: Data ) {
2129+ let tokenHex = tokenData. map { String ( format: " %02x " , $0) } . joined ( )
2130+ let trimmed = tokenHex. trimmingCharacters ( in: . whitespacesAndNewlines)
2131+ guard !trimmed. isEmpty else { return }
2132+ self . apnsDeviceTokenHex = trimmed
2133+ UserDefaults . standard. set ( trimmed, forKey: Self . apnsDeviceTokenUserDefaultsKey)
2134+ Task { [ weak self] in
2135+ await self ? . registerAPNsTokenIfNeeded ( )
2136+ }
2137+ }
2138+
2139+ private func registerAPNsTokenIfNeeded( ) async {
2140+ guard self . gatewayConnected else { return }
2141+ guard let token = self . apnsDeviceTokenHex? . trimmingCharacters ( in: . whitespacesAndNewlines) ,
2142+ !token. isEmpty
2143+ else {
2144+ return
2145+ }
2146+ if token == self . apnsLastRegisteredTokenHex {
2147+ return
2148+ }
2149+ guard let topic = Bundle . main. bundleIdentifier? . trimmingCharacters ( in: . whitespacesAndNewlines) ,
2150+ !topic. isEmpty
2151+ else {
2152+ return
2153+ }
2154+
2155+ struct PushRegistrationPayload : Codable {
2156+ var token : String
2157+ var topic : String
2158+ var environment : String
2159+ }
2160+
2161+ let payload = PushRegistrationPayload (
2162+ token: token,
2163+ topic: topic,
2164+ environment: Self . apnsEnvironment)
2165+ do {
2166+ let json = try Self . encodePayload ( payload)
2167+ await self . nodeGateway. sendEvent ( event: " push.apns.register " , payloadJSON: json)
2168+ self . apnsLastRegisteredTokenHex = token
2169+ } catch {
2170+ // Best-effort only.
2171+ }
2172+ }
21132173}
21142174
21152175#if DEBUG
0 commit comments