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