macOS/onboarding: add one-time security acknowledgement
This commit is contained in:
parent
fbbca10c83
commit
5f2f9a05a3
@ -6,6 +6,7 @@ let launchdLabel = "ai.openclaw.mac"
|
||||
let gatewayLaunchdLabel = "ai.openclaw.gateway"
|
||||
let onboardingVersionKey = "openclaw.onboardingVersion"
|
||||
let onboardingSeenKey = "openclaw.onboardingSeen"
|
||||
let onboardingSecurityAcknowledgedKey = "openclaw.onboardingSecurityAcknowledged"
|
||||
let currentOnboardingVersion = 7
|
||||
let pauseDefaultsKey = "openclaw.pauseEnabled"
|
||||
let iconAnimationsEnabledKey = "openclaw.iconAnimationsEnabled"
|
||||
|
||||
@ -25,6 +25,7 @@ final class OnboardingController {
|
||||
if ProcessInfo.processInfo.isNixMode {
|
||||
// Nix mode is fully declarative; onboarding would suggest interactive setup that doesn't apply.
|
||||
UserDefaults.standard.set(true, forKey: "openclaw.onboardingSeen")
|
||||
UserDefaults.standard.set(true, forKey: onboardingSecurityAcknowledgedKey)
|
||||
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
|
||||
AppStateStore.shared.onboardingSeen = true
|
||||
return
|
||||
@ -90,6 +91,7 @@ struct OnboardingView: View {
|
||||
@State var onboardingWizard = OnboardingWizardModel()
|
||||
@State var didLoadOnboardingSkills = false
|
||||
@State var localGatewayProbe: LocalGatewayProbe?
|
||||
@State var securityNoticeAcknowledged: Bool
|
||||
@Bindable var state: AppState
|
||||
var permissionMonitor: PermissionMonitor
|
||||
|
||||
@ -148,13 +150,27 @@ struct OnboardingView: View {
|
||||
self.activePageIndex == self.wizardPageIndex && !self.onboardingWizard.isComplete
|
||||
}
|
||||
|
||||
var isSecurityNoticeBlocking: Bool {
|
||||
self.activePageIndex == 0 && !self.securityNoticeAcknowledged
|
||||
}
|
||||
|
||||
var canAdvance: Bool {
|
||||
if self.isSecurityNoticeBlocking {
|
||||
return false
|
||||
}
|
||||
if self.activePageIndex == self.cliPageIndex {
|
||||
return self.cliInstalled && !self.installingCLI
|
||||
}
|
||||
return !self.isWizardBlocking
|
||||
}
|
||||
|
||||
static func resolveSecurityNoticeAcknowledged(
|
||||
onboardingSeen: Bool,
|
||||
storedAcknowledgement: Bool) -> Bool
|
||||
{
|
||||
storedAcknowledgement || onboardingSeen
|
||||
}
|
||||
|
||||
var devLinkCommand: String {
|
||||
let version = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
|
||||
return "npm install -g openclaw@\(version)"
|
||||
@ -177,6 +193,10 @@ struct OnboardingView: View {
|
||||
self.state = state
|
||||
self.permissionMonitor = permissionMonitor
|
||||
self._gatewayDiscovery = State(initialValue: discoveryModel)
|
||||
self._securityNoticeAcknowledged = State(
|
||||
initialValue: Self.resolveSecurityNoticeAcknowledged(
|
||||
onboardingSeen: state.onboardingSeen,
|
||||
storedAcknowledgement: UserDefaults.standard.bool(forKey: onboardingSecurityAcknowledgedKey)))
|
||||
self._onboardingChatModel = State(
|
||||
initialValue: OpenClawChatViewModel(
|
||||
sessionKey: "onboarding",
|
||||
|
||||
@ -45,7 +45,7 @@ extension OnboardingView {
|
||||
}
|
||||
|
||||
func handleNext() {
|
||||
if self.isWizardBlocking { return }
|
||||
if !self.canAdvance { return }
|
||||
if self.currentPage < self.pageCount - 1 {
|
||||
withAnimation { self.currentPage += 1 }
|
||||
} else {
|
||||
@ -53,7 +53,13 @@ extension OnboardingView {
|
||||
}
|
||||
}
|
||||
|
||||
func setSecurityNoticeAcknowledged(_ acknowledged: Bool) {
|
||||
self.securityNoticeAcknowledged = acknowledged
|
||||
UserDefaults.standard.set(acknowledged, forKey: onboardingSecurityAcknowledgedKey)
|
||||
}
|
||||
|
||||
func finish() {
|
||||
UserDefaults.standard.set(true, forKey: onboardingSecurityAcknowledgedKey)
|
||||
UserDefaults.standard.set(true, forKey: "openclaw.onboardingSeen")
|
||||
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
|
||||
OnboardingController.shared.close()
|
||||
|
||||
@ -72,6 +72,29 @@ extension OnboardingView {
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.fill(Color.orange.opacity(0.06)))
|
||||
.frame(maxWidth: 520)
|
||||
|
||||
self.onboardingCard(spacing: 8, padding: 14) {
|
||||
if self.securityNoticeAcknowledged {
|
||||
Label("Security notice acknowledged on this Mac.", systemImage: "checkmark.shield.fill")
|
||||
.font(.callout.weight(.medium))
|
||||
.foregroundStyle(Color(nsColor: .systemGreen))
|
||||
} else {
|
||||
Toggle(
|
||||
isOn: Binding(
|
||||
get: { self.securityNoticeAcknowledged },
|
||||
set: { self.setSecurityNoticeAcknowledged($0) }))
|
||||
{
|
||||
Text("I understand the risks and want to continue.")
|
||||
.font(.callout.weight(.medium))
|
||||
}
|
||||
.toggleStyle(.checkbox)
|
||||
|
||||
Text("You only need to acknowledge this once on this Mac.")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: 520)
|
||||
}
|
||||
.padding(.top, 16)
|
||||
}
|
||||
|
||||
@ -59,6 +59,7 @@ extension OnboardingView {
|
||||
_ = view.connectionPage()
|
||||
|
||||
view.currentPage = 0
|
||||
view.setSecurityNoticeAcknowledged(true)
|
||||
view.handleNext()
|
||||
view.handleBack()
|
||||
|
||||
|
||||
@ -27,6 +27,50 @@ struct OnboardingViewSmokeTests {
|
||||
#expect(!order.contains(8))
|
||||
}
|
||||
|
||||
@Test func `fresh installs require security acknowledgement before advancing`() {
|
||||
let defaults = UserDefaults.standard
|
||||
let previous = defaults.object(forKey: onboardingSecurityAcknowledgedKey)
|
||||
defaults.removeObject(forKey: onboardingSecurityAcknowledgedKey)
|
||||
defer {
|
||||
if let previous {
|
||||
defaults.set(previous, forKey: onboardingSecurityAcknowledgedKey)
|
||||
} else {
|
||||
defaults.removeObject(forKey: onboardingSecurityAcknowledgedKey)
|
||||
}
|
||||
}
|
||||
|
||||
let freshState = AppState(preview: true)
|
||||
freshState.onboardingSeen = false
|
||||
let freshView = OnboardingView(
|
||||
state: freshState,
|
||||
permissionMonitor: PermissionMonitor.shared,
|
||||
discoveryModel: GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName))
|
||||
|
||||
#expect(freshView.isSecurityNoticeBlocking)
|
||||
#expect(!freshView.canAdvance)
|
||||
|
||||
defaults.set(true, forKey: onboardingSecurityAcknowledgedKey)
|
||||
|
||||
let acknowledgedState = AppState(preview: true)
|
||||
acknowledgedState.onboardingSeen = false
|
||||
let acknowledgedView = OnboardingView(
|
||||
state: acknowledgedState,
|
||||
permissionMonitor: PermissionMonitor.shared,
|
||||
discoveryModel: GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName))
|
||||
|
||||
#expect(!acknowledgedView.isSecurityNoticeBlocking)
|
||||
#expect(acknowledgedView.canAdvance)
|
||||
}
|
||||
|
||||
@Test func `existing onboarded users keep their acknowledgement`() {
|
||||
#expect(OnboardingView.resolveSecurityNoticeAcknowledged(
|
||||
onboardingSeen: true,
|
||||
storedAcknowledgement: false))
|
||||
#expect(!OnboardingView.resolveSecurityNoticeAcknowledged(
|
||||
onboardingSeen: false,
|
||||
storedAcknowledgement: false))
|
||||
}
|
||||
|
||||
@Test func `select remote gateway clears stale ssh target when endpoint unresolved`() async {
|
||||
let override = FileManager().temporaryDirectory
|
||||
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user