Clients can use VMC or a custom server with email/password or device code; API locks after 3 failed logins with admin unlock. Co-authored-by: Cursor <cursoragent@cursor.com>
121 lines
4.5 KiB
Swift
121 lines
4.5 KiB
Swift
import SwiftUI
|
|
|
|
struct LinkScreen: View {
|
|
@EnvironmentObject private var app: AppModel
|
|
@State private var code: String?
|
|
@State private var linkUrl = ""
|
|
@State private var status = "Verbinden…"
|
|
@State private var error: String?
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
LinearGradient(
|
|
colors: [
|
|
Color(red: 0.12, green: 0.04, blue: 0.06),
|
|
McColors.bg,
|
|
Color(red: 0.04, green: 0.05, blue: 0.1),
|
|
],
|
|
startPoint: .topLeading,
|
|
endPoint: .bottomTrailing
|
|
)
|
|
.ignoresSafeArea()
|
|
|
|
VStack(spacing: 18) {
|
|
Text("VMC")
|
|
.font(McFonts.brand)
|
|
.foregroundStyle(McColors.accent)
|
|
.shadow(color: McColors.accent.opacity(0.4), radius: 18)
|
|
|
|
Text("Koppel dit apparaat")
|
|
.font(McFonts.title)
|
|
.foregroundStyle(McColors.text)
|
|
|
|
Text("Open je Vonas-account, voer deze code in en bevestig.")
|
|
.font(McFonts.body)
|
|
.foregroundStyle(McColors.textDim)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal, 28)
|
|
|
|
if let code {
|
|
Text(code)
|
|
.font(.system(size: 42, weight: .bold, design: .monospaced))
|
|
.foregroundStyle(McColors.text)
|
|
.padding(.vertical, 10)
|
|
.padding(.horizontal, 22)
|
|
.background(McColors.surface.opacity(0.9), in: RoundedRectangle(cornerRadius: 16, style: .continuous))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
|
.stroke(McColors.accent.opacity(0.35), lineWidth: 1)
|
|
)
|
|
} else if error == nil {
|
|
ProgressView().tint(.white)
|
|
}
|
|
|
|
if !linkUrl.isEmpty {
|
|
Text(linkUrl)
|
|
.font(McFonts.caption)
|
|
.foregroundStyle(McColors.textDim)
|
|
}
|
|
|
|
Text(status)
|
|
.font(McFonts.caption)
|
|
.foregroundStyle(McColors.textDim)
|
|
|
|
if let error {
|
|
Text(error)
|
|
.font(McFonts.body)
|
|
.foregroundStyle(McColors.danger)
|
|
.multilineTextAlignment(.center)
|
|
.padding(.horizontal)
|
|
Button("Opnieuw") { Task { await startLink() } }
|
|
.buttonStyle(McPrimaryButtonStyle())
|
|
}
|
|
|
|
Button("Change server") { app.phase = .server }
|
|
.buttonStyle(McSecondaryButtonStyle())
|
|
Button("Back") { app.phase = .auth }
|
|
.buttonStyle(McSecondaryButtonStyle())
|
|
}
|
|
.padding()
|
|
.frame(maxWidth: 420)
|
|
}
|
|
.task { await startLink() }
|
|
}
|
|
|
|
private func startLink() async {
|
|
error = nil
|
|
code = nil
|
|
status = "Verbinden…"
|
|
await app.api.configure(baseUrl: app.store.apiBase, token: nil)
|
|
do {
|
|
let created = try await app.api.createDeviceCode(deviceName: AppModel.deviceName)
|
|
code = created.code
|
|
linkUrl = created.linkUrl ?? ""
|
|
status = "Wachten tot je de code bevestigt…"
|
|
let interval = max(created.pollIntervalSeconds ?? 2, 2)
|
|
while true {
|
|
try await Task.sleep(nanoseconds: UInt64(interval) * 1_000_000_000)
|
|
switch try await app.api.pollDeviceCode(code: created.code) {
|
|
case .linked(let token, let name):
|
|
status = "Welkom, \(name)"
|
|
try await Task.sleep(nanoseconds: 700_000_000)
|
|
await app.didLink(token: token)
|
|
return
|
|
case .expired:
|
|
status = "Code verlopen"
|
|
error = "Code verlopen — probeer opnieuw"
|
|
return
|
|
case .alreadyLinked:
|
|
status = "Al gekoppeld"
|
|
error = "Al gekoppeld — herstart de app"
|
|
return
|
|
case .pending:
|
|
break
|
|
}
|
|
}
|
|
} catch {
|
|
self.error = error.localizedDescription
|
|
status = "Mislukt"
|
|
}
|
|
}
|
|
}
|