fix(ios): clear private native state and apply upstream dark tokens

Discard editor drafts on account/server changes, obscure inactive sheets and clear loaded records even when vault erasure fails. Serialize local removal and distinguish dark tint from primary button fill. Swift format/parsing and sixteen portable tests pass; Apple SDK validation remains outstanding.
This commit is contained in:
Yuvomi Mobile Contributors committed 2026-10-21 09:09:39 +02:00
1 parent 97570b7fdf
commit 81973c3c45
2 files changed
+40 -8

No files matched your search

+36 -6
View File
@@ -2,6 +2,12 @@ import SwiftUI
private enum NativeDesign { private enum NativeDesign {
static let accent = Color(red: 108 / 255, green: 58 / 255, blue: 237 / 255) static let accent = Color(red: 108 / 255, green: 58 / 255, blue: 237 / 255)
static func tint(_ dark: Bool) -> Color {
dark ? Color(red: 167 / 255, green: 139 / 255, blue: 250 / 255) : accent
}
static func button(_ dark: Bool) -> Color {
dark ? Color(red: 124 / 255, green: 58 / 255, blue: 237 / 255) : accent
}
static func background(_ dark: Bool) -> Color { static func background(_ dark: Bool) -> Color {
dark dark
? Color(red: 25 / 255, green: 24 / 255, blue: 22 / 255) ? Color(red: 25 / 255, green: 24 / 255, blue: 22 / 255)
@@ -39,8 +45,18 @@ struct NativeRootView: View {
NativeBrandMark().frame(width: 64, height: 64) NativeBrandMark().frame(width: 64, height: 64)
} }
} }
.tint(NativeDesign.accent) .tint(NativeDesign.tint(scheme == .dark))
.task { await store.restore() } .task { await store.restore() }
.onChange(of: store.user?.id) {
draft = nil
deletePath = nil
search = ""
}
.onChange(of: store.server) {
draft = nil
deletePath = nil
search = ""
}
.sheet(item: $draft) { entry in .sheet(item: $draft) { entry in
NativeEditor(draft: entry, busy: store.busy, error: store.error) { body in NativeEditor(draft: entry, busy: store.busy, error: store.error) { body in
let path: String let path: String
@@ -139,7 +155,7 @@ struct NativeRootView: View {
Image(systemName: page.symbol).font(.system(size: 21)) Image(systemName: page.symbol).font(.system(size: 21))
Text(page.title).font(.system(size: 11)) Text(page.title).font(.system(size: 11))
} }
.foregroundStyle(store.page == page ? NativeDesign.accent : .secondary) .foregroundStyle(store.page == page ? NativeDesign.tint(scheme == .dark) : .secondary)
.frame(maxWidth: .infinity).padding(.vertical, 10) .frame(maxWidth: .infinity).padding(.vertical, 10)
} }
.disabled(store.busy).accessibilityIdentifier("nav-\(page.rawValue)") .disabled(store.busy).accessibilityIdentifier("nav-\(page.rawValue)")
@@ -341,6 +357,7 @@ struct NativeRootView: View {
} }
private struct NativeAuthenticationScreen: View { private struct NativeAuthenticationScreen: View {
@Environment(\.colorScheme) private var scheme
@ObservedObject var store: NativeStore @ObservedObject var store: NativeStore
let forget: () -> Void let forget: () -> Void
@State private var address = "" @State private var address = ""
@@ -379,8 +396,10 @@ private struct NativeAuthenticationScreen: View {
} }
invalidAddress = false invalidAddress = false
Task { await store.connect(address) } Task { await store.connect(address) }
}.buttonStyle(.borderedProminent).disabled(store.busy || address.isEmpty) }.buttonStyle(.borderedProminent).tint(NativeDesign.button(scheme == .dark)).disabled(
.accessibilityIdentifier("connect") store.busy || address.isEmpty
)
.accessibilityIdentifier("connect")
} else if store.twoFactor { } else if store.twoFactor {
TextField("Code oder Wiederherstellungscode", text: $code).textContentType(.oneTimeCode) TextField("Code oder Wiederherstellungscode", text: $code).textContentType(.oneTimeCode)
.textFieldStyle(.roundedBorder) .textFieldStyle(.roundedBorder)
@@ -388,7 +407,8 @@ private struct NativeAuthenticationScreen: View {
let value = code let value = code
code = "" code = ""
Task { await store.verify(value) } Task { await store.verify(value) }
}.buttonStyle(.borderedProminent).disabled(store.busy || code.isEmpty) }.buttonStyle(.borderedProminent).tint(NativeDesign.button(scheme == .dark)).disabled(
store.busy || code.isEmpty)
Button("Zurück zur Anmeldung") { store.cancelTwoFactor() }.disabled(store.busy) Button("Zurück zur Anmeldung") { store.cancelTwoFactor() }.disabled(store.busy)
} else { } else {
if store.setup { TextField("Anzeigename", text: $name).textFieldStyle(.roundedBorder) } if store.setup { TextField("Anzeigename", text: $name).textFieldStyle(.roundedBorder) }
@@ -406,7 +426,7 @@ private struct NativeAuthenticationScreen: View {
Task { Task {
await store.login(username: username, password: submitted, displayName: name) await store.login(username: username, password: submitted, displayName: name)
} }
}.buttonStyle(.borderedProminent).disabled( }.buttonStyle(.borderedProminent).tint(NativeDesign.button(scheme == .dark)).disabled(
store.busy || username.isEmpty || password.isEmpty store.busy || username.isEmpty || password.isEmpty
).accessibilityIdentifier("login") ).accessibilityIdentifier("login")
} }
@@ -437,6 +457,8 @@ private struct NativeAuthenticationScreen: View {
private struct NativeEditor: View { private struct NativeEditor: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@Environment(\.scenePhase) private var phase
@Environment(\.colorScheme) private var scheme
@State var draft: NativeDraft @State var draft: NativeDraft
let busy: Bool let busy: Bool
let error: String? let error: String?
@@ -482,6 +504,14 @@ private struct NativeEditor: View {
} }
} }
}.interactiveDismissDisabled(busy) }.interactiveDismissDisabled(busy)
.overlay {
if phase != .active {
ZStack {
NativeDesign.background(scheme == .dark).ignoresSafeArea()
NativeBrandMark().frame(width: 64, height: 64)
}
}
}
} }
} }
+4 -2
View File
@@ -175,26 +175,28 @@ final class NativeStore: ObservableObject {
revoked = true revoked = true
} catch { revoked = false } } catch { revoked = false }
await api?.close() await api?.close()
clearLoadedData()
try vault?.clear() try vault?.clear()
if let address = try? ServerAddress(server), let storage = vault { if let address = try? ServerAddress(server), let storage = vault {
api = try NativeAPI(server: address, saved: nil, persist: storage.save) api = try NativeAPI(server: address, saved: nil, persist: storage.save)
} }
clearLoadedData()
if !revoked { error = "Lokal abgemeldet. Die Abmeldung auf dem Server konnte nicht bestätigt werden." } if !revoked { error = "Lokal abgemeldet. Die Abmeldung auf dem Server konnte nicht bestätigt werden." }
} }
} }
func forget() async { func forget() async {
guard !busy else { return } guard !busy else { return }
busy = true
defer { busy = false }
generation += 1 generation += 1
await api?.close() await api?.close()
clearLoadedData()
do { try vault?.clear() } catch { do { try vault?.clear() } catch {
self.error = NativeAPIError.unavailable.errorDescription self.error = NativeAPIError.unavailable.errorDescription
return return
} }
api = nil api = nil
vault = nil vault = nil
clearLoadedData()
server = "" server = ""
appName = "Yuvomi" appName = "Yuvomi"
setup = false setup = false