Section 10/171 menit
10. Integrasi dengan SwiftUI
10. Integrasi dengan SwiftUI
Login Screen dengan State Machine
swift
// MARK: - Complete Login Screen
enum LoginStep {
case idle
case authenticating
case authenticated(User)
case needsPasskeySetup // user baru, belum punya passkey
case error(String)
}
@MainActor
@Observable
final class CompleteLoginViewModel {
var step: LoginStep = .idle
var username = ""
var password = ""
var showPasswordField = false
private let authManager = UnifiedAuthManager()
private let registrationManager = PasskeyRegistrationManager()
func loginWithPasskeyOrPassword() async {
step = .authenticating
do {
let serverChallenge = try await AuthAPIClient.shared.beginPasskeyAuthentication()
let result = try await authManager.authenticate(
relyingPartyID: "yourdomain.com",
challenge: Data(base64URLEncoded: serverChallenge.challenge)!
)
switch result {
case .passkey(let response):
let user = try await AuthAPIClient.shared.finishPasskeyAuthentication(
credentialID: response.credentialID.base64URLEncodedString(),
clientDataJSON: response.rawClientDataJSON.base64URLEncodedString(),
authenticatorData: response.rawAuthenticatorData.base64URLEncodedString(),
signature: response.signature.base64URLEncodedString(),
userID: response.userID?.base64URLEncodedString()
)
step = .authenticated(user)
case .password(let user, let pass):
// Login dengan password lama
let loggedUser = try await authenticateWithPassword(username: user, password: pass)
step = .authenticated(loggedUser)
// Tawarkan upgrade ke passkey
await offerPasskeyUpgrade(for: loggedUser)
}
} catch PasskeyError.userCancelled {
step = .idle
} catch {
step = .error(error.localizedDescription)
}
}
private func authenticateWithPassword(username: String, password: String) async throws -> User {
// Legacy password auth
return User(id: "123", name: username)
}
func offerPasskeyUpgrade(for user: User) async {
// Tampilkan prompt "Buat Passkey untuk Login Lebih Cepat?"
step = .needsPasskeySetup
}
func setupPasskey(for user: User) async {
do {
let serverChallenge = try await AuthAPIClient.shared.beginPasskeyRegistration(
username: user.name
)
let challenge = PasskeyRegistrationChallenge(
challenge: Data(base64URLEncoded: serverChallenge.challenge)!,
userID: Data(serverChallenge.userID.utf8),
userName: user.name,
userDisplayName: user.name,
relyingPartyID: "yourdomain.com",
relyingPartyName: "Your App"
)
let response = try await registrationManager.registerPasskey(with: challenge)
try await AuthAPIClient.shared.finishPasskeyRegistration(
credentialID: response.credentialID.base64URLEncodedString(),
clientDataJSON: response.rawClientDataJSON.base64URLEncodedString(),
attestationObject: response.rawAttestationObject.base64URLEncodedString()
)
step = .authenticated(user)
} catch {
step = .authenticated(user) // gagal setup passkey, tetap login
}
}
}
struct LoginScreen: View {
@State private var viewModel = CompleteLoginViewModel()
var body: some View {
NavigationStack {
VStack(spacing: 24) {
Image(systemName: "person.badge.key.fill")
.font(.system(size: 64))
.foregroundStyle(.blue)
Text("Selamat Datang")
.font(.largeTitle.bold())
switch viewModel.step {
case .idle, .error:
loginButtons
case .authenticating:
ProgressView("Memverifikasi...")
case .authenticated(let user):
VStack {
Image(systemName: "checkmark.circle.fill")
.font(.largeTitle)
.foregroundStyle(.green)
Text("Login berhasil, \(user.name)!")
}
case .needsPasskeySetup:
passkeyUpgradePrompt
}
if case .error(let message) = viewModel.step {
Text(message)
.foregroundStyle(.red)
.font(.caption)
}
}
.padding()
.navigationTitle("Login")
}
}
@ViewBuilder
private var loginButtons: some View {
Button {
Task { await viewModel.loginWithPasskeyOrPassword() }
} label: {
Label("Login dengan Face ID / Passkey", systemImage: "faceid")
.frame(maxWidth: .infinity)
}
.buttonStyle(.borderedProminent)
.controlSize(.large)
Button("Login dengan Password") {
viewModel.showPasswordField = true
}
.foregroundStyle(.secondary)
}
@ViewBuilder
private var passkeyUpgradePrompt: some View {
VStack(spacing: 16) {
Text("Buat Passkey?")
.font(.headline)
Text("Login lebih cepat dan aman tanpa password di semua perangkat Apple Anda.")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
Button("Buat Passkey Sekarang") {
if case .needsPasskeySetup = viewModel.step,
case .authenticated(let user) = viewModel.step {
Task { await viewModel.setupPasskey(for: user) }
}
}
.buttonStyle(.borderedProminent)
Button("Lewati") {
// tetap authenticated
}
.foregroundStyle(.secondary)
}
}
}