feat: 苹果登录
This commit is contained in:
parent
cd9af65019
commit
fc5735964f
@ -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;
|
||||||
|
|||||||
Binary file not shown.
@ -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>
|
||||||
|
|||||||
@ -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
10
wake/wake.entitlements
Normal 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>
|
||||||
Loading…
x
Reference in New Issue
Block a user