Add account sign-in structure
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import AppIntents
|
||||
import AuthenticationServices
|
||||
import PhotosUI
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
@@ -188,6 +189,9 @@ enum UserProfileStore {
|
||||
static let onboardingCompletedKey = "hasCompletedOnboarding"
|
||||
static let displayNameKey = "userDisplayName"
|
||||
static let profileImageDataKey = "userProfileImageData"
|
||||
static let accountProviderKey = "accountProvider"
|
||||
static let accountEmailKey = "accountEmail"
|
||||
static let appleUserIDKey = "appleUserID"
|
||||
|
||||
static func saveProfile(displayName: String, imageData: Data?) {
|
||||
UserDefaults.standard.set(displayName.trimmingCharacters(in: .whitespacesAndNewlines), forKey: displayNameKey)
|
||||
@@ -196,12 +200,50 @@ enum UserProfileStore {
|
||||
UserDefaults.standard.set(imageData, forKey: profileImageDataKey)
|
||||
}
|
||||
}
|
||||
|
||||
static func saveAppleAccount(userID: String, email: String?, fullName: PersonNameComponents?) {
|
||||
UserDefaults.standard.set(AccountProvider.apple.rawValue, forKey: accountProviderKey)
|
||||
UserDefaults.standard.set(userID, forKey: appleUserIDKey)
|
||||
|
||||
if let email, !email.isEmpty {
|
||||
UserDefaults.standard.set(email, forKey: accountEmailKey)
|
||||
}
|
||||
|
||||
let formatter = PersonNameComponentsFormatter()
|
||||
let name = fullName.map { formatter.string(from: $0).trimmingCharacters(in: .whitespacesAndNewlines) } ?? ""
|
||||
if !name.isEmpty {
|
||||
UserDefaults.standard.set(name, forKey: displayNameKey)
|
||||
}
|
||||
}
|
||||
|
||||
static func clearAccount() {
|
||||
UserDefaults.standard.removeObject(forKey: accountProviderKey)
|
||||
UserDefaults.standard.removeObject(forKey: accountEmailKey)
|
||||
UserDefaults.standard.removeObject(forKey: appleUserIDKey)
|
||||
}
|
||||
}
|
||||
|
||||
enum AccountProvider: String {
|
||||
case local
|
||||
case apple
|
||||
case google
|
||||
case email
|
||||
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .local: "Lokales Profil"
|
||||
case .apple: "Apple Account"
|
||||
case .google: "Google Account"
|
||||
case .email: "E-Mail und Passwort"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct OnboardingView: View {
|
||||
@Environment(\.appColorProfile) private var colorProfile
|
||||
@AppStorage(UserProfileStore.onboardingCompletedKey) private var hasCompletedOnboarding = false
|
||||
@AppStorage(UserProfileStore.displayNameKey) private var storedDisplayName = ""
|
||||
@AppStorage(UserProfileStore.accountProviderKey) private var accountProviderRaw = AccountProvider.local.rawValue
|
||||
|
||||
@State private var page = 0
|
||||
@State private var displayName = ""
|
||||
@@ -209,6 +251,7 @@ private struct OnboardingView: View {
|
||||
@State private var profileImageData: Data?
|
||||
@State private var customCategories: [DiaryCategory] = []
|
||||
@State private var newCategoryName = ""
|
||||
@State private var accountMessage: String?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -245,6 +288,11 @@ private struct OnboardingView: View {
|
||||
.onChange(of: selectedPhoto) { _, item in
|
||||
Task { await loadSelectedPhoto(item) }
|
||||
}
|
||||
.alert("Account", isPresented: accountMessageBinding) {
|
||||
Button("OK", role: .cancel) { }
|
||||
} message: {
|
||||
Text(accountMessage ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
private var progressDots: some View {
|
||||
@@ -393,15 +441,41 @@ private struct OnboardingView: View {
|
||||
private var accountPage: some View {
|
||||
VStack(spacing: 20) {
|
||||
onboardingHeader(
|
||||
symbol: "icloud.and.arrow.up.fill",
|
||||
title: "Zugang sichern",
|
||||
text: "Zum Abschluss wird dein lokales Profil gespeichert. Premium-Käufe laufen später über deine Apple-ID, und deine Daten können über iCloud synchronisiert werden, sobald die iCloud-Funktion aktiv ist."
|
||||
symbol: "person.crop.circle.badge.checkmark",
|
||||
title: "Account sichern",
|
||||
text: "Melde dich an, damit Premium später über den App Store wiederhergestellt werden kann. Google und E-Mail brauchen noch ein Backend, Apple ist in iOS nativ vorgesehen."
|
||||
)
|
||||
|
||||
VStack(alignment: .leading, spacing: 14) {
|
||||
accountRow(symbol: "person.crop.circle.badge.checkmark", title: "Profil", text: displayName.isEmpty ? "Ohne Namen fortfahren" : displayName)
|
||||
accountRow(symbol: "icloud", title: "Datenzugang", text: "Über deinen Apple-Account und iCloud absichern")
|
||||
accountRow(symbol: "checkmark.seal", title: "Premium", text: "Käufe können über die Apple-ID wiederhergestellt werden")
|
||||
VStack(spacing: 12) {
|
||||
SignInWithAppleButton(.signUp) { request in
|
||||
request.requestedScopes = [.fullName, .email]
|
||||
} onCompletion: { result in
|
||||
handleAppleSignIn(result)
|
||||
}
|
||||
.signInWithAppleButtonStyle(.black)
|
||||
.frame(height: 48)
|
||||
|
||||
Button {
|
||||
accountMessage = "Google Login braucht einen OAuth Client und ein Backend wie Firebase, Supabase oder einen eigenen Server. Sobald du dich für eines davon entscheidest, kann ich die echte Anmeldung anbinden."
|
||||
} label: {
|
||||
Label("Mit Google anmelden", systemImage: "g.circle")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button {
|
||||
accountMessage = "E-Mail und Passwort darf ich nicht lokal faken. Dafür braucht FeelAloud einen Auth-Server, der Passwörter sicher hasht, Reset-Mails verschickt und Sessions verwaltet."
|
||||
} label: {
|
||||
Label("Mit E-Mail registrieren", systemImage: "envelope")
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
accountRow(
|
||||
symbol: accountProviderRaw == AccountProvider.apple.rawValue ? "checkmark.seal.fill" : "info.circle",
|
||||
title: "Aktueller Zugang",
|
||||
text: AccountProvider(rawValue: accountProviderRaw)?.displayName ?? AccountProvider.local.displayName
|
||||
)
|
||||
}
|
||||
.padding(16)
|
||||
.background(.background.opacity(0.72), in: RoundedRectangle(cornerRadius: 18))
|
||||
@@ -415,6 +489,13 @@ private struct OnboardingView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var accountMessageBinding: Binding<Bool> {
|
||||
Binding(
|
||||
get: { accountMessage != nil },
|
||||
set: { if !$0 { accountMessage = nil } }
|
||||
)
|
||||
}
|
||||
|
||||
private var navigationControls: some View {
|
||||
HStack(spacing: 12) {
|
||||
if page > 0 {
|
||||
@@ -547,6 +628,30 @@ private struct OnboardingView: View {
|
||||
hasCompletedOnboarding = true
|
||||
}
|
||||
|
||||
private func handleAppleSignIn(_ result: Result<ASAuthorization, Error>) {
|
||||
switch result {
|
||||
case .success(let authorization):
|
||||
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
|
||||
accountMessage = "Apple hat keine gültigen Accountdaten zurückgegeben."
|
||||
return
|
||||
}
|
||||
|
||||
UserProfileStore.saveAppleAccount(
|
||||
userID: credential.user,
|
||||
email: credential.email,
|
||||
fullName: credential.fullName
|
||||
)
|
||||
accountProviderRaw = AccountProvider.apple.rawValue
|
||||
if displayName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
let formatter = PersonNameComponentsFormatter()
|
||||
displayName = credential.fullName.map { formatter.string(from: $0) } ?? ""
|
||||
}
|
||||
accountMessage = "Apple Account wurde verknüpft."
|
||||
case .failure(let error):
|
||||
accountMessage = "Apple Login konnte nicht abgeschlossen werden: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
|
||||
private func loadSelectedPhoto(_ item: PhotosPickerItem?) async {
|
||||
guard let item,
|
||||
let data = try? await item.loadTransferable(type: Data.self) else {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import AppIntents
|
||||
import AuthenticationServices
|
||||
import FoundationModels
|
||||
import SwiftData
|
||||
import SwiftUI
|
||||
@@ -15,6 +16,8 @@ struct SettingsView: View {
|
||||
@AppStorage(UserProfileStore.onboardingCompletedKey) private var hasCompletedOnboarding = false
|
||||
@AppStorage(UserProfileStore.displayNameKey) private var displayName = ""
|
||||
@AppStorage(UserProfileStore.profileImageDataKey) private var profileImageData = Data()
|
||||
@AppStorage(UserProfileStore.accountProviderKey) private var accountProviderRaw = AccountProvider.local.rawValue
|
||||
@AppStorage(UserProfileStore.accountEmailKey) private var accountEmail = ""
|
||||
@AppStorage("snapshotRemindersEnabled") private var remindersEnabled = false
|
||||
@AppStorage("snapshotReminderCount") private var reminderCount = 3
|
||||
@AppStorage("snapshotReminderTime1") private var reminderTime1 = 10 * 60
|
||||
@@ -198,7 +201,33 @@ struct SettingsView: View {
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
|
||||
LabeledContent("Account", value: "Apple-ID / iCloud")
|
||||
LabeledContent("Account", value: currentAccountProvider.displayName)
|
||||
if !accountEmail.isEmpty {
|
||||
LabeledContent("E-Mail", value: accountEmail)
|
||||
}
|
||||
}
|
||||
|
||||
Section("Anmelden") {
|
||||
SignInWithAppleButton(.signIn) { request in
|
||||
request.requestedScopes = [.fullName, .email]
|
||||
} onCompletion: { result in
|
||||
handleAppleSignIn(result)
|
||||
}
|
||||
.signInWithAppleButtonStyle(.black)
|
||||
.frame(height: 46)
|
||||
.listRowInsets(EdgeInsets(top: 8, leading: 16, bottom: 8, trailing: 16))
|
||||
|
||||
Button {
|
||||
profileMessage = "Google Login braucht einen OAuth Client und ein Backend wie Firebase, Supabase oder einen eigenen Server. Danach kann FeelAloud Google sicher als Account-Anbieter verwenden."
|
||||
} label: {
|
||||
Label("Mit Google anmelden", systemImage: "g.circle")
|
||||
}
|
||||
|
||||
Button {
|
||||
profileMessage = "E-Mail und Passwort brauchen einen Auth-Server. Dort werden Passwörter sicher gehasht, Reset-Mails verschickt und Sessions verwaltet. Lokal in der App wäre das kein echter Account."
|
||||
} label: {
|
||||
Label("Mit E-Mail anmelden", systemImage: "envelope")
|
||||
}
|
||||
}
|
||||
|
||||
Section("Premium") {
|
||||
@@ -226,7 +255,7 @@ struct SettingsView: View {
|
||||
|
||||
Section("Zugang") {
|
||||
Button {
|
||||
profileMessage = "FeelAloud verwendet aktuell kein eigenes Passwort. Der Zugang läuft über dein Gerät, Face ID und später über deine Apple-ID/iCloud. Ein App-Passwort kann erst mit einem echten Login-System ergänzt werden."
|
||||
profileMessage = "Passwort ändern ist nur für E-Mail-Accounts sinnvoll. Dafür braucht FeelAloud zuerst einen echten Auth-Server mit sicherer Passwortspeicherung und Reset-Mail-Funktion."
|
||||
} label: {
|
||||
Label("Passwort ändern", systemImage: "key")
|
||||
}
|
||||
@@ -495,6 +524,10 @@ struct SettingsView: View {
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
private var currentAccountProvider: AccountProvider {
|
||||
AccountProvider(rawValue: accountProviderRaw) ?? .local
|
||||
}
|
||||
|
||||
private var profileAvatar: some View {
|
||||
Group {
|
||||
#if canImport(UIKit)
|
||||
@@ -726,8 +759,42 @@ struct SettingsView: View {
|
||||
private func logoutProfile() {
|
||||
displayName = ""
|
||||
profileImageData = Data()
|
||||
accountEmail = ""
|
||||
accountProviderRaw = AccountProvider.local.rawValue
|
||||
UserProfileStore.clearAccount()
|
||||
hasCompletedOnboarding = false
|
||||
}
|
||||
|
||||
private func handleAppleSignIn(_ result: Result<ASAuthorization, Error>) {
|
||||
switch result {
|
||||
case .success(let authorization):
|
||||
guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
|
||||
profileMessage = "Apple hat keine gültigen Accountdaten zurückgegeben."
|
||||
return
|
||||
}
|
||||
|
||||
UserProfileStore.saveAppleAccount(
|
||||
userID: credential.user,
|
||||
email: credential.email,
|
||||
fullName: credential.fullName
|
||||
)
|
||||
|
||||
accountProviderRaw = AccountProvider.apple.rawValue
|
||||
if let email = credential.email, !email.isEmpty {
|
||||
accountEmail = email
|
||||
}
|
||||
|
||||
let formatter = PersonNameComponentsFormatter()
|
||||
let appleName = credential.fullName.map { formatter.string(from: $0).trimmingCharacters(in: .whitespacesAndNewlines) } ?? ""
|
||||
if !appleName.isEmpty {
|
||||
displayName = appleName
|
||||
}
|
||||
|
||||
profileMessage = "Apple Account wurde verknüpft."
|
||||
case .failure(let error):
|
||||
profileMessage = "Apple Login konnte nicht abgeschlossen werden: \(error.localizedDescription)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct SettingsExportFile: Identifiable {
|
||||
|
||||
Reference in New Issue
Block a user