wake-ios/wake/View/Login/Login.swift
2025-08-22 18:58:08 +08:00

313 lines
11 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
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))
.foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 24)
.padding(.top, 44)
Text("Welcome~")
.font(Typography.font(for: .largeTitle))
.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)
.fullScreenCover(isPresented: $isLoggedIn) {
NavigationStack {
UserInfo()
}
}
}
// MARK: - Views
private func signInButton() -> some View {
AppleSignInButton { request in
let nonce = String.randomURLSafeString(length: 32)
self.currentNonce = nonce
request.nonce = self.sha256(nonce)
} onCompletion: { result in
switch result {
case .success(let authResults):
print("✅ [Apple Sign In] 登录授权成功")
if let appleIDCredential = authResults.credential as? ASAuthorizationAppleIDCredential {
self.processAppleIDCredential(appleIDCredential)
}
case .failure(let error):
print("❌ [Apple Sign In] 登录失败: \(error.localizedDescription)")
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://yourwebsite.com/terms")
}
.font(.caption2)
.foregroundColor(.themeTextMessageMain)
}
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
HStack(spacing: 8) {
Button("Service") {
openURL("https://yourwebsite.com/terms")
}
.font(.caption2)
.foregroundColor(.themeTextMessageMain)
Text("and")
.foregroundColor(.themeTextMessage)
.font(.caption)
Button("Privacy Policy") {
openURL("https://yourwebsite.com/privacy")
}
.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("🔵 [Apple Sign In] 开始处理登录结果...")
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(
identityToken: identityToken,
authCode: authCode
)
}
// MARK: - Network
private func authenticateWithBackend(
identityToken: String,
authCode: String?
) {
isLoading = true
print("🔵 [Backend] 开始后端认证...")
var parameters: [String: Any] = [
"token": identityToken,
"provider": "Apple",
]
if let authCode = authCode {
parameters["authorization_code"] = authCode
}
NetworkService.shared.post(
path: "/iam/login/oauth",
parameters: parameters
) { (result: Result<AuthResponse, NetworkError>) in
DispatchQueue.main.async {
switch result {
case .success(let authResponse):
print("✅ [Backend] 认证成功")
// token
if let loginInfo = authResponse.data?.userLoginInfo {
KeychainHelper.saveAccessToken(loginInfo.accessToken)
KeychainHelper.saveRefreshToken(loginInfo.refreshToken)
// userId, nickname
print("👤 用户ID: \(loginInfo.userId)")
print("👤 昵称: \(loginInfo.nickname)")
}
self.isLoggedIn = true
case .failure(let error):
print("❌ [Backend] 认证失败: \(error.localizedDescription)")
self.errorMessage = error.localizedDescription
self.showError = true
self.isLoading = false
}
}
}
}
// 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()
}
}