import SwiftUI /// Paste a token, name the instance, done. There is no browser flow and no /// password in the app: tokens are minted on the instance's web settings /// page or over SSH. 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) { if let tokenPage = (try? GitbayInstance(url: instanceURL))?.tokenPageURL { Link(destination: tokenPage) { Text("Create one with full scope under Settings → API tokens") .multilineTextAlignment(.leading) .frame(maxWidth: .infinity, alignment: .leading) } Text("Or over SSH on a machine that has your key:") } else { Text("Create 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("Full scope can comment and merge, and on an admin account, administer the instance. 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()) }