- backend: /api/magic/{register,devices,profile,release} + AdGuard provisioning + 24h cooldown
- prisma: magic_binding_fields migration (additive on UserDevice)
- mac-app: Phase 2 - Login + MacRegistration + Profile install
- marketing: landing section + /download/rebreakmagic + DMG
- lyra: forbidden phrases + RebreakMagic coach guidance
121 lines
4.0 KiB
Swift
121 lines
4.0 KiB
Swift
import SwiftUI
|
|
|
|
struct LoginView: View {
|
|
@State private var email = ""
|
|
@State private var password = ""
|
|
@State private var isLoading = false
|
|
@State private var errorMessage: String?
|
|
|
|
let onSuccess: (AuthSession) -> Void
|
|
|
|
var body: some View {
|
|
VStack(spacing: 24) {
|
|
// Logo + Header
|
|
VStack(spacing: 12) {
|
|
Image(systemName: "shield.checkered")
|
|
.font(.system(size: 64))
|
|
.foregroundStyle(.blue)
|
|
|
|
Text("ReBreak Magic")
|
|
.font(.title.bold())
|
|
|
|
Text("Bitte mit deinem ReBreak-Account anmelden")
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.top, 40)
|
|
|
|
// Form
|
|
VStack(spacing: 16) {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text("Email")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
|
|
TextField("name@example.com", text: $email)
|
|
.textFieldStyle(.roundedBorder)
|
|
.textContentType(.emailAddress)
|
|
.autocorrectionDisabled()
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text("Passwort")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
|
|
SecureField("••••••••", text: $password)
|
|
.textFieldStyle(.roundedBorder)
|
|
.textContentType(.password)
|
|
}
|
|
|
|
if let error = errorMessage {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: "exclamationmark.triangle.fill")
|
|
.foregroundStyle(.red)
|
|
Text(error)
|
|
.font(.caption)
|
|
.foregroundStyle(.red)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
|
|
Button(action: handleSignIn) {
|
|
HStack {
|
|
if isLoading {
|
|
ProgressView()
|
|
.controlSize(.small)
|
|
.tint(.white)
|
|
}
|
|
Text(isLoading ? "Anmeldung läuft..." : "Anmelden")
|
|
.fontWeight(.medium)
|
|
}
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.vertical, 8)
|
|
}
|
|
.buttonStyle(.borderedProminent)
|
|
.disabled(email.isEmpty || password.isEmpty || isLoading)
|
|
}
|
|
.padding(.horizontal, 40)
|
|
.frame(maxWidth: 400)
|
|
|
|
Spacer()
|
|
|
|
// Signup Link
|
|
HStack(spacing: 4) {
|
|
Text("Noch kein Account?")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
|
|
Link("Jetzt registrieren →", destination: URL(string: "https://rebreak.org/signup")!)
|
|
.font(.caption)
|
|
}
|
|
.padding(.bottom, 20)
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
.background(Color(nsColor: .windowBackgroundColor))
|
|
}
|
|
|
|
private func handleSignIn() {
|
|
Task {
|
|
isLoading = true
|
|
errorMessage = nil
|
|
|
|
do {
|
|
let session = try await AuthService.shared.signIn(email: email, password: password)
|
|
onSuccess(session)
|
|
} catch {
|
|
errorMessage = error.localizedDescription
|
|
}
|
|
|
|
isLoading = false
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
LoginView { session in
|
|
print("Logged in: \(session.email)")
|
|
}
|
|
.frame(width: 720, height: 600)
|
|
}
|