135 lines
6.7 KiB
Swift
135 lines
6.7 KiB
Swift
import SwiftUI
|
|
|
|
struct LoginView: View {
|
|
@ObservedObject var auth: AuthStore
|
|
@ObservedObject var settings: SettingsStore
|
|
@State private var username = ""
|
|
@State private var password = ""
|
|
@State private var server = ""
|
|
@State private var serverError: String?
|
|
@Environment(\.dynamicTypeSize) private var typeSize
|
|
var body: some View {
|
|
NavigationStack {
|
|
Form {
|
|
Section {
|
|
VStack(alignment: .leading, spacing: Nearlight.s12) {
|
|
Text("Sign in to Hope")
|
|
.font(NearlightFont.welcome(typeSize))
|
|
.foregroundStyle(Nearlight.ink)
|
|
.accessibilityAddTraits(.isHeader)
|
|
Text("Use the account provided by your server administrator. Hermes is managed by your server.")
|
|
.font(.body)
|
|
.foregroundStyle(Nearlight.quiet)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
.padding(.vertical, Nearlight.s16)
|
|
.listRowBackground(Nearlight.ground)
|
|
.listRowSeparator(.hidden)
|
|
}
|
|
Section("Account") {
|
|
TextField("Username", text: $username).textContentType(.username)
|
|
.textInputAutocapitalization(.never).autocorrectionDisabled()
|
|
SecureField("Password", text: $password).textContentType(.password)
|
|
Button("Sign in") {
|
|
let suppliedPassword = password
|
|
password = ""
|
|
Task { await auth.login(username: username, password: suppliedPassword) }
|
|
}
|
|
.font(NearlightFont.control(typeSize))
|
|
.buttonStyle(.borderedProminent)
|
|
.tint(Nearlight.rosewood)
|
|
.foregroundStyle(Nearlight.onRosewood)
|
|
.frame(minHeight: 44)
|
|
.padding(.vertical, Nearlight.s4)
|
|
.disabled(auth.isBusy || username.isEmpty || password.isEmpty)
|
|
if auth.isBusy { ProgressView("Signing in…").tint(Nearlight.rosewood) }
|
|
if let error = auth.error {
|
|
Text(error).foregroundStyle(Nearlight.alert)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
.listRowBackground(Nearlight.surface)
|
|
Section("Server") {
|
|
TextField("HTTPS server address", text: $server).keyboardType(.URL)
|
|
.textInputAutocapitalization(.never).autocorrectionDisabled()
|
|
Button("Save server address") {
|
|
switch ServerConfig.parse(server) {
|
|
case .success(let config): settings.serverURL = config.baseURL.absoluteString
|
|
case .failure(let error): serverError = error.localizedDescription
|
|
}
|
|
}.disabled(auth.isBusy)
|
|
if let serverError {
|
|
Text(serverError).foregroundStyle(Nearlight.alert)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
Text("Connections use HTTPS and secure WebSockets. A private server certificate must be trusted by this iPhone for conversations.")
|
|
.font(.footnote).foregroundStyle(Nearlight.quiet)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
if auth.config.baseURL.host == TLSPinning.host, auth.config.baseURL.port == 9178 {
|
|
Link("Install Hope server certificate in Safari", destination: auth.config.baseURL.appendingPathComponent("hope-server.mobileconfig"))
|
|
Text("Install Hope LAN Server Trust in Settings → General → VPN & Device Management. Then enable 192.168.86.2 in Settings → General → About → Certificate Trust Settings. Only trust this certificate for your own server.")
|
|
.font(.footnote).foregroundStyle(Nearlight.quiet)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
}
|
|
.listRowBackground(Nearlight.surface)
|
|
}
|
|
.foregroundStyle(Nearlight.ink)
|
|
.tint(Nearlight.rosewood)
|
|
.scrollContentBackground(.hidden)
|
|
.background(Nearlight.ground)
|
|
.navigationTitle("Hope")
|
|
.toolbarBackground(Nearlight.ground, for: .navigationBar)
|
|
.onAppear { server = settings.serverURL }
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AuthenticationRoot: View {
|
|
@ObservedObject var settings: SettingsStore
|
|
@State private var auth: AuthStore
|
|
@State private var replacement: Task<Void, Never>?
|
|
init(settings: SettingsStore) {
|
|
self.settings = settings
|
|
_auth = State(initialValue: Self.makeAuth(settings))
|
|
}
|
|
private static func makeAuth(_ settings: SettingsStore) -> AuthStore {
|
|
guard case .success(let config) = ServerConfig.parse(settings.serverURL) else { preconditionFailure("Invalid configured origin") }
|
|
#if DEBUG
|
|
if ProcessInfo.processInfo.arguments.contains("--ui-testing"), !ProcessInfo.processInfo.arguments.contains("--live-testing") {
|
|
return AuthStore(config: config, tokens: MemoryTokenStore(), fixture: true)
|
|
}
|
|
#endif
|
|
return AuthStore(config: config)
|
|
}
|
|
var body: some View {
|
|
RootContent(settings: settings, auth: auth)
|
|
.task(id: auth.config.baseURL) { await auth.restore() }
|
|
.onChange(of: settings.serverURL) { _, _ in
|
|
guard !auth.fixture else { return }
|
|
guard replacement == nil else { return }
|
|
replacement = Task { @MainActor in
|
|
await auth.logout { await auth.disconnectAccount?() }
|
|
guard !Task.isCancelled else { return }
|
|
// Read latest origin after teardown; intermediate edits never
|
|
// restore credentials or connect an intermediate account.
|
|
auth = Self.makeAuth(settings)
|
|
replacement = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private struct RootContent: View {
|
|
@ObservedObject var settings: SettingsStore
|
|
@ObservedObject var auth: AuthStore
|
|
var body: some View {
|
|
if let user = auth.user {
|
|
ConversationView(settings: settings, auth: auth, user: user, bearerToken: auth.token ?? "")
|
|
.id(auth.config.baseURL.absoluteString + ":" + user.id)
|
|
|
|
} else {
|
|
LoginView(auth: auth, settings: settings)
|
|
}
|
|
}
|
|
}
|