1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
//
// LoginView.swift
// AT-IRC
//
import SwiftUI
struct LoginView: View {
@Bindable var store: ChatStore
@State private var handle = ""
@State private var authURL: IdentifiableURL?
@State private var errorText: String?
@State private var isBusy = false
var body: some View {
VStack(spacing: 24) {
Spacer()
Image(systemName: "bubble.left.and.bubble.right.fill")
.font(.system(size: 56))
.foregroundStyle(.tint)
Text("AT-IRC")
.font(.largeTitle.bold())
Text("Sign in with your atproto account to join the chat.")
.font(.subheadline)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
TextField("handle (e.g. alice.bsky.social)", text: $handle)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.URL)
.textFieldStyle(.roundedBorder)
.disabled(isBusy)
Button(action: signIn) {
HStack(spacing: 8) {
if isBusy {
ProgressView()
.controlSize(.small)
}
Text("Sign in with Bluesky")
}
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
.disabled(trimmedHandle.isEmpty || isBusy)
if let errorText {
Text(errorText)
.font(.footnote)
.foregroundStyle(.red)
.multilineTextAlignment(.center)
}
Spacer()
}
.padding(24)
.sheet(item: $authURL) { item in
SafariView(url: item.url)
.ignoresSafeArea()
}
.task { autoSignInIfRequested() }
}
/// Simulator sign-in automation. See SignInTestHook and scripts/sim-signin.sh.
private func autoSignInIfRequested() {
#if DEBUG
guard let auto = SignInTestHook.autoHandle, handle.isEmpty, !isBusy else { return }
handle = auto
signIn()
#endif
}
private var trimmedHandle: String {
handle.trimmingCharacters(in: .whitespacesAndNewlines)
}
private func signIn() {
isBusy = true
errorText = nil
Task {
do {
try await store.signIn(handle: trimmedHandle) { url in
#if DEBUG
if SignInTestHook.isEnabled {
// The headless browser is the only one to open the
// authorization request; the PAR request_uri is
// single-use ("initiated from another device").
SignInTestHook.writeAuthorizationURL(url)
return
}
#endif
authURL = IdentifiableURL(url: url)
}
#if DEBUG
if SignInTestHook.isEnabled { SignInTestHook.finish(.success(store)) }
#endif
} catch {
errorText = error.localizedDescription
#if DEBUG
if SignInTestHook.isEnabled { SignInTestHook.finish(.failure(error)) }
#endif
}
authURL = nil
isBusy = false
}
}
}