wake-ios/wake/View/Login/Login.swift
2025-08-19 20:34:25 +08:00

443 lines
17 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
/// -
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 {
NavigationStack {
ZStack {
// Background
Color(red: 1.0, green: 0.67, blue: 0.15)
.edgesIgnoringSafeArea(.all)
VStack(alignment: .leading, spacing: 4) {
Text("Hi, I'm MeMo!")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 24)
.padding(.top, 44)
Text("Welcome~")
.font(.largeTitle)
.fontWeight(.semibold)
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 24)
.padding(.bottom, 20)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
VStack(spacing: 16) {
Spacer()
signInButton()
termsAndPrivacyView()
}
.padding()
.alert(isPresented: $showError) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("OK"))
)
}
if isLoading {
loadingView()
}
}
.navigationBarHidden(true)
.fullScreenCover(isPresented: $isLoggedIn) {
NavigationStack {
UserInfo()
}
}
}
}
// MARK: - Views
private func signInButton() -> some View {
SignInWithAppleButton(
onRequest: { request in
let nonce = String.randomURLSafeString(length: 32)
self.currentNonce = nonce
request.requestedScopes = [.fullName, .email]
request.nonce = self.sha256(nonce)
},
onCompletion: handleAppleSignIn
)
.signInWithAppleButtonStyle(.white)
.frame(height: 50)
.cornerRadius(25)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.black, lineWidth: 1)
)
}
private func termsAndPrivacyView() -> some View {
VStack(spacing: 4) {
HStack {
Text("By continuing, you agree to our")
.font(.caption)
.foregroundColor(.secondary)
Button("Terms of") {
openURL("https://yourwebsite.com/terms")
}
.font(.caption2)
.foregroundColor(.blue)
}
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
HStack(spacing: 8) {
Button("Service") {
openURL("https://yourwebsite.com/terms")
}
.font(.caption2)
.foregroundColor(.blue)
Text("and")
.foregroundColor(.secondary)
.font(.caption)
Button("Privacy Policy") {
openURL("https://yourwebsite.com/privacy")
}
.font(.caption2)
.foregroundColor(.blue)
}
.padding(.top, 4)
}
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity)
.padding(.horizontal, 24)
.padding(.bottom, 24)
}
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("🔵 [Apple Sign In] 开始处理登录结果...")
DispatchQueue.main.async {
self.isLoggedIn = true
}
switch result {
case .success(let authResults):
print("✅ [Apple Sign In] 登录授权成功")
processAppleIDCredential(authResults.credential)
case .failure(let error):
print("❌ [Apple Sign In] 登录失败: \(error.localizedDescription)")
handleSignInError(error)
}
}
private func processAppleIDCredential(_ credential: ASAuthorizationCredential) {
print("🔵 [Apple ID] 开始处理凭证...")
guard let appleIDCredential = credential as? ASAuthorizationAppleIDCredential else {
print("❌ [Apple ID] 凭证类型不匹配")
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(" [Apple ID] 用户数据 - ID: \(userId), 邮箱: \(email.isEmpty ? "未提供" : email), 姓名: \(fullName.isEmpty ? "未提供" : fullName)")
guard let identityTokenData = appleIDCredential.identityToken,
let identityToken = String(data: identityTokenData, encoding: .utf8) else {
print("❌ [Apple ID] 无法获取身份令牌")
showError(message: "无法获取身份令牌")
return
}
var authCode: String? = nil
if let authCodeData = appleIDCredential.authorizationCode {
authCode = String(data: authCodeData, encoding: .utf8)
print(" [Apple ID] 获取到授权码")
} else {
print(" [Apple ID] 未获取到授权码(可选)")
}
print("🔵 [Apple ID] 准备调用后端认证...")
authenticateWithBackend(
userId: appleIDCredential.user,
email: appleIDCredential.email ?? "",
name: [appleIDCredential.fullName?.givenName,
appleIDCredential.fullName?.familyName]
.compactMap { $0 }
.joined(separator: " "),
identityToken: identityToken,
authCode: authCode
)
}
// MARK: - Network
private func authenticateWithBackend(
userId: String,
email: String,
name: String,
identityToken: String,
authCode: String?
) {
isLoading = true
print("🔵 [Backend] 开始后端认证...")
let endpoint = "\(APIConfig.baseURL)/iam/login/oauth"
guard let url = URL(string: endpoint) else {
print("❌ [Backend] 无效的URL: \(endpoint)")
self.handleAuthenticationError(NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的URL"]))
return
}
var parameters: [String: Any] = [
"provider": "Apple",
"token": identityToken,
"userId": userId,
"email": email,
"name": name,
]
if let authCode = authCode {
parameters["authorization_code"] = authCode
}
print("📤 [Backend] 请求URL: \(endpoint)")
print("📤 [Backend] 请求参数: \(parameters)")
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
do {
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
} catch {
print("❌ [Backend] 参数序列化失败: \(error.localizedDescription)")
self.handleAuthenticationError(error)
return
}
let task = URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
self.isLoading = false
// 1.
if let error = error {
print("❌ [Backend] 请求失败: \(error.localizedDescription)")
self.handleAuthenticationError(error)
return
}
// 2.
guard let httpResponse = response as? HTTPURLResponse else {
let error = NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "无效的服务器响应"])
print("❌ [Backend] \(error.localizedDescription)")
self.handleAuthenticationError(error)
return
}
// 3.
let statusCode = httpResponse.statusCode
print("""
📥 [Backend] 响应信息:
- 状态码: \(statusCode)
- URL: \(httpResponse.url?.absoluteString ?? "N/A")
- Headers: \(httpResponse.allHeaderFields)
""")
// 4.
if let data = data {
if let jsonString = String(data: data, encoding: .utf8) {
print("📦 [Backend] 响应内容: \(jsonString)")
}
// 5.
do {
// JSON
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] {
print("✅ [Backend] 响应解析成功")
print("📦 [Backend] 响应内容: \(json)")
//
if let code = json["code"] as? Int, code != 0 {
let errorMsg = json["message"] as? String ?? "未知错误"
print("❌ [Backend] 请求失败: \(errorMsg)")
self.showError(message: errorMsg)
return
}
// data
guard let responseData = json["data"] as? [String: Any] else {
print("⚠️ [Backend] 未找到 data 字段")
self.showError(message: "服务器返回数据格式错误")
return
}
// user_login_info
if let userLoginInfo = responseData["user_login_info"] as? [String: Any] {
print("👤 [Backend] 用户登录信息: \(userLoginInfo)")
// Keychain
if let accessToken = userLoginInfo["access_token"] as? String {
_ = KeychainHelper.saveAccessToken(accessToken)
print("🔑 [Keychain] 访问令牌已保存")
// UserDefaults
var userInfo: [String: Any] = [
"user_id": userLoginInfo["user_id"] as? Int64 ?? 0,
"account": userLoginInfo["account"] as? String ?? "",
"nickname": userLoginInfo["nickname"] as? String ?? "",
"avatar": userLoginInfo["avatar_file_url"] as? String ?? ""
]
UserDefaults.standard.set(userInfo, forKey: "currentUserInfo")
print("👤 [UserDefaults] 用户信息已保存")
}
if let refreshToken = userLoginInfo["refresh_token"] as? String {
_ = KeychainHelper.saveRefreshToken(refreshToken)
print("🔄 [Keychain] 刷新令牌已保存")
}
//
DispatchQueue.main.async {
self.handleSuccessfulAuthentication()
}
return
} else {
print("⚠️ [Backend] 未找到 user_login_info 字段")
self.showError(message: "登录信息不完整")
}
}
} catch {
print("⚠️ [Backend] 响应解析失败: \(error.localizedDescription)")
self.showError(message: "数据解析失败")
}
}
// 6. return
if statusCode < 200 || statusCode >= 300 {
let errorMessage: String
switch statusCode {
case 400:
errorMessage = "请求参数错误"
case 401:
errorMessage = "认证失败,请重新登录"
case 403:
errorMessage = "权限不足"
case 404:
errorMessage = "请求的接口不存在"
case 500...599:
errorMessage = "服务器内部错误,请稍后重试"
default:
errorMessage = "未知错误 (状态码: \(statusCode))"
}
self.showError(message: errorMessage)
}
}
}
task.resume()
}
// MARK: - Helpers
private func handleSuccessfulAuthentication() {
print("✅ [Auth] 登录成功,准备跳转到用户信息页面...")
DispatchQueue.main.async {
self.isLoggedIn = true
}
}
private func handleSignInError(_ error: Error) {
let errorMessage = (error as NSError).localizedDescription
print("❌ [Auth] 登录错误: \(errorMessage)")
showError(message: "登录失败: \(error.localizedDescription)")
}
private func handleAuthenticationError(_ error: Error) {
let errorMessage = error.localizedDescription
print("❌ [Auth] 认证错误: \(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()
}
}