AppleSignInCoordinator.swift 2.82 KB
//
//  AppleSignInCoordinator.swift
//  Runner
//
//  Created by 权海 on 2026/6/15.
//

import Foundation
import AuthenticationServices

enum PlatformHostApiError: LocalizedError {
   case deallocated
   case invalidAppleCredential
   case missingPresentationAnchor

   var errorDescription: String? {
       switch self {
       case .deallocated:
           return "PlatformHostApiImpl was released before Apple sign-in started."
       case .invalidAppleCredential:
           return "Apple sign-in did not return an Apple ID credential."
       case .missingPresentationAnchor:
           return "Unable to find a window for Apple sign-in presentation."
       }
   }
}

final class AppleSignInCoordinator: NSObject, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
    private let presentationAnchor: ASPresentationAnchor
    private let completion: (Result<AppleSignInModel?, Error>) -> Void
    
    static func currentPresentationAnchor() -> ASPresentationAnchor? {
        if let keyWindow = UIApplication.shared.connectedScenes
            .compactMap({ $0 as? UIWindowScene })
            .flatMap(\.windows)
            .first(where: \.isKeyWindow) {
            return keyWindow
        }

        return nil
    }

    init(presentationAnchor: ASPresentationAnchor, completion: @escaping (Result<AppleSignInModel?, Error>) -> Void) {
        self.presentationAnchor = presentationAnchor
        self.completion = completion
    }

    func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
            completion(.failure(PlatformHostApiError.invalidAppleCredential))
            return
        }

        let nickname = credential.fullName.flatMap {
            let fullName = PersonNameComponentsFormatter.localizedString(from: $0, style: .medium)
            return fullName.isEmpty ? nil : fullName
        }
        let identityToken = credential.identityToken.flatMap { String(data: $0, encoding: .utf8) }

        completion(.success(AppleSignInModel(
            userId: credential.user,
            email: credential.email,
            nickname: nickname,
            identityToken: identityToken
        )))
    }

    func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        let nsError = error as NSError
        if nsError.domain == ASAuthorizationError.errorDomain,
           ASAuthorizationError.Code(rawValue: nsError.code) == .canceled {
            completion(.success(nil))
            return
        }
        completion(.failure(error))
    }

    func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
        presentationAnchor
    }
}