gitbay/Views/SignInView.swift
119 lines · 4366 bytes
3 symbols in this file
1import SwiftUI
2
3/// Paste a token, name the instance, done. There is no browser flow and no
4/// password in the app: tokens are minted on the instance's web settings
5/// page or over SSH.
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 if let tokenPage = (try? GitbayInstance(url: instanceURL))?.tokenPageURL {
46 Link(destination: tokenPage) {
47 Text("Create one with full scope under Settings → API tokens")
48 .multilineTextAlignment(.leading)
49 .frame(maxWidth: .infinity, alignment: .leading)
50 }
51 Text("Or over SSH on a machine that has your key:")
52 } else {
53 Text("Create one over SSH on a machine that has your key:")
54 }
55 Text(verbatim: "gitbay auth token create --name iphone --scope full --ttl 90d")
56 .font(.gbMono(.caption))
57 .textSelection(.enabled)
58 Text("Full scope can comment and merge, and on an admin account, administer the instance. A read-only token cannot comment or merge.")
59 }
60 }
61
62 if let errorMessage {
63 Section {
64 GBNotice(errorMessage)
65 }
66 }
67
68 Section {
69 Button {
70 signIn()
71 } label: {
72 if signingIn {
73 ProgressView()
74 .frame(maxWidth: .infinity)
75 } else {
76 Text("Sign In")
77 .frame(maxWidth: .infinity)
78 }
79 }
80 .disabled(signingIn || token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
81 }
82 }
83 .navigationTitle("Sign in to gitbay")
84 .onAppear { tokenFieldFocused = true }
85 }
86
87 private func signIn() {
88 signingIn = true
89 errorMessage = nil
90 Task {
91 defer { signingIn = false }
92 do {
93 try await session.signIn(instanceURL: instanceURL, token: token)
94 token = ""
95 } catch let error as GitbayError {
96 errorMessage = signInMessage(for: error)
97 } catch let error as GitbayInstance.InvalidURL {
98 errorMessage = error.errorDescription
99 } catch {
100 errorMessage = "Something went wrong. Please try again."
101 }
102 }
103 }
104
105 /// Sign-in is the one place a 401 means "that token", not "your session".
106 private func signInMessage(for error: GitbayError) -> String {
107 if error.requiresReauthentication {
108 return "That token was not accepted. Check it was pasted whole and has not expired."
109 }
110 return error.userFacingMessage
111 }
112}
113
114#Preview {
115 NavigationStack {
116 SignInView()
117 }
118 .environment(SessionStore())
119}