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: "") }
}