wake-ios/wake/View/Login/Login.swift

390 lines
14 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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