惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 三生石上(FineUI控件)
博客园 - Franky
GbyAI
GbyAI
B
Blog
WordPress大学
WordPress大学
D
Docker
小众软件
小众软件
月光博客
月光博客
博客园 - 【当耐特】
T
The Blog of Author Tim Ferriss
IT之家
IT之家
腾讯CDC
Engineering at Meta
Engineering at Meta
Vercel News
Vercel News
H
Help Net Security
M
MIT News - Artificial intelligence
L
LangChain Blog
云风的 BLOG
云风的 BLOG
S
SegmentFault 最新的问题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
美团技术团队
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
V
V2EX
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix: gate ios push enrollment on notification consent · o...
joshavant · 2026-06-23 · via Recent Commits to openclaw:main
Original file line numberDiff line numberDiff line change

@@ -152,6 +152,7 @@ extension SettingsProTab {

152152

}

153153

let notificationSettings = await UNUserNotificationCenter.current().notificationSettings()

154154

self.applyNotificationStatus(notificationSettings.authorizationStatus)

155+

self.registerForRemoteNotificationsIfEnrollmentReady()

155156
156157

let issueCount = SettingsDiagnostics.issueCount(

157158

gatewayConnected: self.gatewayDiagnosticConnected,

@@ -417,6 +418,7 @@ extension SettingsProTab {

417418

let status = settings.authorizationStatus

418419

Task { @MainActor in

419420

self.applyNotificationStatus(status)

421+

self.registerForRemoteNotificationsIfEnrollmentReady()

420422

}

421423

}

422424

}

@@ -437,6 +439,7 @@ extension SettingsProTab {

437439
438440

func requestNotificationAuthorizationFromSettings() {

439441

guard !self.isRequestingNotificationAuthorization else { return }

442+

PushEnrollmentConsent.markDisclosureAccepted()

440443

self.isRequestingNotificationAuthorization = true

441444

Task {

442445

let granted = await (try? UNUserNotificationCenter.current().requestAuthorization(options: [

@@ -448,12 +451,19 @@ extension SettingsProTab {

448451

await MainActor.run {

449452

self.isRequestingNotificationAuthorization = false

450453

self.notificationStatus = SettingsNotificationStatus(settings.authorizationStatus)

451-

guard granted, self.notificationStatus.allowsNotifications else { return }

452-

UIApplication.shared.registerForRemoteNotifications()

454+

guard granted else { return }

455+

self.registerForRemoteNotificationsIfEnrollmentReady()

453456

}

454457

}

455458

}

456459
460+

@MainActor

461+

func registerForRemoteNotificationsIfEnrollmentReady() {

462+

guard PushEnrollmentConsent.disclosureAccepted else { return }

463+

guard self.notificationStatus.allowsNotifications else { return }

464+

UIApplication.shared.registerForRemoteNotifications()

465+

}

466+
457467

@MainActor

458468

func applyNotificationStatus(_ status: UNAuthorizationStatus) {

459469

self.notificationStatus = SettingsNotificationStatus(status)

Original file line numberDiff line numberDiff line change

@@ -4103,6 +4103,9 @@ extension NodeAppModel {

41034103
41044104

private func registerAPNsTokenIfNeeded() async {

41054105

let usesRelayTransport = await self.pushRegistrationManager.usesRelayTransport

4106+

guard await self.canPublishAPNsRegistration(usesRelayTransport: usesRelayTransport) else {

4107+

return

4108+

}

41064109

guard self.gatewayConnected else {

41074110

if usesRelayTransport {

41084111

GatewayDiagnostics.pushRelay.skipped("gateway_offline")

@@ -4163,6 +4166,23 @@ extension NodeAppModel {

41634166

}

41644167

}

41654168
4169+

private func canPublishAPNsRegistration(usesRelayTransport: Bool) async -> Bool {

4170+

guard PushEnrollmentConsent.disclosureAccepted else {

4171+

if usesRelayTransport {

4172+

GatewayDiagnostics.pushRelay.skipped("enrollment_disclosure_not_accepted")

4173+

}

4174+

return false

4175+

}

4176+

let status = await self.notificationAuthorizationStatus()

4177+

guard Self.isNotificationAuthorizationAllowed(status) else {

4178+

if usesRelayTransport {

4179+

GatewayDiagnostics.pushRelay.skipped("notifications_not_authorized")

4180+

}

4181+

return false

4182+

}

4183+

return true

4184+

}

4185+
41664186

private func fetchPushRelayGatewayIdentity() async throws -> PushRelayGatewayIdentity {

41674187

let response = try await self.operatorGateway.request(

41684188

method: "gateway.identity.get",

@@ -5126,6 +5146,10 @@ extension NodeAppModel {

51265146

self.setOperatorConnected(connected)

51275147

}

51285148
5149+

func _test_canPublishAPNsRegistration(usesRelayTransport: Bool = true) async -> Bool {

5150+

await self.canPublishAPNsRegistration(usesRelayTransport: usesRelayTransport)

5151+

}

5152+
51295153

nonisolated static func _test_makeWatchChatItems(from raw: [OpenClawKit.AnyCodable]) -> [OpenClawWatchChatItem] {

51305154

self.makeWatchChatItems(from: raw)

51315155

}

Original file line numberDiff line numberDiff line change

@@ -123,10 +123,30 @@ final class OpenClawAppDelegate: NSObject, UIApplicationDelegate, @preconcurrenc

123123

let notificationCenter = UNUserNotificationCenter.current()

124124

notificationCenter.delegate = self

125125

ExecApprovalNotificationBridge.registerCategory(center: notificationCenter)

126-

application.registerForRemoteNotifications()

126+

Task { @MainActor in

127+

await self.registerForRemoteNotificationsIfEnrollmentReady(application)

128+

}

127129

return true

128130

}

129131
132+

private func registerForRemoteNotificationsIfEnrollmentReady(_ application: UIApplication) async {

133+

guard PushEnrollmentConsent.disclosureAccepted else { return }

134+

guard await Self.isNotificationAuthorizationAllowed() else { return }

135+

application.registerForRemoteNotifications()

136+

}

137+
138+

private static func isNotificationAuthorizationAllowed() async -> Bool {

139+

let settings = await UNUserNotificationCenter.current().notificationSettings()

140+

switch settings.authorizationStatus {

141+

case .authorized, .provisional, .ephemeral:

142+

return true

143+

case .denied, .notDetermined:

144+

return false

145+

@unknown default:

146+

return false

147+

}

148+

}

149+
130150

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {

131151

if let appModel = self.resolvedAppModel() {

132152

Task { @MainActor in

Original file line numberDiff line numberDiff line change

@@ -0,0 +1,19 @@

1+

import Foundation

2+
3+

enum PushEnrollmentConsent {

4+

static let disclosureAcceptedKey = "push.enrollment.disclosureAccepted"

5+
6+

static var disclosureAccepted: Bool {

7+

UserDefaults.standard.bool(forKey: disclosureAcceptedKey)

8+

}

9+
10+

static func markDisclosureAccepted() {

11+

UserDefaults.standard.set(true, forKey: self.disclosureAcceptedKey)

12+

}

13+
14+

#if DEBUG

15+

static func reset() {

16+

UserDefaults.standard.removeObject(forKey: self.disclosureAcceptedKey)

17+

}

18+

#endif

19+

}

Original file line numberDiff line numberDiff line change

@@ -76,6 +76,7 @@ Sources/Permissions/PermissionRequestBridge.swift

7676

Sources/Push/ExecApprovalNotificationBridge.swift

7777

Sources/Push/BackgroundAliveBeacon.swift

7878

Sources/Push/PushBuildConfig.swift

79+

Sources/Push/PushEnrollmentConsent.swift

7980

Sources/Push/PushRegistrationManager.swift

8081

Sources/Push/PushRelayClient.swift

8182

Sources/Push/PushRelayKeychainStore.swift

Original file line numberDiff line numberDiff line change

@@ -1377,6 +1377,24 @@ private final class MockBootstrapNotificationCenter: NotificationCentering, @unc

13771377

#expect(center.addCalls == 1)

13781378

}

13791379
1380+

@Test @MainActor func apnsRegistrationRequiresDisclosureAndNotificationAuthorization() async {

1381+

let center = MockBootstrapNotificationCenter()

1382+

center.status = .authorized

1383+

let appModel = NodeAppModel(notificationCenter: center)

1384+

PushEnrollmentConsent.reset()

1385+

defer { PushEnrollmentConsent.reset() }

1386+
1387+

#expect(await appModel._test_canPublishAPNsRegistration() == false)

1388+

#expect(await appModel._test_canPublishAPNsRegistration(usesRelayTransport: false) == false)

1389+
1390+

PushEnrollmentConsent.markDisclosureAccepted()

1391+

center.status = .notDetermined

1392+

#expect(await appModel._test_canPublishAPNsRegistration() == false)

1393+
1394+

center.status = .authorized

1395+

#expect(await appModel._test_canPublishAPNsRegistration())

1396+

}

1397+
13801398

@Test @MainActor func chatPushWithoutSpeechReturnsUnavailableWhenNotificationsOff() async throws {

13811399

let center = MockBootstrapNotificationCenter()

13821400

center.status = .notDetermined

Original file line numberDiff line numberDiff line change

@@ -550,6 +550,20 @@ struct RootTabsSourceGuardTests {

550550

#expect(docsSource.contains(".accessibilityHint(\"Opens Settings / Gateway\")"))

551551

}

552552
553+

@Test func `push enrollment stays behind notification disclosure flow`() throws {

554+

let appSource = try String(contentsOf: Self.openClawAppSourceURL(), encoding: .utf8)

555+

let actionsSource = try String(contentsOf: Self.settingsProTabActionsSourceURL(), encoding: .utf8)

556+

let modelSource = try String(contentsOf: Self.nodeAppModelSourceURL(), encoding: .utf8)

557+
558+

#expect(appSource.contains("PushEnrollmentConsent.disclosureAccepted"))

559+

#expect(appSource.contains("await Self.isNotificationAuthorizationAllowed()"))

560+

#expect(actionsSource.contains("PushEnrollmentConsent.markDisclosureAccepted()"))

561+

#expect(actionsSource.contains("self.registerForRemoteNotificationsIfEnrollmentReady()"))

562+

#expect(modelSource.contains("PushEnrollmentConsent.disclosureAccepted"))

563+

#expect(modelSource.contains("notifications_not_authorized"))

564+

#expect(modelSource.contains("enrollment_disclosure_not_accepted"))

565+

}

566+
553567

@Test func `gateway settings keeps pairing trust diagnostics and tailscale actions`() throws {

554568

let settingsSource = try String(contentsOf: Self.settingsProTabSourceURL(), encoding: .utf8)

555569

let sectionsSource = try String(contentsOf: Self.settingsProTabSectionsSourceURL(), encoding: .utf8)

@@ -786,6 +800,13 @@ struct RootTabsSourceGuardTests {

786800

.appendingPathComponent("Sources/Design/SettingsProTab.swift")

787801

}

788802
803+

private static func openClawAppSourceURL() -> URL {

804+

URL(fileURLWithPath: #filePath)

805+

.deletingLastPathComponent()

806+

.deletingLastPathComponent()

807+

.appendingPathComponent("Sources/OpenClawApp.swift")

808+

}

809+
789810

private static func notificationPermissionGuidanceDialogSourceURL() -> URL {

790811

URL(fileURLWithPath: #filePath)

791812

.deletingLastPathComponent()