Skip to content

Commit bbc4673

Browse files
JohnnyBravermantisai-bot
authored andcommitted
Merge remote-tracking branch 'origin/main' into feat/agent-turn-save-outcome-hook
# Conflicts: # scripts/plugin-sdk-surface-report.mjs # test/scripts/plugin-sdk-surface-report.test.ts
2 parents 071ac04 + 3ff59df commit bbc4673

29 files changed

Lines changed: 955 additions & 222 deletions

apps/ios/Sources/Design/SettingsProTab.swift

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,22 +54,27 @@ struct SettingsProTab: View {
5454
@State var locationStatusText: String?
5555
@State var previousLocationModeRaw: String = OpenClawLocationMode.off.rawValue
5656
@State var notificationStatus: SettingsNotificationStatus = .checking
57+
@State var isRequestingNotificationAuthorization = false
58+
@State var showNotificationRelayDisclosure = false
5759
@State var diagnosticsLastRunText = "Not run"
5860
@State var diagnosticsIssueCount: Int?
5961
@State var showTalkIssueDetails = false
6062
@State private var navigationPath: [SettingsRoute] = []
6163
let initialRoute: SettingsRoute?
6264
let directRoute: SettingsRoute?
6365
let headerLeadingAction: OpenClawSidebarHeaderAction?
66+
let onRouteChange: ((SettingsRoute?) -> Void)?
6467

6568
init(
6669
initialRoute: SettingsRoute? = nil,
6770
directRoute: SettingsRoute? = nil,
68-
headerLeadingAction: OpenClawSidebarHeaderAction? = nil)
71+
headerLeadingAction: OpenClawSidebarHeaderAction? = nil,
72+
onRouteChange: ((SettingsRoute?) -> Void)? = nil)
6973
{
7074
self.initialRoute = initialRoute
7175
self.directRoute = directRoute
7276
self.headerLeadingAction = headerLeadingAction
77+
self.onRouteChange = onRouteChange
7378
}
7479

7580
var body: some View {
@@ -117,6 +122,7 @@ struct SettingsProTab: View {
117122
self.refreshNotificationSettings()
118123
self.applyPendingGatewaySetupLinkIfNeeded()
119124
self.applyInitialRouteIfNeeded()
125+
self.notifyRouteChange()
120126
}
121127
.onChange(of: self.scenePhase) { _, phase in
122128
if phase == .active {
@@ -153,6 +159,9 @@ struct SettingsProTab: View {
153159
.onChange(of: self.appModel.gatewaySetupRequestID) { _, _ in
154160
self.applyPendingGatewaySetupLinkIfNeeded()
155161
}
162+
.onChange(of: self.navigationPath) { _, _ in
163+
self.notifyRouteChange()
164+
}
156165
}
157166

158167
private func settingsModalPresentation(_ content: some View) -> some View {
@@ -217,6 +226,19 @@ struct SettingsProTab: View {
217226
} message: {
218227
Text(self.scannerError ?? "")
219228
}
229+
.alert("Enable OpenClaw Hosted Push Relay?", isPresented: self.$showNotificationRelayDisclosure) {
230+
Button("Continue") {
231+
self.requestNotificationAuthorizationFromSettings()
232+
}
233+
Button("Not Now", role: .cancel) {}
234+
} message: {
235+
Text(self.notificationRelayDisclosureMessage)
236+
}
237+
}
238+
239+
func openNotificationsRouteFromApprovals() {
240+
guard self.directRoute == nil else { return }
241+
self.navigationPath = [.notifications]
220242
}
221243

222244
private func applyInitialRouteIfNeeded() {
@@ -225,4 +247,12 @@ struct SettingsProTab: View {
225247
guard self.navigationPath != [initialRoute] else { return }
226248
self.navigationPath = [initialRoute]
227249
}
250+
251+
private func notifyRouteChange() {
252+
if let directRoute {
253+
self.onRouteChange?(directRoute)
254+
return
255+
}
256+
self.onRouteChange?(self.navigationPath.last)
257+
}
228258
}

apps/ios/Sources/Design/SettingsProTabActions.swift

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -426,15 +426,30 @@ extension SettingsProTab {
426426
self.openNotificationSettings()
427427
return
428428
}
429+
guard self.notificationStatus == .notSet else { return }
429430

431+
if PushBuildConfig.current.usesOpenClawHostedRelay {
432+
self.showNotificationRelayDisclosure = true
433+
return
434+
}
435+
self.requestNotificationAuthorizationFromSettings()
436+
}
437+
438+
func requestNotificationAuthorizationFromSettings() {
439+
guard !self.isRequestingNotificationAuthorization else { return }
440+
self.isRequestingNotificationAuthorization = true
430441
Task {
431442
let granted = await (try? UNUserNotificationCenter.current().requestAuthorization(options: [
432443
.alert,
433444
.badge,
434445
.sound,
435446
])) ?? false
447+
let settings = await UNUserNotificationCenter.current().notificationSettings()
436448
await MainActor.run {
437-
self.notificationStatus = granted ? .allowed : .notAllowed
449+
self.isRequestingNotificationAuthorization = false
450+
self.notificationStatus = SettingsNotificationStatus(settings.authorizationStatus)
451+
guard granted, self.notificationStatus.allowsNotifications else { return }
452+
UIApplication.shared.registerForRemoteNotifications()
438453
}
439454
}
440455
}
@@ -661,6 +676,9 @@ extension SettingsProTab {
661676
if self.appModel.isAppleReviewDemoModeEnabled {
662677
return "Live gateway requests are disabled in demo mode."
663678
}
679+
if self.notificationsNeedAttention {
680+
return "Foreground approvals still appear while OpenClaw is connected."
681+
}
664682
return self.gatewayConnected ? "Gateway requests will appear here." : "Connect to the gateway."
665683
}
666684

@@ -700,7 +718,19 @@ extension SettingsProTab {
700718
}
701719

702720
var approvalsDetail: String {
703-
self.pendingApproval == nil ? "No approvals waiting" : "1 request waiting"
721+
if self.notificationsNeedAttention {
722+
return self.pendingApproval == nil ? "Notifications off" : "1 waiting, notifications off"
723+
}
724+
return self.pendingApproval == nil ? "No approvals waiting" : "1 request waiting"
725+
}
726+
727+
var notificationsNeedAttention: Bool {
728+
switch self.notificationStatus {
729+
case .allowed, .checking:
730+
false
731+
case .notAllowed, .notSet, .unknown:
732+
true
733+
}
704734
}
705735

706736
var approvalItems: [SettingsApprovalItem] {
@@ -771,4 +801,33 @@ extension SettingsProTab {
771801
var notificationActionText: String {
772802
self.notificationStatus.actionTitle
773803
}
804+
805+
var notificationStatusDetail: String {
806+
switch self.notificationStatus {
807+
case .checking:
808+
"Checking iOS notification permission."
809+
case .allowed:
810+
"OpenClaw can show approval prompts and event alerts when the app is not active."
811+
case .notAllowed:
812+
"Notifications have been denied. Enable them in iOS Settings."
813+
case .notSet:
814+
"Enable notifications to receive approval prompts and event alerts outside the app."
815+
case .unknown:
816+
"OpenClaw cannot determine the current notification permission state."
817+
}
818+
}
819+
820+
var notificationRelayDetail: String {
821+
if PushBuildConfig.current.usesOpenClawHostedRelay {
822+
return """
823+
This build uses OpenClaw's hosted push relay at ios-push-relay.openclaw.ai for notification \
824+
delivery data.
825+
"""
826+
}
827+
return "This build is not configured to use OpenClaw's hosted push relay."
828+
}
829+
830+
var notificationRelayDisclosureMessage: String {
831+
"Enabling this sends delivery data through OpenClaw's hosted push relay."
832+
}
774833
}

apps/ios/Sources/Design/SettingsProTabSections.swift

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -308,15 +308,57 @@ extension SettingsProTab {
308308
self.detailStatusCard(
309309
icon: "checkmark.shield.fill",
310310
title: "Approvals",
311-
detail: self.pendingApproval == nil ? "No gateway actions are waiting for review." :
312-
"Review the pending gateway action.",
313-
value: self.pendingApproval == nil ? "clear" : "1 waiting",
314-
color: self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn)
311+
detail: self.notificationsNeedAttention
312+
? "Out-of-app approval alerts need notification permission."
313+
: (self.pendingApproval == nil ? "No gateway actions are waiting for review." :
314+
"Review the pending gateway action."),
315+
value: self.notificationsNeedAttention
316+
? "Alerts Off"
317+
: (self.pendingApproval == nil ? "clear" : "1 waiting"),
318+
color: self.notificationsNeedAttention ? OpenClawBrand.warn :
319+
(self.pendingApproval == nil ? OpenClawBrand.ok : OpenClawBrand.warn))
320+
321+
if self.notificationsNeedAttention {
322+
self.approvalNotificationsWarningCard
323+
}
315324

316325
self.approvalsReviewCard
317326
}
318327
}
319328

329+
var approvalNotificationsWarningCard: some View {
330+
ProCard(radius: SettingsLayout.cardRadius) {
331+
VStack(alignment: .leading, spacing: 12) {
332+
HStack(alignment: .top, spacing: 12) {
333+
ProIconBadge(systemName: "bell.slash.fill", color: OpenClawBrand.warn)
334+
VStack(alignment: .leading, spacing: 4) {
335+
Text("Notifications are off")
336+
.font(.subheadline.weight(.semibold))
337+
Text(
338+
"""
339+
Enable Notifications to receive approval notifications while OpenClaw is not open.
340+
""")
341+
.font(.caption)
342+
.foregroundStyle(.secondary)
343+
.fixedSize(horizontal: false, vertical: true)
344+
}
345+
}
346+
347+
if self.directRoute == nil {
348+
Button {
349+
self.openNotificationsRouteFromApprovals()
350+
} label: {
351+
Label("Open Notifications", systemImage: "bell.badge")
352+
.frame(maxWidth: .infinity)
353+
}
354+
.buttonStyle(.bordered)
355+
.controlSize(.small)
356+
}
357+
}
358+
}
359+
.padding(.horizontal, OpenClawProMetric.pagePadding)
360+
}
361+
320362
var approvalsReviewCard: some View {
321363
ProCard(radius: SettingsLayout.cardRadius) {
322364
VStack(alignment: .leading, spacing: 12) {
@@ -490,7 +532,7 @@ extension SettingsProTab {
490532
self.detailStatusCard(
491533
icon: "bell",
492534
title: "Notifications",
493-
detail: "Approvals and event alerts from OpenClaw.",
535+
detail: self.notificationStatusDetail,
494536
value: self.notificationStatusText,
495537
color: self.notificationStatus.color)
496538

@@ -506,10 +548,25 @@ extension SettingsProTab {
506548
}
507549
.buttonStyle(.borderedProminent)
508550
.controlSize(.small)
551+
.disabled(self.notificationStatus == .checking || self.isRequestingNotificationAuthorization)
509552

510-
Text("OpenClaw uses notifications for approval prompts and mirrored event alerts.")
553+
Text(self.notificationStatusDetail)
511554
.font(.caption)
512555
.foregroundStyle(.secondary)
556+
.fixedSize(horizontal: false, vertical: true)
557+
558+
Divider()
559+
560+
HStack(alignment: .top, spacing: 10) {
561+
Image(systemName: "network")
562+
.font(.caption.weight(.semibold))
563+
.foregroundStyle(OpenClawBrand.accent)
564+
.frame(width: 22, height: 22)
565+
Text(self.notificationRelayDetail)
566+
.font(.caption)
567+
.foregroundStyle(.secondary)
568+
.fixedSize(horizontal: false, vertical: true)
569+
}
513570
}
514571
}
515572
.padding(.horizontal, OpenClawProMetric.pagePadding)

apps/ios/Sources/Design/SettingsProTabSupport.swift

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -89,28 +89,48 @@ enum SettingsNotificationStatus: Equatable {
8989
var text: String {
9090
switch self {
9191
case .checking: "Checking"
92-
case .allowed: "Allowed"
93-
case .notAllowed: "Not Allowed"
94-
case .notSet: "Not Set"
92+
case .allowed: "Enabled"
93+
case .notAllowed: "Denied"
94+
case .notSet: "Not Enabled"
9595
case .unknown: "Unknown"
9696
}
9797
}
9898

9999
var actionTitle: String {
100100
switch self {
101-
case .notSet, .checking:
102-
"Request Access"
103-
case .allowed, .notAllowed, .unknown:
104-
"Open System Settings"
101+
case .notSet:
102+
"Enable Notifications"
103+
case .checking:
104+
"Checking"
105+
case .allowed:
106+
"Manage in iOS Settings"
107+
case .notAllowed, .unknown:
108+
"Open iOS Settings"
105109
}
106110
}
107111

108112
var actionIcon: String {
109-
self == .allowed ? "gear" : "bell.badge"
113+
switch self {
114+
case .allowed:
115+
"gear"
116+
case .notAllowed, .unknown:
117+
"gear.badge"
118+
case .checking:
119+
"hourglass"
120+
case .notSet:
121+
"bell.badge"
122+
}
110123
}
111124

112125
var color: Color {
113-
self == .allowed ? OpenClawBrand.ok : .secondary
126+
switch self {
127+
case .allowed:
128+
OpenClawBrand.ok
129+
case .notAllowed, .unknown:
130+
OpenClawBrand.warn
131+
case .checking, .notSet:
132+
.secondary
133+
}
114134
}
115135

116136
var shouldOpenNotificationSettings: Bool {
@@ -121,6 +141,10 @@ enum SettingsNotificationStatus: Equatable {
121141
false
122142
}
123143
}
144+
145+
var allowsNotifications: Bool {
146+
self == .allowed
147+
}
124148
}
125149

126150
enum SettingsDiagnosticIssue: String, Equatable, CaseIterable {

apps/ios/Sources/Gateway/ExecApprovalPromptDialog.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@ import SwiftUI
22

33
private struct ExecApprovalPromptDialogModifier: ViewModifier {
44
@Environment(NodeAppModel.self) private var appModel: NodeAppModel
5+
let suppressedApprovalID: String?
56

67
func body(content: Content) -> some View {
78
content
89
.overlay {
9-
if let prompt = self.appModel.pendingExecApprovalPrompt {
10+
if let prompt = self.appModel.pendingExecApprovalPrompt,
11+
prompt.id != self.suppressedApprovalID
12+
{
1013
ZStack {
1114
Color.black.opacity(0.38)
1215
.ignoresSafeArea()
@@ -58,7 +61,7 @@ private struct ExecApprovalPromptCard: View {
5861
VStack(alignment: .leading, spacing: 6) {
5962
Text("Exec approval required")
6063
.font(.headline)
61-
Text("OpenClaw opened from a notification. Review this exec request before continuing.")
64+
Text("Review this exec request before continuing. Your decision will be sent back to the gateway.")
6265
.font(.subheadline)
6366
.foregroundStyle(.secondary)
6467
}
@@ -188,7 +191,7 @@ private struct ExecApprovalPromptMetadataRow: View {
188191
}
189192

190193
extension View {
191-
func execApprovalPromptDialog() -> some View {
192-
self.modifier(ExecApprovalPromptDialogModifier())
194+
func execApprovalPromptDialog(suppressedApprovalID: String? = nil) -> some View {
195+
self.modifier(ExecApprovalPromptDialogModifier(suppressedApprovalID: suppressedApprovalID))
193196
}
194197
}

0 commit comments

Comments
 (0)