392 lines
15 KiB
Swift
392 lines
15 KiB
Swift
import SwiftUI
|
||
import AuthenticationServices
|
||
import Alamofire
|
||
import CryptoKit
|
||
import Foundation
|
||
|
||
/// 主登录视图 - 处理苹果登录
|
||
struct LoginView: View {
|
||
// MARK: - Properties
|
||
|
||
@State private var isLoading = false
|
||
@State private var showError = false
|
||
@State private var errorMessage = ""
|
||
@State private var currentNonce: String?
|
||
@State private var isLoggedIn = false
|
||
// MARK: - Body
|
||
|
||
var body: some View {
|
||
ZStack {
|
||
// Background
|
||
Color(red: 1.0, green: 0.67, blue: 0.15)
|
||
.ignoresSafeArea()
|
||
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
Text("Hi, I'm MeMo!")
|
||
.font(Typography.font(for: .largeTitle, family: .quicksandBold))
|
||
.foregroundColor(.black)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(.horizontal, 24)
|
||
.padding(.top, 44)
|
||
|
||
Text("Welcome~")
|
||
.font(Typography.font(for: .largeTitle, family: .quicksandBold))
|
||
.foregroundColor(.black)
|
||
.frame(maxWidth: .infinity, alignment: .leading)
|
||
.padding(.horizontal, 24)
|
||
.padding(.bottom, 20)
|
||
|
||
Spacer()
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||
|
||
VStack(spacing: 16) {
|
||
Spacer()
|
||
signInButton()
|
||
.padding(.horizontal, 24)
|
||
termsAndPrivacyView()
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
.alert(isPresented: $showError) {
|
||
Alert(
|
||
title: Text("Error"),
|
||
message: Text(errorMessage),
|
||
dismissButton: .default(Text("OK"))
|
||
)
|
||
}
|
||
|
||
if isLoading {
|
||
loadingView()
|
||
}
|
||
}
|
||
.navigationBarBackButtonHidden(true)
|
||
.navigationBarHidden(true)
|
||
}
|
||
|
||
// MARK: - Views
|
||
|
||
private func signInButton() -> some View {
|
||
print(" [1] 用户点击了登录按钮")
|
||
return AppleSignInButton { request in
|
||
print(" 开始创建登录请求")
|
||
let nonce = String.randomURLSafeString(length: 32)
|
||
self.currentNonce = nonce
|
||
request.nonce = self.sha256(nonce)
|
||
// 不请求任何可识别个人信息的范围,遵循最小化数据收集原则
|
||
// 若业务未来确有需要,可根据功能在提交审核前按需开启
|
||
// request.requestedScopes = [.fullName, .email]
|
||
print(" 登录请求配置完成,nonce 已设置")
|
||
} onCompletion: { result in
|
||
print(" 收到 Apple 登录回调")
|
||
switch result {
|
||
case .success(let authResults):
|
||
print(" Apple 登录授权成功,开始处理凭证")
|
||
if let appleIDCredential = authResults.credential as? ASAuthorizationAppleIDCredential {
|
||
print(" 成功获取 Apple ID 凭证")
|
||
self.processAppleIDCredential(appleIDCredential)
|
||
} else {
|
||
print(" 凭证类型转换失败")
|
||
print(" 凭证类型: \(type(of: authResults.credential))")
|
||
self.showError(message: "无法处理登录凭证")
|
||
}
|
||
case .failure(let error):
|
||
print(" Apple 登录失败: \(error.localizedDescription)")
|
||
print(" 错误详情: \(error as NSError)")
|
||
self.handleSignInError(error)
|
||
}
|
||
}
|
||
}
|
||
|
||
private func termsAndPrivacyView() -> some View {
|
||
VStack(spacing: 4) {
|
||
HStack {
|
||
Text("By continuing, you agree to our")
|
||
.font(.caption)
|
||
.foregroundColor(.themeTextMessage)
|
||
|
||
Button("Terms of") {
|
||
openURL("https://memorywake.com/privacy-policy") // FIXME
|
||
}
|
||
.font(.caption2)
|
||
.foregroundColor(.themeTextMessageMain)
|
||
}
|
||
.multilineTextAlignment(.center)
|
||
.padding(.horizontal, 24)
|
||
|
||
HStack(spacing: 8) {
|
||
Button("Service") {
|
||
if let url = URL(string: "https://memorywake.com/privacy-policy") {
|
||
UIApplication.shared.open(url)
|
||
}
|
||
}
|
||
.font(.caption2)
|
||
.foregroundColor(.themeTextMessageMain)
|
||
|
||
Text("and")
|
||
.foregroundColor(.themeTextMessage)
|
||
.font(.caption)
|
||
|
||
Button("Privacy Policy") {
|
||
if let url = URL(string: "https://memorywake.com/privacy-policy") {
|
||
UIApplication.shared.open(url)
|
||
}
|
||
}
|
||
.font(.caption2)
|
||
.foregroundColor(.themeTextMessageMain)
|
||
}
|
||
.padding(.top, 4)
|
||
}
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
.frame(maxWidth: .infinity)
|
||
.padding(.horizontal, 24)
|
||
.padding(.vertical, 12)
|
||
}
|
||
|
||
private func loadingView() -> some View {
|
||
ZStack {
|
||
Color.black.opacity(0.4)
|
||
.edgesIgnoringSafeArea(.all)
|
||
|
||
ProgressView()
|
||
.scaleEffect(1.5)
|
||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||
}
|
||
}
|
||
|
||
// MARK: - Authentication
|
||
|
||
private func handleAppleSignIn(result: Result<ASAuthorization, Error>) {
|
||
print(" 开始处理登录结果...")
|
||
switch result {
|
||
case .success(let authResults):
|
||
print(" 登录授权成功")
|
||
processAppleIDCredential(authResults.credential)
|
||
case .failure(let error):
|
||
print(" 登录失败: \(error.localizedDescription)")
|
||
handleSignInError(error)
|
||
}
|
||
}
|
||
|
||
private func processAppleIDCredential(_ credential: ASAuthorizationCredential) {
|
||
print(" 开始处理 Apple ID 凭证")
|
||
guard let appleIDCredential = credential as? ASAuthorizationAppleIDCredential else {
|
||
print(" 凭证类型不匹配")
|
||
print(" 实际凭证类型: \(type(of: credential))")
|
||
showError(message: "无法处理Apple ID凭证")
|
||
return
|
||
}
|
||
|
||
let userId = appleIDCredential.user
|
||
let email = appleIDCredential.email ?? ""
|
||
let fullName = [
|
||
appleIDCredential.fullName?.givenName,
|
||
appleIDCredential.fullName?.familyName
|
||
]
|
||
.compactMap { $0 }
|
||
.joined(separator: " ")
|
||
|
||
print(" 用户数据 - ID: \(userId), 邮箱: \(email.isEmpty ? "未提供" : email), 姓名: \(fullName.isEmpty ? "未提供" : fullName)")
|
||
|
||
guard let identityTokenData = appleIDCredential.identityToken,
|
||
let identityToken = String(data: identityTokenData, encoding: .utf8) else {
|
||
print(" 无法获取身份令牌")
|
||
showError(message: "无法获取身份令牌")
|
||
return
|
||
}
|
||
|
||
print(" 成功获取 identityToken")
|
||
|
||
var authCode: String? = nil
|
||
if let authCodeData = appleIDCredential.authorizationCode {
|
||
authCode = String(data: authCodeData, encoding: .utf8)
|
||
print(" 获取到授权码")
|
||
} else {
|
||
print(" 未获取到授权码(可选)")
|
||
}
|
||
|
||
print(" 准备调用后端认证接口...")
|
||
authenticateWithBackend(
|
||
identityToken: identityToken,
|
||
authCode: authCode
|
||
)
|
||
}
|
||
|
||
// MARK: - Network
|
||
|
||
private func authenticateWithBackend(
|
||
identityToken: String,
|
||
authCode: String?
|
||
) {
|
||
print(" 开始后端认证流程...")
|
||
isLoading = true
|
||
|
||
var parameters: [String: Any] = [
|
||
"token": identityToken,
|
||
"provider": "Apple",
|
||
]
|
||
|
||
if let authCode = authCode {
|
||
parameters["authorization_code"] = authCode
|
||
print(" 添加授权码到请求参数")
|
||
}
|
||
|
||
print(" 发送认证请求到服务器...")
|
||
print(" 接口: /iam/login/oauth")
|
||
print(" 请求参数: \(parameters.keys)")
|
||
|
||
NetworkService.shared.post(
|
||
path: "/iam/login/oauth",
|
||
parameters: parameters
|
||
) { (result: Result<AuthResponse, NetworkError>) -> Void in
|
||
let handleResult: () -> Void = {
|
||
self.isLoading = false
|
||
|
||
switch result {
|
||
case .success(let authResponse):
|
||
print("✅ [15] 后端认证成功")
|
||
|
||
if let loginInfo = authResponse.data?.userLoginInfo {
|
||
print("🔑 [16] 保存认证信息")
|
||
print(" - 用户ID: \(loginInfo.userId)")
|
||
print(" - 昵称: \(loginInfo.nickname)")
|
||
|
||
KeychainHelper.saveAccessToken(loginInfo.accessToken)
|
||
KeychainHelper.saveRefreshToken(loginInfo.refreshToken)
|
||
|
||
print("🔄 [17] 准备跳转到用户信息页面...")
|
||
print("🔍 isLoggedIn 当前值: \(self.isLoggedIn)")
|
||
|
||
self.isLoggedIn = true
|
||
|
||
print("✅ [18] isLoggedIn 已设置为 true")
|
||
print("🎉 登录流程完成,即将跳转")
|
||
} else {
|
||
print("⚠️ [16] 认证成功但返回的用户信息不完整")
|
||
self.errorMessage = "登录信息不完整,请重试"
|
||
self.showError = true
|
||
}
|
||
// 跳转到userinfo
|
||
Router.shared.navigate(to: .userInfo(createFirstBlindBox: true))
|
||
|
||
case .failure(let error):
|
||
print("❌ [15] 后端认证失败")
|
||
print("⚠️ 错误类型: \(type(of: error))")
|
||
print("📝 错误信息: \(error.localizedDescription)")
|
||
|
||
var errorMessage = "登录失败,请重试"
|
||
|
||
switch error {
|
||
case .invalidURL:
|
||
print(" → 无效的URL")
|
||
errorMessage = "服务器地址无效"
|
||
case .noData:
|
||
print(" → 服务器未返回数据")
|
||
errorMessage = "服务器未响应,请检查网络"
|
||
case .decodingError(let error):
|
||
print(" → 数据解析失败: \(error.localizedDescription)")
|
||
errorMessage = "服务器响应格式错误"
|
||
case .serverError(let message):
|
||
print(" → 服务器错误: \(message)")
|
||
errorMessage = "服务器错误: \(message)"
|
||
case .unauthorized:
|
||
print(" → 认证失败: 未授权")
|
||
errorMessage = "登录信息已过期,请重新登录"
|
||
case .networkError(let error):
|
||
print(" → 网络错误: \(error.localizedDescription)")
|
||
errorMessage = "网络连接失败,请检查网络"
|
||
case .other(let error):
|
||
print(" → 其他错误: \(error.localizedDescription)")
|
||
errorMessage = "发生未知错误"
|
||
case .unknownError(let error):
|
||
print(" → 未知错误: \(error.localizedDescription)")
|
||
errorMessage = "发生未知错误"
|
||
case .invalidParameters:
|
||
print(" → 无效的参数")
|
||
errorMessage = "请求参数错误,请重试"
|
||
}
|
||
|
||
self.errorMessage = errorMessage
|
||
self.showError = true
|
||
print("❌ 登录失败: \(errorMessage)")
|
||
}
|
||
}
|
||
|
||
// Execute on main thread
|
||
if Thread.isMainThread {
|
||
handleResult()
|
||
} else {
|
||
DispatchQueue.main.async(execute: handleResult)
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: - Helpers
|
||
|
||
private func handleSuccessfulAuthentication() {
|
||
print(" 登录成功,准备跳转到用户信息页面...")
|
||
print(" isLoggedIn before update: \(isLoggedIn)")
|
||
DispatchQueue.main.async {
|
||
print(" Setting isLoggedIn to true")
|
||
self.isLoggedIn = true
|
||
print(" isLoggedIn after update: \(self.isLoggedIn)")
|
||
}
|
||
}
|
||
|
||
private func handleSignInError(_ error: Error) {
|
||
let errorMessage = (error as NSError).localizedDescription
|
||
print(" 登录错误: \(errorMessage)")
|
||
showError(message: "登录失败: \(error.localizedDescription)")
|
||
}
|
||
|
||
private func handleAuthenticationError(_ error: Error) {
|
||
let errorMessage = error.localizedDescription
|
||
print(" 认证错误: \(errorMessage)")
|
||
DispatchQueue.main.async {
|
||
self.isLoggedIn = false
|
||
self.showError(message: "登录失败: \(errorMessage)")
|
||
}
|
||
}
|
||
|
||
private func showError(message: String) {
|
||
DispatchQueue.main.async {
|
||
self.errorMessage = message
|
||
self.showError = true
|
||
}
|
||
}
|
||
|
||
private func openURL(_ string: String) {
|
||
guard let url = URL(string: string) else { return }
|
||
UIApplication.shared.open(url)
|
||
}
|
||
|
||
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()
|
||
}
|
||
}
|
||
|
||
// MARK: - Extensions
|
||
|
||
extension String {
|
||
static func randomURLSafeString(length: Int) -> String {
|
||
let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"
|
||
var randomString = ""
|
||
|
||
for _ in 0..<length {
|
||
let randomIndex = Int.random(in: 0..<characters.count)
|
||
let character = characters[characters.index(characters.startIndex, offsetBy: randomIndex)]
|
||
randomString.append(character)
|
||
}
|
||
|
||
return randomString
|
||
}
|
||
}
|
||
|
||
// MARK: - Preview
|
||
|
||
struct LoginView_Previews: PreviewProvider {
|
||
static var previews: some View {
|
||
LoginView()
|
||
}
|
||
} |