Section 7/171 menit
7. AutoFill dan Conditional UI
7. AutoFill dan Conditional UI
AutoFill memungkinkan passkey muncul di atas keyboard saat user mulai mengetik di field username/email — pengalaman yang sangat seamless. Ini disebut conditional mediation di WebAuthn.
Setup AutoFill di SwiftUI
swift
// MARK: - AutoFill-enabled Login View
@MainActor
@Observable
final class AutoFillLoginViewModel {
var username = ""
var authState: AuthState = .unauthenticated
private let authManager = PasskeyAuthenticationManager()
private var autoFillTask: Task<Void, Never>?
enum AuthState {
case unauthenticated, authenticating, authenticated(User)
}
// Mulai conditional request — passkeys muncul di QuickType bar
func beginAutoFillPasskeyRequest() {
autoFillTask = Task { @MainActor in
await performConditionalPasskeyRequest()
}
}
func cancelAutoFillRequest() {
autoFillTask?.cancel()
}
private func performConditionalPasskeyRequest() async {
guard let serverChallenge = try? await AuthAPIClient.shared.beginPasskeyAuthentication() else {
return
}
let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
relyingPartyIdentifier: "yourdomain.com"
)
let request = provider.createCredentialAssertionRequest(
challenge: Data(base64URLEncoded: serverChallenge.challenge)!
)
request.userVerificationPreference = .required
// Tambahan: password request untuk AutoFill password juga
let passwordProvider = ASAuthorizationPasswordProvider()
let passwordRequest = passwordProvider.createRequest()
let controller = ASAuthorizationController(
authorizationRequests: [request, passwordRequest]
)
// performAutoFillAssistedRequests — berbeda dari performRequests biasa!
// Tidak memunculkan full-screen sheet; hanya tambah entri ke QuickType bar
controller.performAutoFillAssistedRequests()
}
}
struct AutoFillLoginView: View {
@State private var viewModel = AutoFillLoginViewModel()
var body: some View {
VStack(spacing: 20) {
TextField("Email atau Username", text: $viewModel.username)
.textContentType(.username) // penting untuk AutoFill
.keyboardType(.emailAddress)
.autocapitalization(.none)
.textFieldStyle(.roundedBorder)
// AutoFill passkeys muncul di atas keyboard keyboard secara otomatis
Button("Login dengan Passkey") {
Task { await viewModel.loginWithPasskey() }
}
.buttonStyle(.borderedProminent)
}
.padding()
.onAppear {
// Mulai conditional request saat view muncul
viewModel.beginAutoFillPasskeyRequest()
}
.onDisappear {
viewModel.cancelAutoFillRequest()
}
}
}
extension AutoFillLoginViewModel {
func loginWithPasskey() async {
// Full-screen passkey request (bukan conditional)
authState = .authenticating
do {
let serverChallenge = try await AuthAPIClient.shared.beginPasskeyAuthentication()
let challenge = PasskeyAssertionChallenge(
challenge: Data(base64URLEncoded: serverChallenge.challenge)!,
relyingPartyID: "yourdomain.com",
allowedCredentialIDs: []
)
let response = try await authManager.authenticate(with: challenge)
let user = try await AuthAPIClient.shared.finishPasskeyAuthentication(
credentialID: response.credentialID.base64URLEncodedString(),
clientDataJSON: response.rawClientDataJSON.base64URLEncodedString(),
authenticatorData: response.rawAuthenticatorData.base64URLEncodedString(),
signature: response.signature.base64URLEncodedString(),
userID: nil
)
authState = .authenticated(user)
} catch {
authState = .unauthenticated
}
}
}
// MARK: - Placeholder types
struct User { let id: String; let name: String }
struct AuthAPIClient {
static let shared = AuthAPIClient()
func beginPasskeyRegistration(username: String) async throws -> (challenge: String, userID: String, displayName: String) { ("", "", "") }
func finishPasskeyRegistration(credentialID: String, clientDataJSON: String, attestationObject: String) async throws {}
func beginPasskeyAuthentication() async throws -> (challenge: String) { ("") }
func finishPasskeyAuthentication(credentialID: String, clientDataJSON: String, authenticatorData: String, signature: String, userID: String?) async throws -> User { User(id: "", name: "") }
}