import SwiftUI /// Paste a token, name the instance, done. There is no browser flow and no /// password anywhere in the system: tokens are minted over SSH on a /// machine that has it. struct SignInView: View { @Environment(SessionStore.self) private var session @State private var instanceURL = "gitbay.org" @State private var token = "" @State private var errorMessage: String? @State private var signingIn = false @FocusState private var tokenFieldFocused: Bool var body: some View { Form { if let notice = session.signedOutMessage { Section { Label(notice, systemImage: "key.slash") .foregroundStyle(.secondary) } } Section("Instance") { TextField("gitbay.org", text: $instanceURL) .textContentType(.URL) .keyboardType(.URL) .autocorrectionDisabled() .textInputAutocapitalization(.never) } Section { SecureField("Paste a token", text: $token) .autocorrectionDisabled() .textInputAutocapitalization(.never) .focused($tokenFieldFocused) .submitLabel(.go) .onSubmit { signIn() } .privacySensitive() } header: { Text("Token") } footer: { VStack(alignment: .leading, spacing: 8) { Text("Mint one over SSH on a machine that has your key:") Text(verbatim: "gitbay auth token create --name iphone --scope full --ttl 90d") .font(.gbMono(.caption)) .textSelection(.enabled) Text("`--scope read` also works, but a read-only token cannot comment or merge.") } } if let errorMessage { Section { GBNotice(errorMessage) } } Section { Button { signIn() } label: { if signingIn { ProgressView() .frame(maxWidth: .infinity) } else { Text("Sign In") .frame(maxWidth: .infinity) } } .disabled(signingIn || token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } } .navigationTitle("Sign in to gitbay") .onAppear { tokenFieldFocused = true } } private func signIn() { signingIn = true errorMessage = nil Task { defer { signingIn = false } do { try await session.signIn(instanceURL: instanceURL, token: token) token = "" } catch let error as GitbayError { errorMessage = signInMessage(for: error) } catch let error as GitbayInstance.InvalidURL { errorMessage = error.errorDescription } catch { errorMessage = "Something went wrong. Please try again." } } } /// Sign-in is the one place a 401 means "that token", not "your session". private func signInMessage(for error: GitbayError) -> String { if error.requiresReauthentication { return "That token was not accepted. Check it was pasted whole and has not expired." } return error.userFacingMessage } } #Preview { NavigationStack { SignInView() } .environment(SessionStore()) }