Replace the Opal-style custom presentation with the bare iOS design language, keeping the backend, flows, and accessibility identifiers: - Home: NavigationStack + List; rules now presented as list rows with kind icon, block summary, and trailing live status; '+' in the toolbar - New Rule: plain list of rule types and presets; editor still pushed via navigationDestination - Editor: native Form (DatePicker rows, day circles with summary in the section header, toggle rows with footers, stepper rows); create commits with a prominent 'Add Rule' button replacing Hold to Commit; edit uses toolbar Done plus red Disable/Delete rows - Detail: sheet with inline title + status caption, LabeledContent rows; Edit Rule pushes the editor natively - Default color scheme: drop forced dark mode and custom tint; system appearance and accent throughout - Delete Theme, HoldToCommitButton, RuleCardView and custom chrome - Use concrete Color.primary/secondary in button row labels (the hierarchical styles resolve against the button tint) - Tests: 93 passing; only delta is holdToCommitButton press → commitRuleButton tap in 5 call sites - Spec: add §6 mapping the original presentation to the native one Co-Authored-By: Claude <noreply@anthropic.com>
129 lines
4.3 KiB
Swift
129 lines
4.3 KiB
Swift
//
|
|
// OnboardingView.swift
|
|
// Severed
|
|
//
|
|
|
|
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("Severed")
|
|
.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", "Severed 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. Severed 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)
|
|
}
|
|
}
|