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