Section 12/161 menit
12. Migrasi Bertahap: Password ke Passkey
12. Migrasi Bertahap: Password ke Passkey
Strategi 4 Fase
swift
Fase 1 — Offer (non-disruptive):
- Tambah "Setup Passkey" di Settings/Profile
- Tidak ada prompt otomatis
- Track berapa user yang adopt
Fase 2 — Promote (setelah login):
- Setelah user login dengan password, tampilkan upgrade sheet
- "Buat login lebih mudah dengan Face ID"
- Bisa di-dismiss (skip once atau "don't ask again")
Fase 3 — Prefer (default ke passkey):
- AutoFill passkey aktif secara default
- Password masih tersedia tapi bukan default
- Conditional UI di semua layar login
Fase 4 — Password-optional (jangka panjang):
- User dengan passkey bisa hapus password
- New user bisa daftar tanpa password
- Password masih ada untuk recovery
Implementasi Upgrade Flow
swift
import AuthenticationServices
@Observable
class AuthUpgradeManager {
var shouldPromptUpgrade = false
private let upgradeKey = "passkeyUpgradeOfferedCount"
private let maxOfferCount = 3
func checkAndPromptUpgrade() {
let offerCount = UserDefaults.standard.integer(forKey: upgradeKey)
let hasPasskey = UserDefaults.standard.bool(forKey: "hasPasskeyRegistered")
guard !hasPasskey,
offerCount < maxOfferCount,
isPasskeySupported() else { return }
// Delay prompt sedikit setelah login berhasil
Task {
try await Task.sleep(nanoseconds: 2_000_000_000)
await MainActor.run {
shouldPromptUpgrade = true
}
}
}
func isPasskeySupported() -> Bool {
if #available(iOS 16, *) {
return true
}
return false
}
func userDeclinedUpgrade() {
let count = UserDefaults.standard.integer(forKey: upgradeKey)
UserDefaults.standard.set(count + 1, forKey: upgradeKey)
shouldPromptUpgrade = false
}
func userAcceptedUpgrade() async throws {
// Lakukan registrasi passkey
let coordinator = AuthenticationCoordinator()
let challenge = try await server.fetchRegistrationChallenge()
let username = try currentUsername()
let registration = try await coordinator.registerPasskey(
username: username,
userID: Data(username.utf8).sha256(),
challenge: challenge,
rpID: "example.com"
)
// Kirim ke server
try await server.addPasskey(registration, forUser: username)
// Simpan flag lokal
UserDefaults.standard.set(true, forKey: "hasPasskeyRegistered")
UserDefaults.standard.set(
registration.credentialID.base64EncodedString(),
forKey: "passkeyCredentialID"
)
shouldPromptUpgrade = false
// Track analytics
Analytics.log("passkey_registered", parameters: ["source": "upgrade_prompt"])
}
}
// SwiftUI upgrade sheet
struct PasskeyUpgradeSheet: View {
@State private var upgradeManager = AuthUpgradeManager()
@State private var isRegistering = false
var body: some View {
VStack(spacing: 20) {
Image(systemName: "faceid")
.font(.system(size: 60))
.foregroundStyle(.blue)
Text("Login Lebih Mudah dengan Face ID")
.font(.title2.bold())
Text("Gunakan Face ID atau Touch ID untuk masuk — tidak perlu ingat password lagi.")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
Button("Aktifkan Face ID Login") {
Task {
isRegistering = true
try? await upgradeManager.userAcceptedUpgrade()
isRegistering = false
}
}
.buttonStyle(.borderedProminent)
.disabled(isRegistering)
Button("Nanti Saja") {
upgradeManager.userDeclinedUpgrade()
}
.foregroundStyle(.secondary)
}
.padding(32)
}
}