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 gatewayLaunchdLabel = "ai.openclaw.gateway"
|
||||||
let onboardingVersionKey = "openclaw.onboardingVersion"
|
let onboardingVersionKey = "openclaw.onboardingVersion"
|
||||||
let onboardingSeenKey = "openclaw.onboardingSeen"
|
let onboardingSeenKey = "openclaw.onboardingSeen"
|
||||||
|
let onboardingSecurityAcknowledgedKey = "openclaw.onboardingSecurityAcknowledged"
|
||||||
let currentOnboardingVersion = 7
|
let currentOnboardingVersion = 7
|
||||||
let pauseDefaultsKey = "openclaw.pauseEnabled"
|
let pauseDefaultsKey = "openclaw.pauseEnabled"
|
||||||
let iconAnimationsEnabledKey = "openclaw.iconAnimationsEnabled"
|
let iconAnimationsEnabledKey = "openclaw.iconAnimationsEnabled"
|
||||||
|
|||||||
@ -25,6 +25,7 @@ final class OnboardingController {
|
|||||||
if ProcessInfo.processInfo.isNixMode {
|
if ProcessInfo.processInfo.isNixMode {
|
||||||
// Nix mode is fully declarative; onboarding would suggest interactive setup that doesn't apply.
|
// 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: "openclaw.onboardingSeen")
|
||||||
|
UserDefaults.standard.set(true, forKey: onboardingSecurityAcknowledgedKey)
|
||||||
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
|
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
|
||||||
AppStateStore.shared.onboardingSeen = true
|
AppStateStore.shared.onboardingSeen = true
|
||||||
return
|
return
|
||||||
@ -90,6 +91,7 @@ struct OnboardingView: View {
|
|||||||
@State var onboardingWizard = OnboardingWizardModel()
|
@State var onboardingWizard = OnboardingWizardModel()
|
||||||
@State var didLoadOnboardingSkills = false
|
@State var didLoadOnboardingSkills = false
|
||||||
@State var localGatewayProbe: LocalGatewayProbe?
|
@State var localGatewayProbe: LocalGatewayProbe?
|
||||||
|
@State var securityNoticeAcknowledged: Bool
|
||||||
@Bindable var state: AppState
|
@Bindable var state: AppState
|
||||||
var permissionMonitor: PermissionMonitor
|
var permissionMonitor: PermissionMonitor
|
||||||
|
|
||||||
@ -148,13 +150,27 @@ struct OnboardingView: View {
|
|||||||
self.activePageIndex == self.wizardPageIndex && !self.onboardingWizard.isComplete
|
self.activePageIndex == self.wizardPageIndex && !self.onboardingWizard.isComplete
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isSecurityNoticeBlocking: Bool {
|
||||||
|
self.activePageIndex == 0 && !self.securityNoticeAcknowledged
|
||||||
|
}
|
||||||
|
|
||||||
var canAdvance: Bool {
|
var canAdvance: Bool {
|
||||||
|
if self.isSecurityNoticeBlocking {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if self.activePageIndex == self.cliPageIndex {
|
if self.activePageIndex == self.cliPageIndex {
|
||||||
return self.cliInstalled && !self.installingCLI
|
return self.cliInstalled && !self.installingCLI
|
||||||
}
|
}
|
||||||
return !self.isWizardBlocking
|
return !self.isWizardBlocking
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func resolveSecurityNoticeAcknowledged(
|
||||||
|
onboardingSeen: Bool,
|
||||||
|
storedAcknowledgement: Bool) -> Bool
|
||||||
|
{
|
||||||
|
storedAcknowledgement || onboardingSeen
|
||||||
|
}
|
||||||
|
|
||||||
var devLinkCommand: String {
|
var devLinkCommand: String {
|
||||||
let version = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
|
let version = GatewayEnvironment.expectedGatewayVersionString() ?? "latest"
|
||||||
return "npm install -g openclaw@\(version)"
|
return "npm install -g openclaw@\(version)"
|
||||||
@ -177,6 +193,10 @@ struct OnboardingView: View {
|
|||||||
self.state = state
|
self.state = state
|
||||||
self.permissionMonitor = permissionMonitor
|
self.permissionMonitor = permissionMonitor
|
||||||
self._gatewayDiscovery = State(initialValue: discoveryModel)
|
self._gatewayDiscovery = State(initialValue: discoveryModel)
|
||||||
|
self._securityNoticeAcknowledged = State(
|
||||||
|
initialValue: Self.resolveSecurityNoticeAcknowledged(
|
||||||
|
onboardingSeen: state.onboardingSeen,
|
||||||
|
storedAcknowledgement: UserDefaults.standard.bool(forKey: onboardingSecurityAcknowledgedKey)))
|
||||||
self._onboardingChatModel = State(
|
self._onboardingChatModel = State(
|
||||||
initialValue: OpenClawChatViewModel(
|
initialValue: OpenClawChatViewModel(
|
||||||
sessionKey: "onboarding",
|
sessionKey: "onboarding",
|
||||||
|
|||||||
@ -45,7 +45,7 @@ extension OnboardingView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handleNext() {
|
func handleNext() {
|
||||||
if self.isWizardBlocking { return }
|
if !self.canAdvance { return }
|
||||||
if self.currentPage < self.pageCount - 1 {
|
if self.currentPage < self.pageCount - 1 {
|
||||||
withAnimation { self.currentPage += 1 }
|
withAnimation { self.currentPage += 1 }
|
||||||
} else {
|
} else {
|
||||||
@ -53,7 +53,13 @@ extension OnboardingView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setSecurityNoticeAcknowledged(_ acknowledged: Bool) {
|
||||||
|
self.securityNoticeAcknowledged = acknowledged
|
||||||
|
UserDefaults.standard.set(acknowledged, forKey: onboardingSecurityAcknowledgedKey)
|
||||||
|
}
|
||||||
|
|
||||||
func finish() {
|
func finish() {
|
||||||
|
UserDefaults.standard.set(true, forKey: onboardingSecurityAcknowledgedKey)
|
||||||
UserDefaults.standard.set(true, forKey: "openclaw.onboardingSeen")
|
UserDefaults.standard.set(true, forKey: "openclaw.onboardingSeen")
|
||||||
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
|
UserDefaults.standard.set(currentOnboardingVersion, forKey: onboardingVersionKey)
|
||||||
OnboardingController.shared.close()
|
OnboardingController.shared.close()
|
||||||
|
|||||||
@ -72,6 +72,29 @@ extension OnboardingView {
|
|||||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||||
.fill(Color.orange.opacity(0.06)))
|
.fill(Color.orange.opacity(0.06)))
|
||||||
.frame(maxWidth: 520)
|
.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)
|
.padding(.top, 16)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -59,6 +59,7 @@ extension OnboardingView {
|
|||||||
_ = view.connectionPage()
|
_ = view.connectionPage()
|
||||||
|
|
||||||
view.currentPage = 0
|
view.currentPage = 0
|
||||||
|
view.setSecurityNoticeAcknowledged(true)
|
||||||
view.handleNext()
|
view.handleNext()
|
||||||
view.handleBack()
|
view.handleBack()
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,50 @@ struct OnboardingViewSmokeTests {
|
|||||||
#expect(!order.contains(8))
|
#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 {
|
@Test func `select remote gateway clears stale ssh target when endpoint unresolved`() async {
|
||||||
let override = FileManager().temporaryDirectory
|
let override = FileManager().temporaryDirectory
|
||||||
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
|
.appendingPathComponent("openclaw-config-\(UUID().uuidString)")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user