Per-platform login screens (iOS/macOS share a view model) that validate against auth.info and store the API token via OutlineKit's Keychain wrapper — never UserDefaults, per CLAUDE.md.
91 lines
3.3 KiB
Swift
91 lines
3.3 KiB
Swift
#if os(macOS)
|
|
import SwiftUI
|
|
|
|
struct AuthView_macOS: View {
|
|
@State private var viewModel = AuthViewModel()
|
|
@FocusState private var focusedField: Field?
|
|
@State private var isShowingServerHint = false
|
|
var onSigningIn: (AuthViewModel.AuthResult) -> Void
|
|
|
|
private enum Field {
|
|
case serverURL, apiToken
|
|
}
|
|
|
|
var body: some View {
|
|
VStack(spacing: 24) {
|
|
AuthHeaderView()
|
|
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
HStack(spacing: 4) {
|
|
Text("Server")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
Button {
|
|
isShowingServerHint.toggle()
|
|
} label: {
|
|
Image(systemName: "info.circle")
|
|
.font(.caption)
|
|
.foregroundStyle(.tertiary)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.help(AuthViewModel.httpsHint)
|
|
.popover(isPresented: $isShowingServerHint, arrowEdge: .bottom) {
|
|
Text(AuthViewModel.httpsHint)
|
|
.font(.callout)
|
|
.padding()
|
|
.frame(width: 240)
|
|
}
|
|
}
|
|
|
|
TextField("outline.example.com", text: $viewModel.serverURLString, prompt: Text("outline.example.com"))
|
|
.textFieldStyle(.roundedBorder)
|
|
.focused($focusedField, equals: .serverURL)
|
|
.onSubmit { focusedField = .apiToken }
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text("API Token")
|
|
.font(.caption.weight(.semibold))
|
|
.foregroundStyle(.secondary)
|
|
|
|
SecureField("Personal API token", text: $viewModel.apiToken, prompt: Text("Personal API token"))
|
|
.textFieldStyle(.roundedBorder)
|
|
.focused($focusedField, equals: .apiToken)
|
|
.onSubmit { submit() }
|
|
}
|
|
|
|
if let errorMessage = viewModel.errorMessage {
|
|
Label(errorMessage, systemImage: "exclamationmark.triangle.fill")
|
|
.font(.callout)
|
|
.foregroundStyle(.red)
|
|
.transition(.opacity)
|
|
}
|
|
|
|
HStack {
|
|
Spacer()
|
|
if viewModel.isValidating {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
}
|
|
Button("Sign In", action: submit)
|
|
.keyboardShortcut(.defaultAction)
|
|
.disabled(!viewModel.canSubmit)
|
|
}
|
|
}
|
|
}
|
|
.padding(32)
|
|
.frame(width: 380)
|
|
.animation(.easeInOut(duration: 0.2), value: viewModel.errorMessage)
|
|
}
|
|
|
|
private func submit() {
|
|
focusedField = nil
|
|
Task {
|
|
guard let result = await viewModel.signIn() else { return }
|
|
onSigningIn(result)
|
|
}
|
|
}
|
|
}
|
|
#endif
|