271 lines
11 KiB
Swift
271 lines
11 KiB
Swift
import SwiftUI
|
||
import AuthenticationServices
|
||
import CryptoKit
|
||
import os.log
|
||
|
||
/// 自定义的 Apple 登录按钮组件
|
||
struct AppleSignInButton: View {
|
||
// MARK: - 属性
|
||
|
||
/// 授权请求回调
|
||
let onRequest: (ASAuthorizationAppleIDRequest) -> Void
|
||
|
||
/// 授权完成回调
|
||
let onCompletion: (Result<ASAuthorization, Error>) -> Void
|
||
|
||
/// 按钮文字
|
||
let buttonText: String
|
||
|
||
// 创建日志记录器
|
||
private let logger = Logger(subsystem: "com.yourapp", category: "AppleSignInButton")
|
||
|
||
// 添加一个强引用到coordinator
|
||
@State private var coordinator: Coordinator?
|
||
|
||
// MARK: - 初始化方法
|
||
|
||
init(buttonText: String = "Continue with Apple",
|
||
onRequest: @escaping (ASAuthorizationAppleIDRequest) -> Void,
|
||
onCompletion: @escaping (Result<ASAuthorization, Error>) -> Void) {
|
||
self.buttonText = buttonText
|
||
self.onRequest = onRequest
|
||
self.onCompletion = onCompletion
|
||
logger.debug("AppleSignInButton 初始化,按钮文字: \(buttonText)")
|
||
}
|
||
|
||
// MARK: - 视图主体
|
||
|
||
var body: some View {
|
||
Button(action: handleSignIn) {
|
||
HStack(alignment: .center, spacing: 8) {
|
||
Image(systemName: "applelogo")
|
||
.font(.system(size: 20, weight: .regular))
|
||
Text(buttonText)
|
||
.font(.system(size: 18, weight: .regular))
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.frame(height: 60)
|
||
.background(Color.white)
|
||
.foregroundColor(.black)
|
||
.cornerRadius(30)
|
||
.overlay(
|
||
RoundedRectangle(cornerRadius: 30)
|
||
.stroke(Color.black, lineWidth: 1)
|
||
)
|
||
}
|
||
}
|
||
|
||
// MARK: - 私有方法
|
||
|
||
private func handleSignIn() {
|
||
logger.debug("🍎 用户点击了Apple登录按钮")
|
||
|
||
let provider = ASAuthorizationAppleIDProvider()
|
||
let request = provider.createRequest()
|
||
request.requestedScopes = [.fullName, .email]
|
||
|
||
// 创建 nonce 用于安全验证
|
||
let nonce = randomNonceString(length: 32)
|
||
logger.debug("🔑 生成Nonce: \(nonce)")
|
||
|
||
request.nonce = sha256(nonce)
|
||
logger.debug("🔐 Nonce的SHA256: \(request.nonce ?? "nil")")
|
||
|
||
// 调用请求回调
|
||
logger.debug("📞 调用onRequest回调")
|
||
onRequest(request)
|
||
|
||
// 创建并显示授权控制器
|
||
logger.debug("🔄 创建ASAuthorizationController")
|
||
let controller = ASAuthorizationController(authorizationRequests: [request])
|
||
|
||
// 创建presentation context provider
|
||
let presentationContextProvider = PresentationContextProvider()
|
||
|
||
// 创建coordinator并保持强引用
|
||
let newCoordinator = Coordinator(
|
||
onCompletion: { [logger] result in
|
||
// 处理完成后释放coordinator
|
||
DispatchQueue.main.async {
|
||
self.coordinator = nil
|
||
}
|
||
|
||
switch result {
|
||
case .success(let auth):
|
||
logger.debug("✅ 授权成功 - 开始处理授权响应")
|
||
|
||
if let appleIDCredential = auth.credential as? ASAuthorizationAppleIDCredential {
|
||
let userIdentifier = appleIDCredential.user
|
||
logger.debug("👤 用户标识符: \(userIdentifier)")
|
||
|
||
// 保存用户ID用于后续验证
|
||
UserDefaults.standard.set(userIdentifier, forKey: "appleAuthorizedUserIdKey")
|
||
}
|
||
case .failure(let error):
|
||
let nsError = error as NSError
|
||
logger.error("❌ 授权失败: \(nsError.localizedDescription)")
|
||
}
|
||
|
||
// 调用完成回调
|
||
self.onCompletion(result)
|
||
},
|
||
logger: logger
|
||
)
|
||
|
||
// 保存coordinator的强引用
|
||
self.coordinator = newCoordinator
|
||
|
||
// 设置代理和presentation context provider
|
||
controller.delegate = newCoordinator
|
||
controller.presentationContextProvider = presentationContextProvider
|
||
|
||
// 执行请求
|
||
logger.debug("🚀 开始执行授权请求...")
|
||
DispatchQueue.main.async {
|
||
controller.performRequests()
|
||
}
|
||
}
|
||
|
||
private func logAppleIDError(_ error: ASAuthorizationError, logger: Logger) {
|
||
switch error.code {
|
||
case .canceled:
|
||
logger.error("❌ 用户取消了授权")
|
||
case .failed:
|
||
logger.error("❌ 授权请求失败")
|
||
case .invalidResponse:
|
||
logger.error("❌ 无效的授权响应")
|
||
case .notHandled:
|
||
logger.error("❌ 授权请求未被处理")
|
||
case .unknown:
|
||
logger.error("❌ 未知的授权错误")
|
||
@unknown default:
|
||
logger.error("❌ 未处理的授权错误")
|
||
}
|
||
}
|
||
|
||
private func sha256(_ input: String) -> String {
|
||
let inputData = Data(input.utf8)
|
||
let hashedData = SHA256.hash(data: inputData)
|
||
return hashedData.compactMap { String(format: "%02x", $0) }.joined()
|
||
}
|
||
|
||
// 使用项目中的现有方法生成随机字符串
|
||
private func randomNonceString(length: Int) -> String {
|
||
let letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
return String((0..<length).map{ _ in letters.randomElement()! })
|
||
}
|
||
|
||
// MARK: - Coordinator
|
||
|
||
private class Coordinator: NSObject, ASAuthorizationControllerDelegate {
|
||
let onCompletion: (Result<ASAuthorization, Error>) -> Void
|
||
private let logger: Logger
|
||
|
||
init(onCompletion: @escaping (Result<ASAuthorization, Error>) -> Void, logger: Logger) {
|
||
self.onCompletion = onCompletion
|
||
self.logger = logger
|
||
super.init()
|
||
logger.debug("Coordinator 初始化")
|
||
}
|
||
|
||
// 授权成功回调
|
||
func authorizationController(controller: ASAuthorizationController,
|
||
didCompleteWithAuthorization authorization: ASAuthorization) {
|
||
logger.debug("✅ 授权成功 - 开始处理授权响应")
|
||
|
||
if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
|
||
// 创建用户标识符
|
||
let userIdentifier = appleIDCredential.user
|
||
logger.debug("👤 用户标识符: \(userIdentifier)")
|
||
|
||
// 检查是否是新用户
|
||
if let email = appleIDCredential.email, let fullName = appleIDCredential.fullName {
|
||
logger.debug("👋 检测到新用户")
|
||
logger.debug("📧 新用户邮箱: \(email)")
|
||
logger.debug("👤 新用户全名: \(fullName.givenName ?? "") \(fullName.familyName ?? "")")
|
||
} else {
|
||
logger.debug("👋 现有用户登录")
|
||
}
|
||
|
||
// 保存用户标识符到UserDefaults
|
||
UserDefaults.standard.set(userIdentifier, forKey: "appleAuthorizedUserIdKey")
|
||
|
||
// 获取授权码
|
||
if let authCodeData = appleIDCredential.authorizationCode,
|
||
let _ = String(data: authCodeData, encoding: .utf8) {
|
||
logger.debug("🔑 成功获取授权码")
|
||
} else {
|
||
logger.warning("⚠️ 未获取到授权码")
|
||
}
|
||
|
||
// 获取身份令牌
|
||
if let identityTokenData = appleIDCredential.identityToken,
|
||
let _ = String(data: identityTokenData, encoding: .utf8) {
|
||
logger.debug("🔐 成功获取身份令牌")
|
||
} else {
|
||
logger.error("❌ 获取身份令牌失败")
|
||
}
|
||
} else {
|
||
logger.error("❌ 无法获取有效的 Apple ID 凭证")
|
||
}
|
||
|
||
// 调用完成处理程序
|
||
logger.debug("🔄 调用完成处理程序")
|
||
onCompletion(.success(authorization))
|
||
}
|
||
|
||
// 授权失败回调
|
||
func authorizationController(controller: ASAuthorizationController,
|
||
didCompleteWithError error: Error) {
|
||
let nsError = error as NSError
|
||
logger.error("❌ 授权失败: \(nsError.localizedDescription)")
|
||
logger.error("错误域: \(nsError.domain), 错误码: \(nsError.code)")
|
||
|
||
if let appleIDError = error as? ASAuthorizationError {
|
||
switch appleIDError.code {
|
||
case .canceled:
|
||
logger.error("❌ 用户取消了授权")
|
||
case .failed:
|
||
logger.error("❌ 授权请求失败")
|
||
case .invalidResponse:
|
||
logger.error("❌ 无效的授权响应")
|
||
case .notHandled:
|
||
logger.error("❌ 授权请求未被处理")
|
||
case .unknown:
|
||
logger.error("❌ 未知的授权错误")
|
||
@unknown default:
|
||
logger.error("❌ 未处理的授权错误")
|
||
}
|
||
|
||
// 记录更多错误详情
|
||
if let errorString = (appleIDError as NSError).userInfo[NSDebugDescriptionErrorKey] as? String {
|
||
logger.error("🔍 错误详情: \(errorString)")
|
||
}
|
||
}
|
||
|
||
// 调用完成处理程序
|
||
logger.debug("🔄 调用完成处理程序(错误)")
|
||
onCompletion(.failure(error))
|
||
}
|
||
}
|
||
|
||
// MARK: - Presentation Context Provider
|
||
|
||
private class PresentationContextProvider: NSObject, ASAuthorizationControllerPresentationContextProviding {
|
||
private let logger = Logger(subsystem: "com.yourapp.applesignin", category: "PresentationContextProvider")
|
||
|
||
override init() {
|
||
super.init()
|
||
logger.debug("PresentationContextProvider 初始化")
|
||
}
|
||
|
||
func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
|
||
logger.debug("获取presentation anchor")
|
||
guard let window = UIApplication.shared.windows.first(where: { $0.isKeyWindow }) else {
|
||
logger.error("无法获取key window,将使用第一个window")
|
||
return UIApplication.shared.windows.first ?? UIWindow()
|
||
}
|
||
return window
|
||
}
|
||
}
|
||
} |