125 lines
6.9 KiB
Swift
125 lines
6.9 KiB
Swift
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() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|