feat: 苹果登录

This commit is contained in:
jinyaqiu 2025-08-18 16:36:45 +08:00
parent cd9af65019
commit fc5735964f
5 changed files with 285 additions and 148 deletions

View File

@ -291,13 +291,15 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = wake/wake.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GB3VPJ54BD; DEVELOPMENT_TEAM = 392N3QB7XR;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = wake/Info.plist; INFOPLIST_FILE = wake/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_NSUserTrackingUsageDescription = "我们需要访问您的Apple ID信息来为您提供登录服务";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -307,7 +309,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.wake; PRODUCT_BUNDLE_IDENTIFIER = com.memowake.fairclip2025;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@ -320,13 +322,15 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = wake/wake.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1; CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = GB3VPJ54BD; DEVELOPMENT_TEAM = 392N3QB7XR;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = wake/Info.plist; INFOPLIST_FILE = wake/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_NSUserTrackingUsageDescription = "我们需要访问您的Apple ID信息来为您提供登录服务";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@ -336,7 +340,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.memowake.wake; PRODUCT_BUNDLE_IDENTIFIER = com.memowake.fairclip2025;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;

View File

@ -2,13 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>UIAppFonts</key> <key>ASAuthorizationAppleIDProvider</key>
<array> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<string>Quicksand x.ttf</string>
<string>SankeiCutePopanime.ttf</string>
</array>
<!-- Apple Sign In Configuration -->
<key>CFBundleURLTypes</key> <key>CFBundleURLTypes</key>
<array> <array>
<dict> <dict>
@ -18,14 +13,17 @@
</array> </array>
</dict> </dict>
</array> </array>
<key>ASAuthorizationAppleIDProvider</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>appleid</string> <string>appleid</string>
<string>appleauth</string> <string>appleauth</string>
</array> </array>
<key>NSAppleIDUsageDescription</key>
<string>Sign in with Apple is used to authenticate your account</string>
<key>UIAppFonts</key>
<array>
<string>Quicksand x.ttf</string>
<string>SankeiCutePopanime.ttf</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -1,122 +1,121 @@
import SwiftUI import SwiftUI
import AuthenticationServices import AuthenticationServices
import Alamofire import Alamofire
import CryptoKit
struct Post: Codable { /// Main login view that handles Apple Sign In
let id: Int
let title: String
let body: String
let userId: Int
}
struct Login: Encodable {
let account: String
let password: String
}
struct LoginView: View { struct LoginView: View {
// MARK: - Properties
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
@State private var showModal = false
@State private var isLoading = false @State private var isLoading = false
@State private var showError = false
@State private var errorMessage = ""
@State private var currentNonce: String?
// MARK: - Body
var body: some View { var body: some View {
NavigationView {
ZStack { ZStack {
VStack { // Background
// App logo or title Color(.systemBackground)
VStack { .edgesIgnoringSafeArea(.all)
// Main content
VStack(spacing: 24) {
Spacer()
appHeaderView()
signInButton()
Spacer()
termsAndPrivacyView()
}
.padding()
.alert(isPresented: $showError) {
Alert(
title: Text("Error"),
message: Text(errorMessage),
dismissButton: .default(Text("OK"))
)
}
// Loading indicator
if isLoading {
loadingView()
}
}
.navigationBarHidden(true)
}
// MARK: - View Components
/// App header with icon and welcome text
private func appHeaderView() -> some View {
VStack(spacing: 16) {
Image(systemName: "person.circle.fill") Image(systemName: "person.circle.fill")
.resizable() .resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 80, height: 80) .frame(width: 80, height: 80)
.foregroundColor(.blue) .foregroundColor(.blue)
.padding(.bottom, 20)
Text("Welcome") Text("Welcome to Wake")
.font(.largeTitle) .font(.largeTitle)
.fontWeight(.bold) .fontWeight(.bold)
Text("Sign in to continue")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.bottom, 40) .padding(.bottom, 40)
} }
// Apple Sign In Button /// Apple Sign In button
private func signInButton() -> some View {
SignInWithAppleButton( SignInWithAppleButton(
onRequest: { request in onRequest: { request in
// Generate nonce for security
let nonce = String.randomURLSafeString(length: 32)
self.currentNonce = nonce
// Configure the request
request.requestedScopes = [.fullName, .email] request.requestedScopes = [.fullName, .email]
request.nonce = self.sha256(nonce)
}, },
onCompletion: { result in onCompletion: handleAppleSignIn
switch result {
case .success(let authResults):
print("Authorization successful: \(authResults)")
// Handle successful authentication
// You can get user details from authResults.credential
if let appleIDCredential = authResults.credential as? ASAuthorizationAppleIDCredential {
let userId = appleIDCredential.user
let email = appleIDCredential.email
let fullName = appleIDCredential.fullName
print("User ID: \(userId)")
print("Email: \(email ?? "No email")")
print("Name: \(fullName?.givenName ?? "") \(fullName?.familyName ?? "")")
// TODO: Send this information to your backend
// For example:
// loginWithApple(userId: userId, email: email, name: "\(fullName?.givenName ?? "") \(fullName?.familyName ?? "")")
// Dismiss the login view after successful login
DispatchQueue.main.async {
self.dismiss()
}
}
case .failure(let error):
print("Authorization failed: \(error.localizedDescription)")
// Handle error
}
}
) )
.signInWithAppleButtonStyle(.black) // or .white, .whiteOutline .signInWithAppleButtonStyle(.black)
.frame(height: 50) .frame(height: 50)
.padding(.horizontal, 40) .padding(.horizontal, 40)
.padding(.bottom, 20) .cornerRadius(10)
}
// Terms and Privacy Policy /// Terms and Privacy policy links
private func termsAndPrivacyView() -> some View {
VStack(spacing: 8) { VStack(spacing: 8) {
Text("By continuing, you agree to our") Text("By continuing, you agree to our")
.font(.caption) .font(.caption)
.foregroundColor(.gray) .foregroundColor(.secondary)
HStack(spacing: 16) { HStack(spacing: 16) {
Button("Terms of Service") { Button("Terms of Service") {
// Open terms URL openURL("https://yourwebsite.com/terms")
if let url = URL(string: "https://yourwebsite.com/terms") {
UIApplication.shared.open(url)
}
} }
.font(.caption) .font(.caption)
.foregroundColor(.blue) .foregroundColor(.blue)
Text("") Text("")
.foregroundColor(.gray) .foregroundColor(.secondary)
Button("Privacy Policy") { Button("Privacy Policy") {
// Open privacy policy URL openURL("https://yourwebsite.com/privacy")
if let url = URL(string: "https://yourwebsite.com/privacy") {
UIApplication.shared.open(url)
}
} }
.font(.caption) .font(.caption)
.foregroundColor(.blue) .foregroundColor(.blue)
} }
} }
.padding(.bottom, 40) .padding(.bottom, 24)
Spacer()
} }
.padding()
.navigationBarHidden(true)
.navigationBarBackButtonHidden(true)
// Loading indicator /// Loading overlay view
if isLoading { private func loadingView() -> some View {
return ZStack {
Color.black.opacity(0.4) Color.black.opacity(0.4)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
@ -125,41 +124,167 @@ struct LoginView: View {
.progressViewStyle(CircularProgressViewStyle(tint: .white)) .progressViewStyle(CircularProgressViewStyle(tint: .white))
} }
} }
// MARK: - Apple Sign In Handlers
/// Handles the result of Apple Sign In
private func handleAppleSignIn(result: Result<ASAuthorization, Error>) {
switch result {
case .success(let authResults):
processAppleIDCredential(authResults.credential)
case .failure(let error):
handleSignInError(error)
} }
} }
// Example function to handle login with your backend /// Processes the Apple ID credential
private func loginWithApple(userId: String, email: String?, name: String) { private func processAppleIDCredential(_ credential: ASAuthorizationCredential) {
guard let appleIDCredential = credential as? ASAuthorizationAppleIDCredential else {
showError(message: "Unable to process Apple ID credentials")
return
}
// Get user data
let userId = appleIDCredential.user
let email = appleIDCredential.email ?? ""
let fullName = [
appleIDCredential.fullName?.givenName,
appleIDCredential.fullName?.familyName
]
.compactMap { $0 }
.joined(separator: " ")
// Get the identity token
guard let identityTokenData = appleIDCredential.identityToken,
let identityToken = String(data: identityTokenData, encoding: .utf8) else {
showError(message: "Unable to fetch identity token")
return
}
// Get the authorization code (optional)
var authCode: String? = nil
if let authCodeData = appleIDCredential.authorizationCode {
authCode = String(data: authCodeData, encoding: .utf8)
}
// Proceed with backend authentication
authenticateWithBackend(
userId: userId,
email: email,
name: fullName,
identityToken: identityToken,
authCode: authCode
)
}
// MARK: - Network Operations
/// Authenticates the user with the backend server
private func authenticateWithBackend(
userId: String,
email: String,
name: String,
identityToken: String,
authCode: String?
) {
isLoading = true isLoading = true
// Replace with your actual API endpoint let url = "https://your-api-endpoint.com/api/auth/apple"
let url = "https://your-api-endpoint.com/auth/apple" var parameters: [String: Any] = [
let parameters: [String: Any] = [
"appleUserId": userId, "appleUserId": userId,
"email": email ?? "", "email": email,
"name": name, "name": name,
// Add any other required parameters "identityToken": identityToken
] ]
// Add authorization code if available
if let authCode = authCode {
parameters["authorizationCode"] = authCode
}
AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default) AF.request(url, method: .post, parameters: parameters, encoding: JSONEncoding.default)
.validate() .validate()
.responseJSON { response in .responseJSON { response in
isLoading = false self.isLoading = false
switch response.result { switch response.result {
case .success(let value): case .success(let value):
print("Login successful: \(value)") print("Authentication successful: \(value)")
// Handle successful login (e.g., save auth token, navigate to home screen) self.handleSuccessfulAuthentication()
dismiss()
case .failure(let error): case .failure(let error):
print("Login failed: \(error.localizedDescription)") self.handleAuthenticationError(error)
// Show error message to user
} }
} }
} }
// MARK: - Helper Methods
/// Handles successful authentication
private func handleSuccessfulAuthentication() {
DispatchQueue.main.async {
self.dismiss()
}
}
/// Handles sign in errors
private func handleSignInError(_ error: Error) {
let errorMessage = (error as NSError).localizedDescription
print("Apple Sign In failed: \(errorMessage)")
showError(message: "Sign in failed: \(error.localizedDescription)")
}
/// Handles authentication errors
private func handleAuthenticationError(_ error: AFError) {
let errorMessage = error.localizedDescription
print("API Error: \(errorMessage)")
showError(message: "Failed to sign in: \(errorMessage)")
}
/// Shows error message to the user
private func showError(message: String) {
DispatchQueue.main.async {
self.errorMessage = message
self.showError = true
}
}
/// Opens a URL in Safari
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)
let hashString = hashedData.compactMap { String(format: "%02x", $0) }.joined()
return hashString
}
} }
#Preview { // MARK: - String Extension for Random String Generation
LoginView()
extension String {
/// Generates a random string of the specified length using URL-safe characters
/// - Parameter length: The length of the random string to generate
/// - Returns: A random string containing URL-safe characters
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()
}
} }

10
wake/wake.entitlements Normal file
View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
</dict>
</plist>