diff --git a/ios/AppIcon.icon/icon.json b/ios/AppIcon.icon/icon.json index a2cc60d..f940700 100644 --- a/ios/AppIcon.icon/icon.json +++ b/ios/AppIcon.icon/icon.json @@ -6,6 +6,14 @@ { "layers" : [ { + "fill-specializations" : [ + { + "appearance" : "dark", + "value" : { + "solid" : "extended-gray:1.00000,1.00000" + } + } + ], "glass" : false, "image-name" : "barcode-gelesen.svg", "name" : "barcode-gelesen", diff --git a/ios/Sources/CheckInFormView.swift b/ios/Sources/CheckInFormView.swift index 7ed8073..7920280 100644 --- a/ios/Sources/CheckInFormView.swift +++ b/ios/Sources/CheckInFormView.swift @@ -67,8 +67,14 @@ struct CheckInFormView: View { ForEach($lines) { $line in VStack(alignment: .leading, spacing: 6) { HStack { + // Ohne Beschriftung sah die vorbelegte 1 wie eine + // Nummerierung der Charge aus. + Text("Menge") + Spacer(minLength: 12) TextField("Menge", text: $line.quantity) .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: 110) if lines.count > 1 { Button(role: .destructive) { lines.removeAll { $0.id == line.id } diff --git a/ios/Sources/CheckOutView.swift b/ios/Sources/CheckOutView.swift index 1566751..f2b886e 100644 --- a/ios/Sources/CheckOutView.swift +++ b/ios/Sources/CheckOutView.swift @@ -155,7 +155,14 @@ struct CheckOutFormView: View { } Section("Menge") { - TextField("Menge", text: $quantity).keyboardType(.decimalPad) + HStack { + Text("Menge") + Spacer(minLength: 12) + TextField("Menge", text: $quantity) + .keyboardType(.decimalPad) + .multilineTextAlignment(.trailing) + .frame(maxWidth: 110) + } Picker("Einheit", selection: $unit) { ForEach(unitOptions, id: \.self) { Text($0).tag($0) } } diff --git a/ios/Sources/LoginView.swift b/ios/Sources/LoginView.swift index 849aa49..f363a95 100644 --- a/ios/Sources/LoginView.swift +++ b/ios/Sources/LoginView.swift @@ -8,6 +8,7 @@ struct LoginView: View { @State private var password = "" @State private var error: String? @State private var busy = false + @State private var stayLoggedIn = Session.shared.stayLoggedIn var body: some View { NavigationStack { @@ -23,6 +24,13 @@ struct LoginView: View { .textInputAutocapitalization(.never) .autocorrectionDisabled() SecureField("Passwort", text: $password) + Toggle("Angemeldet bleiben", isOn: $stayLoggedIn) + } + if !stayLoggedIn { + Section { + Text("Die Anmeldung gilt nur, solange die App geoeffnet ist.") + .font(.caption).foregroundStyle(.secondary) + } } if let error { Section { Text(error).foregroundStyle(.red).font(.callout) } @@ -43,7 +51,8 @@ struct LoginView: View { session.setServer(server) do { let token = try await APIClient.shared.login(username: username, password: password) - session.store(token: token.accessToken, username: token.username, isAdmin: token.role == "admin") + session.store(token: token.accessToken, username: token.username, + isAdmin: token.role == "admin", persist: stayLoggedIn) } catch { self.error = error.localizedDescription } diff --git a/ios/Sources/Session.swift b/ios/Sources/Session.swift index 3f03cbe..bc7bb0a 100644 --- a/ios/Sources/Session.swift +++ b/ios/Sources/Session.swift @@ -7,12 +7,16 @@ final class Session: ObservableObject { static let shared = Session() private let urlKey = "server_url" + private let usernameKey = "username" + private let stayKey = "stay_logged_in" private let keychainAccount = "project-good-token" @Published private(set) var baseURL: URL? @Published private(set) var token: String? @Published var username: String = "" @Published var isAdmin: Bool = false + /// Merkt die letzte Wahl, damit der Haken im Login richtig steht. + @Published private(set) var stayLoggedIn: Bool = true var isLoggedIn: Bool { token != nil && baseURL != nil } @@ -20,7 +24,10 @@ final class Session: ObservableObject { if let stored = UserDefaults.standard.string(forKey: urlKey) { baseURL = Session.normalize(stored) } + // Ohne bisherige Wahl bleibt man angemeldet - das ist der Alltagsfall. + stayLoggedIn = UserDefaults.standard.object(forKey: stayKey) as? Bool ?? true token = Keychain.read(account: keychainAccount) + username = UserDefaults.standard.string(forKey: usernameKey) ?? "" } /// Sorgt für eine URL mit Schema und abschließendem "/", damit relative @@ -39,11 +46,22 @@ final class Session: ObservableObject { UserDefaults.standard.set(url.absoluteString, forKey: urlKey) } - func store(token newToken: String, username name: String, isAdmin admin: Bool) { + /// `persist == false` heisst: die Anmeldung gilt nur, solange die App laeuft. + /// Auf dem Geraet bleibt dann nichts zurueck. + func store(token newToken: String, username name: String, isAdmin admin: Bool, persist: Bool) { token = newToken username = name isAdmin = admin - Keychain.write(newToken, account: keychainAccount) + stayLoggedIn = persist + UserDefaults.standard.set(persist, forKey: stayKey) + + if persist { + Keychain.write(newToken, account: keychainAccount) + UserDefaults.standard.set(name, forKey: usernameKey) + } else { + Keychain.delete(account: keychainAccount) + UserDefaults.standard.removeObject(forKey: usernameKey) + } } func logout() { @@ -51,6 +69,7 @@ final class Session: ObservableObject { username = "" isAdmin = false Keychain.delete(account: keychainAccount) + UserDefaults.standard.removeObject(forKey: usernameKey) } }