import FamilyControls import SwiftUI @main struct FamilyQuestsApp: App { @StateObject private var model = AppModel() @Environment(\.scenePhase) private var scenePhase var body: some Scene { WindowGroup { RootView(model: model) .task { await model.synchronize() } .onChange(of: scenePhase) { _, phase in if phase == .active { Task { await model.synchronize() } } if phase == .background { model.closeParentSetup() } } } } } struct RootView: View { @ObservedObject var model: AppModel @State private var server = "" @State private var code = "" @State private var configurationCode = "" @State private var replacementCode = "" var body: some View { NavigationStack { Form { if model.credentials == nil { Section("Parent: pair this iPhone") { Text("Create a child and a pairing code in the parent web dashboard. A guardian must approve Apple Family Controls.") TextField("https://your-server.example", text: $server) .textInputAutocapitalization(.never).autocorrectionDisabled().keyboardType(.URL) SecureField("Single-use pairing code", text: $code) .textInputAutocapitalization(.never).autocorrectionDisabled() Button("Pair and request guardian approval") { let submitted = code; code = "" Task { await model.pair(server: server, code: submitted) } } } } else { Section(model.snapshot?.childName ?? "My time") { if let snapshot = model.snapshot { LabeledContent("Daily allowance", value: "\(snapshot.policy.dailyMinutes) minutes") LabeledContent("Bonus earned today", value: "\(snapshot.bonus(at: Date())) minutes") Text("This is your allowance, not a live remaining-time counter. Rewards expire at the end of the family day and never extend bedtime.") .font(.footnote).foregroundStyle(.secondary) } Text(model.status?.detail ?? "Open or sync the app to check configuration") Button("Sync policy and rewards") { Task { await model.synchronize() } } } Section("Maths challenge") { if let challenge = model.challenge { ForEach(challenge.questions) { question in HStack { Text("\(question.prompt) =") TextField("Answer", text: Binding( get: { model.answers[question.id, default: ""] }, set: { model.answers[question.id] = $0 } )).keyboardType(.numberPad).multilineTextAlignment(.trailing) .accessibilityLabel("Answer to \(question.prompt)") } } Button("Check answers") { Task { await model.submitChallenge() } } } else { Text("Answer every question correctly to earn bonus app usage. An internet connection is required to verify answers.") Button("Start challenge") { Task { await model.startChallenge() } } .disabled(model.status?.state != "monitoring") } if !model.rewardMessage.isEmpty { Text(model.rewardMessage) } } Section("Parent setup") { Text("App selection requires a fresh configuration code from the parent dashboard. Do not restrict this app, Phone, or other essential communication and school apps.") .font(.footnote) Button("Request guardian Screen Time authorization") { Task { await model.authorize() } } SecureField("Configuration code", text: $configurationCode) .textInputAutocapitalization(.never).autocorrectionDisabled() Button("Authorize app selection") { let value = configurationCode; configurationCode = "" Task { await model.unlockConfiguration(code: value) } } DisclosureGroup("Replace revoked pairing") { Text("The parent must revoke the old pairing and issue a new pairing code. The server address remains fixed.") SecureField("New pairing code", text: $replacementCode) .textInputAutocapitalization(.never).autocorrectionDisabled() Button("Pair again with the same server") { let value = replacementCode; replacementCode = "" Task { await model.pair(server: "", code: value) } } } } } if !model.message.isEmpty { Section("Needs attention") { Text(model.message).foregroundStyle(.red) } } if model.busy { ProgressView("Working…") } Section { Text("Development MVP. DeviceActivity behavior must be tested on this iOS version before relying on it.").font(.footnote) } } .disabled(model.busy) .navigationTitle("Family Quests") .sheet(isPresented: $model.pickerPresented, onDismiss: { model.closeParentSetup() }) { NavigationStack { VStack { Text("Choose individual entertainment apps only. Leave Family Quests and essential apps accessible.") .font(.footnote).padding() FamilyActivityPicker(selection: $model.draftSelection) } .navigationTitle("Parent app selection") .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Cancel") { model.closeParentSetup() } } ToolbarItem(placement: .confirmationAction) { Button("Save") { Task { await model.saveSelection() } }.disabled(model.busy) } } .safeAreaInset(edge: .bottom) { if !model.message.isEmpty { Text(model.message).font(.footnote).padding() } } } } } } }