feat: 登录

This commit is contained in:
jinyaqiu 2025-08-19 11:16:07 +08:00
parent cc305aa84a
commit 4f92f52bb7
2 changed files with 62 additions and 114 deletions

View File

@ -1,23 +1,26 @@
import SwiftUI import SwiftUI
import AuthenticationServices // import AuthenticationServices
import Alamofire // import Alamofire
import CryptoKit // import CryptoKit
/// - /// -
struct LoginView: View { struct LoginView: View {
// MARK: - // MARK: - Properties
@Environment(\.dismiss) private var dismiss //
@State private var isLoading = false // @Environment(\.dismiss) private var dismiss
@State private var showError = false // @State private var isLoading = false
@State private var errorMessage = "" // @State private var showError = false
@State private var currentNonce: String? // @State private var errorMessage = ""
@State private var currentNonce: String?
// MARK: - Body
// MARK: -
var body: some View { var body: some View {
ZStack { ZStack {
// // Background
Color(red: 1.0, green: 0.67, blue: 0.15) // FFAA26 in RGB (1.0, 0.67, 0.15) Color(red: 1.0, green: 0.67, blue: 0.15)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text("Hi, I'm MeMo!") Text("Hi, I'm MeMo!")
.font(.largeTitle) .font(.largeTitle)
@ -25,8 +28,8 @@ struct LoginView: View {
.foregroundColor(.black) .foregroundColor(.black)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 24) .padding(.leading, 24)
.padding(.top, 44) // Add some top padding for the status bar .padding(.top, 44)
Text("Welcome~") Text("Welcome~")
.font(.largeTitle) .font(.largeTitle)
.fontWeight(.semibold) .fontWeight(.semibold)
@ -34,15 +37,15 @@ struct LoginView: View {
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
.padding(.leading, 24) .padding(.leading, 24)
.padding(.bottom, 20) .padding(.bottom, 20)
Spacer() Spacer()
} }
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
VStack(spacing: 16) { VStack(spacing: 16) {
// appHeaderView() Spacer()
Spacer() // signInButton()
signInButton() // termsAndPrivacyView()
termsAndPrivacyView() //
} }
.padding() .padding()
.alert(isPresented: $showError) { .alert(isPresented: $showError) {
@ -53,7 +56,6 @@ struct LoginView: View {
) )
} }
//
if isLoading { if isLoading {
loadingView() loadingView()
} }
@ -61,80 +63,45 @@ struct LoginView: View {
.navigationBarHidden(true) .navigationBarHidden(true)
} }
// MARK: - // MARK: - Views
///
private func appHeaderView() -> some View {
VStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 80)
.foregroundColor(.blue)
Text("Welcome to Wake")
.font(.largeTitle)
.fontWeight(.bold)
Text("Sign in to continue")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.bottom, 40)
}
///
private func signInButton() -> some View { private func signInButton() -> some View {
Button(action: { SignInWithAppleButton(
// This will be handled by the SignInWithAppleButton onRequest: { request in
}) { let nonce = String.randomURLSafeString(length: 32)
ZStack { self.currentNonce = nonce
// Invisible SignInWithAppleButton for functionality request.requestedScopes = [.fullName, .email]
SignInWithAppleButton( request.nonce = self.sha256(nonce)
onRequest: { request in },
let nonce = String.randomURLSafeString(length: 32) onCompletion: handleAppleSignIn
self.currentNonce = nonce )
request.requestedScopes = [.fullName, .email] .signInWithAppleButtonStyle(.white)
request.nonce = self.sha256(nonce)
},
onCompletion: handleAppleSignIn
)
.frame(width: 0, height: 0)
.opacity(0)
// Custom button appearance
HStack {
Image(systemName: "applelogo")
Text("Continue with Apple")
// .font(.headline)
}
.frame(maxWidth: .infinity)
.padding(.vertical, 12)
.padding(.horizontal, 24)
.foregroundColor(.black)
.background(Color.white)
.cornerRadius(25)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.black, lineWidth: 1)
)
}
}
.frame(height: 50) .frame(height: 50)
.padding(.horizontal, 40) .cornerRadius(25)
.overlay(
RoundedRectangle(cornerRadius: 25)
.stroke(Color.black, lineWidth: 1)
)
} }
///
private func termsAndPrivacyView() -> some View { private func termsAndPrivacyView() -> some View {
VStack(spacing: 4) { VStack(spacing: 4) {
Text("By continuing, you agree to our") HStack {
.font(.caption) Text("By continuing, you agree to our")
.foregroundColor(.secondary) .font(.caption)
.multilineTextAlignment(.center) .foregroundColor(.secondary)
.padding(.horizontal, 24)
Button("Terms of") {
openURL("https://yourwebsite.com/terms")
}
.font(.caption2)
.foregroundColor(.blue)
}
.multilineTextAlignment(.center)
.padding(.horizontal, 24)
HStack(spacing: 16) { HStack(spacing: 8) {
Button("Terms of Service") { Button("Service") {
openURL("https://yourwebsite.com/terms") openURL("https://yourwebsite.com/terms")
} }
.font(.caption2) .font(.caption2)
@ -143,7 +110,6 @@ struct LoginView: View {
Text("and") Text("and")
.foregroundColor(.secondary) .foregroundColor(.secondary)
.font(.caption) .font(.caption)
.multilineTextAlignment(.center)
Button("Privacy Policy") { Button("Privacy Policy") {
openURL("https://yourwebsite.com/privacy") openURL("https://yourwebsite.com/privacy")
@ -153,16 +119,14 @@ struct LoginView: View {
} }
.padding(.top, 4) .padding(.top, 4)
} }
.lineLimit(2) .fixedSize(horizontal: false, vertical: true)
.fixedSize(horizontal: false, vertical: true)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.horizontal, 24) .padding(.horizontal, 24)
.padding(.bottom, 24) .padding(.bottom, 24)
} }
///
private func loadingView() -> some View { private func loadingView() -> some View {
return ZStack { ZStack {
Color.black.opacity(0.4) Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
@ -172,9 +136,8 @@ struct LoginView: View {
} }
} }
// MARK: - // MARK: - Authentication
///
private func handleAppleSignIn(result: Result<ASAuthorization, Error>) { private func handleAppleSignIn(result: Result<ASAuthorization, Error>) {
print("🔵 [Apple Sign In] 开始处理登录结果...") print("🔵 [Apple Sign In] 开始处理登录结果...")
switch result { switch result {
@ -187,7 +150,6 @@ struct LoginView: View {
} }
} }
/// ID
private func processAppleIDCredential(_ credential: ASAuthorizationCredential) { private func processAppleIDCredential(_ credential: ASAuthorizationCredential) {
print("🔵 [Apple ID] 开始处理凭证...") print("🔵 [Apple ID] 开始处理凭证...")
guard let appleIDCredential = credential as? ASAuthorizationAppleIDCredential else { guard let appleIDCredential = credential as? ASAuthorizationAppleIDCredential else {
@ -196,7 +158,6 @@ struct LoginView: View {
return return
} }
//
let userId = appleIDCredential.user let userId = appleIDCredential.user
let email = appleIDCredential.email ?? "" let email = appleIDCredential.email ?? ""
let fullName = [ let fullName = [
@ -208,7 +169,6 @@ struct LoginView: View {
print(" [Apple ID] 用户数据 - ID: \(userId), 邮箱: \(email.isEmpty ? "未提供" : email), 姓名: \(fullName.isEmpty ? "未提供" : fullName)") print(" [Apple ID] 用户数据 - ID: \(userId), 邮箱: \(email.isEmpty ? "未提供" : email), 姓名: \(fullName.isEmpty ? "未提供" : fullName)")
//
guard let identityTokenData = appleIDCredential.identityToken, guard let identityTokenData = appleIDCredential.identityToken,
let identityToken = String(data: identityTokenData, encoding: .utf8) else { let identityToken = String(data: identityTokenData, encoding: .utf8) else {
print("❌ [Apple ID] 无法获取身份令牌") print("❌ [Apple ID] 无法获取身份令牌")
@ -216,7 +176,6 @@ struct LoginView: View {
return return
} }
//
var authCode: String? = nil var authCode: String? = nil
if let authCodeData = appleIDCredential.authorizationCode { if let authCodeData = appleIDCredential.authorizationCode {
authCode = String(data: authCodeData, encoding: .utf8) authCode = String(data: authCodeData, encoding: .utf8)
@ -235,9 +194,8 @@ struct LoginView: View {
) )
} }
// MARK: - // MARK: - Network
///
private func authenticateWithBackend( private func authenticateWithBackend(
userId: String, userId: String,
email: String, email: String,
@ -256,7 +214,6 @@ struct LoginView: View {
"identityToken": identityToken "identityToken": identityToken
] ]
//
if let authCode = authCode { if let authCode = authCode {
parameters["authorizationCode"] = authCode parameters["authorizationCode"] = authCode
} }
@ -282,9 +239,8 @@ struct LoginView: View {
} }
} }
// MARK: - // MARK: - Helpers
///
private func handleSuccessfulAuthentication() { private func handleSuccessfulAuthentication() {
print("✅ [Auth] 登录成功,准备关闭登录页面...") print("✅ [Auth] 登录成功,准备关闭登录页面...")
DispatchQueue.main.async { DispatchQueue.main.async {
@ -292,21 +248,18 @@ struct LoginView: View {
} }
} }
///
private func handleSignInError(_ error: Error) { private func handleSignInError(_ error: Error) {
let errorMessage = (error as NSError).localizedDescription let errorMessage = (error as NSError).localizedDescription
print("❌ [Auth] 登录错误: \(errorMessage)") print("❌ [Auth] 登录错误: \(errorMessage)")
showError(message: "登录失败: \(error.localizedDescription)") showError(message: "登录失败: \(error.localizedDescription)")
} }
///
private func handleAuthenticationError(_ error: AFError) { private func handleAuthenticationError(_ error: AFError) {
let errorMessage = error.localizedDescription let errorMessage = error.localizedDescription
print("❌ [Auth] 认证错误: \(errorMessage)") print("❌ [Auth] 认证错误: \(errorMessage)")
showError(message: "登录失败: \(errorMessage)") showError(message: "登录失败: \(errorMessage)")
} }
///
private func showError(message: String) { private func showError(message: String) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.errorMessage = message self.errorMessage = message
@ -314,27 +267,21 @@ struct LoginView: View {
} }
} }
/// SafariURL
private func openURL(_ string: String) { private func openURL(_ string: String) {
guard let url = URL(string: string) else { return } guard let url = URL(string: string) else { return }
UIApplication.shared.open(url) UIApplication.shared.open(url)
} }
/// SHA256
private func sha256(_ input: String) -> String { private func sha256(_ input: String) -> String {
let inputData = Data(input.utf8) let inputData = Data(input.utf8)
let hashedData = SHA256.hash(data: inputData) let hashedData = SHA256.hash(data: inputData)
let hashString = hashedData.compactMap { String(format: "%02x", $0) }.joined() return hashedData.compactMap { String(format: "%02x", $0) }.joined()
return hashString
} }
} }
// MARK: - // MARK: - Extensions
extension String { extension String {
/// URL
/// - Parameter length:
/// - Returns:
static func randomURLSafeString(length: Int) -> String { static func randomURLSafeString(length: Int) -> String {
let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~" let characters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~"
var randomString = "" var randomString = ""
@ -349,7 +296,8 @@ extension String {
} }
} }
// MARK: - // MARK: - Preview
struct LoginView_Previews: PreviewProvider { struct LoginView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
LoginView() LoginView()