Files
OpenAppLock/OpenAppLock/Views/Onboarding/OnboardingView.swift
Brendan Chen 1d5e79ba7c refactor: rename app from Severed to OpenAppLock
Full rename ahead of App Store submission: project, targets, bundle
identifiers (dev.bchen.Severed -> dev.bchen.OpenAppLock), entitlements
file, app entry point, user-facing onboarding strings, test helpers,
and docs. Verified: all 62 unit tests pass.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-12 18:05:06 -04:00

129 lines
4.3 KiB
Swift

//
// OnboardingView.swift
// OpenAppLock
//
import SwiftUI
/// Two-step onboarding using system styling: a welcome screen, then the
/// Screen Time permission request. Onboarding only completes once
/// authorization is approved the app cannot block anything without it.
struct OnboardingView: View {
@Environment(ScreenTimeAuthorization.self) private var authorization
@Environment(\.openURL) private var openURL
let onComplete: () -> Void
private enum Step {
case welcome
case permission
}
@State private var step = Step.welcome
@State private var isRequesting = false
var body: some View {
VStack(spacing: 0) {
Spacer()
switch step {
case .welcome: welcome
case .permission: permission
}
Spacer()
footer
}
.padding()
}
private var welcome: some View {
VStack(spacing: 16) {
Image(systemName: "scissors")
.font(.system(size: 56))
.foregroundStyle(.tint)
Text("OpenAppLock")
.font(.largeTitle.bold())
Text("Block your most distracting apps with rules that keep you honest — on a schedule, with no way out when you choose Hard Mode.")
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
}
private var permission: some View {
VStack(spacing: 24) {
Image(systemName: "hourglass")
.font(.system(size: 48))
.foregroundStyle(.tint)
Text("Allow Screen Time Access")
.font(.title.bold())
.multilineTextAlignment(.center)
VStack(alignment: .leading, spacing: 14) {
bullet("shield.fill", "OpenAppLock uses Apple's Screen Time framework to block the apps you choose.")
bullet("hand.raised.fill", "Your app activity stays on this device and is never collected.")
bullet("gearshape.fill", "You can change this anytime in Settings.")
}
if authorization.status == .denied || authorization.lastRequestFailed {
VStack(spacing: 10) {
Text("Screen Time access was declined. OpenAppLock can't block apps without it.")
.font(.footnote)
.foregroundStyle(.red)
.multilineTextAlignment(.center)
.accessibilityIdentifier("permissionDeniedLabel")
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
openURL(url)
}
}
.accessibilityIdentifier("openSettingsButton")
}
}
}
}
private func bullet(_ systemImage: String, _ text: String) -> some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: systemImage)
.foregroundStyle(.tint)
.frame(width: 24)
Text(text)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
@ViewBuilder
private var footer: some View {
switch step {
case .welcome:
pillButton("Continue", identifier: "onboardingContinueButton") {
step = .permission
}
case .permission:
pillButton(
isRequesting ? "Requesting…" : "Allow Screen Time Access",
identifier: "allowScreenTimeButton"
) {
guard !isRequesting else { return }
isRequesting = true
Task {
await authorization.request()
isRequesting = false
if authorization.status == .approved {
onComplete()
}
}
}
}
}
private func pillButton(
_ title: String, identifier: String, action: @escaping () -> Void
) -> some View {
Button(action: action) {
Text(title)
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.accessibilityIdentifier(identifier)
}
}