176 lines
6.6 KiB
Swift
176 lines
6.6 KiB
Swift
import SwiftUI
|
|
|
|
struct PairingView: View {
|
|
var appState: AppState
|
|
@Bindable var authViewModel: AuthViewModel
|
|
@State private var email = ""
|
|
@State private var password = ""
|
|
@State private var pairingCode: String?
|
|
@State private var isStarting = false
|
|
@State private var isWaiting = false
|
|
@State private var statusMessage: String?
|
|
@State private var isError = false
|
|
@Environment(\.dismiss) private var dismiss
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
ScrollView {
|
|
VStack(spacing: 24) {
|
|
Image(systemName: "iphone.and.arrow.forward")
|
|
.font(.system(size: 48))
|
|
.foregroundStyle(.blue)
|
|
|
|
Text("Device Pairing")
|
|
.font(.title2.bold())
|
|
|
|
Text("Transfer your keys from an existing device to this one.")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
if pairingCode == nil {
|
|
// Phase 1: Enter email and start pairing
|
|
VStack(spacing: 16) {
|
|
// Server config
|
|
DisclosureGroup("Server") {
|
|
TextField("Host", text: $authViewModel.serverHost)
|
|
.textContentType(.URL)
|
|
.autocapitalization(.none)
|
|
TextField("Port", text: $authViewModel.serverPort)
|
|
.keyboardType(.numberPad)
|
|
}
|
|
|
|
TextField("Email", text: $email)
|
|
.textContentType(.emailAddress)
|
|
.keyboardType(.emailAddress)
|
|
.autocapitalization(.none)
|
|
.textFieldStyle(.roundedBorder)
|
|
|
|
SecureField("Password (for key encryption)", text: $password)
|
|
.textContentType(.password)
|
|
.autocorrectionDisabled()
|
|
.textInputAutocapitalization(.never)
|
|
.textFieldStyle(.roundedBorder)
|
|
|
|
Button("Start Pairing") {
|
|
Task { await startPairing() }
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(email.isEmpty || password.isEmpty || isStarting)
|
|
|
|
if isStarting {
|
|
ProgressView("Connecting...")
|
|
}
|
|
}
|
|
} else {
|
|
// Phase 2: Show code and wait for authorization
|
|
VStack(spacing: 16) {
|
|
Text("Pairing Code")
|
|
.font(.headline)
|
|
|
|
Text(pairingCode!)
|
|
.font(.system(size: 36, weight: .bold, design: .monospaced))
|
|
.padding()
|
|
.background(Color(.systemGray6))
|
|
.clipShape(RoundedRectangle(cornerRadius: 12))
|
|
|
|
Text("Enter this code on your already logged-in device\nto authorize this device.")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.center)
|
|
|
|
if isWaiting {
|
|
ProgressView("Waiting for authorization...")
|
|
.padding()
|
|
}
|
|
}
|
|
}
|
|
|
|
if let status = statusMessage {
|
|
Text(status)
|
|
.font(.caption)
|
|
.foregroundStyle(isError ? .red : .green)
|
|
.multilineTextAlignment(.center)
|
|
}
|
|
}
|
|
.padding(32)
|
|
}
|
|
.navigationTitle("Pair Device")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .topBarLeading) {
|
|
Button("Cancel") { dismiss() }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func startPairing() async {
|
|
isStarting = true
|
|
isError = false
|
|
statusMessage = nil
|
|
|
|
// Connect to server
|
|
if await !appState.chatClient.isConnected {
|
|
do {
|
|
let port = UInt16(authViewModel.serverPort) ?? Constants.defaultPort
|
|
try await appState.chatClient.connect(
|
|
host: authViewModel.serverHost, port: port
|
|
)
|
|
} catch {
|
|
isStarting = false
|
|
statusMessage = "Connection failed: \(error.localizedDescription)"
|
|
isError = true
|
|
return
|
|
}
|
|
}
|
|
|
|
let (success, codeOrMsg) = await appState.chatClient.pairingStart(email: email)
|
|
isStarting = false
|
|
|
|
if success {
|
|
pairingCode = codeOrMsg
|
|
// Start waiting for authorization
|
|
isWaiting = true
|
|
Task { await waitForAuthorization() }
|
|
} else {
|
|
statusMessage = codeOrMsg
|
|
isError = true
|
|
}
|
|
}
|
|
|
|
private func waitForAuthorization() async {
|
|
let (success, msg) = await appState.chatClient.pairingWait(
|
|
code: pairingCode!, email: email, password: password
|
|
)
|
|
isWaiting = false
|
|
|
|
if success {
|
|
statusMessage = msg
|
|
isError = false
|
|
// Auto-login
|
|
let (loginOk, loginMsg) = await appState.chatClient.login(email: email, password: password)
|
|
if loginOk {
|
|
appState.email = email
|
|
appState.isLoggedIn = true
|
|
appState.connectionStatus = .connected
|
|
appState.startConnectionMonitor()
|
|
if let userId = await appState.chatClient.userId {
|
|
appState.currentUser = User(
|
|
id: userId,
|
|
username: await appState.chatClient.username,
|
|
email: email
|
|
)
|
|
}
|
|
dismiss()
|
|
} else {
|
|
statusMessage = "Keys imported but login failed: \(loginMsg)"
|
|
isError = true
|
|
}
|
|
} else {
|
|
statusMessage = msg
|
|
isError = true
|
|
}
|
|
}
|
|
}
|