Section 11/171 menit

11. Integrasi dengan UIKit

11. Integrasi dengan UIKit

swift
// MARK: - UIKit Login ViewController

final class PasskeyLoginViewController: UIViewController {
    private let authManager = UnifiedAuthManager()
    private lazy var passkeyButton: UIButton = {
        var config = UIButton.Configuration.filled()
        config.title = "Login dengan Face ID / Passkey"
        config.image = UIImage(systemName: "faceid")
        config.imagePadding = 8
        let button = UIButton(configuration: config)
        button.addTarget(self, action: #selector(loginWithPasskeyTapped), for: .touchUpInside)
        return button
    }()

    private lazy var activityIndicator = UIActivityIndicatorView(style: .medium)

    override func viewDidLoad() {
        super.viewDidLoad()
        setupUI()
        // Mulai AutoFill passive request saat view load
        beginConditionalPasskeyRequest()
    }

    @objc private func loginWithPasskeyTapped() {
        Task { await performPasskeyLogin() }
    }

    private func beginConditionalPasskeyRequest() {
        guard let challenge = fetchChallengeSync() else { return }

        let provider = ASAuthorizationPlatformPublicKeyCredentialProvider(
            relyingPartyIdentifier: "yourdomain.com"
        )
        let request = provider.createCredentialAssertionRequest(challenge: challenge)
        let passwordRequest = ASAuthorizationPasswordProvider().createRequest()

        let controller = ASAuthorizationController(
            authorizationRequests: [request, passwordRequest]
        )
        controller.delegate = self
        controller.presentationContextProvider = self
        controller.performAutoFillAssistedRequests()
    }

    @MainActor
    private func performPasskeyLogin() async {
        setLoading(true)
        do {
            guard let challengeData = try? await AuthAPIClient.shared.beginPasskeyAuthentication() else {
                setLoading(false)
                return
            }
            let result = try await authManager.authenticate(
                relyingPartyID: "yourdomain.com",
                challenge: Data(base64URLEncoded: challengeData.challenge)!
            )
            handleAuthResult(result)
        } catch PasskeyError.userCancelled {
            // do nothing
        } catch {
            showError(error.localizedDescription)
        }
        setLoading(false)
    }

    private func handleAuthResult(_ result: UnifiedAuthManager.AuthResult) {
        switch result {
        case .passkey:
            navigateToDashboard()
        case .password(let user, let pass):
            Task {
                // handle password login
                navigateToDashboard()
            }
        }
    }

    private func navigateToDashboard() {
        let dashboard = DashboardViewController()
        navigationController?.pushViewController(dashboard, animated: true)
    }

    private func setLoading(_ loading: Bool) {
        passkeyButton.isEnabled = !loading
        loading ? activityIndicator.startAnimating() : activityIndicator.stopAnimating()
    }

    private func showError(_ message: String) {
        let alert = UIAlertController(title: "Error", message: message, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "OK", style: .default))
        present(alert, animated: true)
    }

    private func fetchChallengeSync() -> Data? {
        // Untuk AutoFill, challenge perlu diambil synchronously atau dari cache
        // Dalam produksi: gunakan pre-fetched challenge atau request asynchronously sebelumnya
        return Data(repeating: 0, count: 32)  // placeholder
    }

    private func setupUI() {
        view.backgroundColor = .systemBackground
        [passkeyButton, activityIndicator].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview($0)
        }
        NSLayoutConstraint.activate([
            passkeyButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            passkeyButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            passkeyButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
            passkeyButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
            activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            activityIndicator.topAnchor.constraint(equalTo: passkeyButton.bottomAnchor, constant: 16)
        ])
    }
}

extension PasskeyLoginViewController: ASAuthorizationControllerDelegate {
    func authorizationController(
        controller: ASAuthorizationController,
        didCompleteWithAuthorization authorization: ASAuthorization
    ) {
        if let passkey = authorization.credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
            Task {
                // proses passkey assertion
                navigateToDashboard()
            }
        } else if let password = authorization.credential as? ASPasswordCredential {
            Task {
                // proses password credential
                navigateToDashboard()
            }
        }
    }

    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        if let authError = error as? ASAuthorizationError, authError.code != .canceled {
            showError(error.localizedDescription)
        }
    }
}

extension PasskeyLoginViewController: ASAuthorizationControllerPresentationContextProviding {
    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        view.window ?? UIWindow()
    }
}

class DashboardViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .systemBackground
        title = "Dashboard"
    }
}