gitbay/Views/SignInView.swift
110 lines · 3762 bytes
1import SwiftUI
2
3/// Paste a token, name the instance, done. There is no browser flow and no
4/// password anywhere in the system: tokens are minted over SSH on a
5/// machine that has it.
6struct SignInView: View {
7
8 @Environment(SessionStore.self) private var session
9
10 @State private var instanceURL = "gitbay.org"
11 @State private var token = ""
12 @State private var errorMessage: String?
13 @State private var signingIn = false
14 @FocusState private var tokenFieldFocused: Bool
15
16 var body: some View {
17 Form {
18 if let notice = session.signedOutMessage {
19 Section {
20 Label(notice, systemImage: "key.slash")
21 .foregroundStyle(.secondary)
22 }
23 }
24
25 Section("Instance") {
26 TextField("gitbay.org", text: $instanceURL)
27 .textContentType(.URL)
28 .keyboardType(.URL)
29 .autocorrectionDisabled()
30 .textInputAutocapitalization(.never)
31 }
32
33 Section {
34 SecureField("Paste a token", text: $token)
35 .autocorrectionDisabled()
36 .textInputAutocapitalization(.never)
37 .focused($tokenFieldFocused)
38 .submitLabel(.go)
39 .onSubmit { signIn() }
40 .privacySensitive()
41 } header: {
42 Text("Token")
43 } footer: {
44 VStack(alignment: .leading, spacing: 8) {
45 Text("Mint one over SSH on a machine that has your key:")
46 Text(verbatim: "gitbay auth token create --name iphone --scope full --ttl 90d")
47 .font(.gbMono(.caption))
48 .textSelection(.enabled)
49 Text("`--scope read` also works, but a read-only token cannot comment or merge.")
50 }
51 }
52
53 if let errorMessage {
54 Section {
55 GBNotice(errorMessage)
56 }
57 }
58
59 Section {
60 Button {
61 signIn()
62 } label: {
63 if signingIn {
64 ProgressView()
65 .frame(maxWidth: .infinity)
66 } else {
67 Text("Sign In")
68 .frame(maxWidth: .infinity)
69 }
70 }
71 .disabled(signingIn || token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
72 }
73 }
74 .navigationTitle("Sign in to gitbay")
75 .onAppear { tokenFieldFocused = true }
76 }
77
78 private func signIn() {
79 signingIn = true
80 errorMessage = nil
81 Task {
82 defer { signingIn = false }
83 do {
84 try await session.signIn(instanceURL: instanceURL, token: token)
85 token = ""
86 } catch let error as GitbayError {
87 errorMessage = signInMessage(for: error)
88 } catch let error as GitbayInstance.InvalidURL {
89 errorMessage = error.errorDescription
90 } catch {
91 errorMessage = "Something went wrong. Please try again."
92 }
93 }
94 }
95
96 /// Sign-in is the one place a 401 means "that token", not "your session".
97 private func signInMessage(for error: GitbayError) -> String {
98 if error.requiresReauthentication {
99 return "That token was not accepted. Check it was pasted whole and has not expired."
100 }
101 return error.userFacingMessage
102 }
103}
104
105#Preview {
106 NavigationStack {
107 SignInView()
108 }
109 .environment(SessionStore())
110}