feat(auth): Guest-Mode + Login-optionale Surface
RootView ohne Hard-Login-Gate — TabBar zeigt sich immer, beim Start wechselt App bei .signedOut automatisch in den anonymen .guest-Modus (mana-swift-core v1.2.0). Auth-Sheets (Login, SignUp, Forgot, Reset) hängen jetzt als ManaAuthGate-Modifier am Root. AccountView zeigt im Guest-Modus eine eigene CTA-Surface („Anmelden / Konto erstellen" + Hinweis was Login bringt). signOut nutzt keepGuestMode: true → App bleibt nach Logout anonym nutzbar, Marketplace und lokale Daten gehen nicht verloren. DeckListView: Empty-State im Guest-Mode mit Login-CTA + Marketplace- Hinweis. Toolbar-„+"-Button via authGate.require gewrappt — Tap aus dem Guest-Modus öffnet erst das Sign-In-Sheet, danach den Editor. DeckListStore.refresh() skippt im Guest-Mode (kein 401-Spam). Cache wird so wie er ist gerendert (heute leer, später Marketplace-Klone). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
da6679770b
commit
8ca7bd3636
5 changed files with 276 additions and 160 deletions
|
|
@ -4,6 +4,7 @@ import SwiftUI
|
|||
|
||||
struct AccountView: View {
|
||||
@Environment(AuthClient.self) private var auth
|
||||
@Environment(ManaAuthGate.self) private var authGate
|
||||
@State private var showChangeEmail = false
|
||||
@State private var showChangePassword = false
|
||||
@State private var showDeleteAccount = false
|
||||
|
|
@ -11,63 +12,16 @@ struct AccountView: View {
|
|||
var body: some View {
|
||||
ZStack {
|
||||
CardsTheme.background.ignoresSafeArea()
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "person.crop.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
|
||||
if let email = auth.currentEmail {
|
||||
Text(email)
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
Group {
|
||||
switch auth.status {
|
||||
case .signedIn:
|
||||
signedInContent
|
||||
case .guest, .signedOut, .error, .unknown:
|
||||
guestContent
|
||||
case .signingIn, .twoFactorRequired:
|
||||
ProgressView().tint(CardsTheme.primary)
|
||||
}
|
||||
|
||||
VStack(spacing: 12) {
|
||||
NavigationLink {
|
||||
SettingsView()
|
||||
} label: {
|
||||
rowLabel("Einstellungen", systemImage: "gear")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button { showChangeEmail = true } label: {
|
||||
rowLabel("Email ändern", systemImage: "envelope")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button { showChangePassword = true } label: {
|
||||
rowLabel("Passwort ändern", systemImage: "key")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(role: .destructive) {
|
||||
Task { await auth.signOut() }
|
||||
} label: {
|
||||
Text("Abmelden")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(CardsTheme.error.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
// App-Store-Guideline 5.1.1(v): jede App mit Sign-Up MUSS
|
||||
// eine Account-Löschung anbieten.
|
||||
Button(role: .destructive) {
|
||||
showDeleteAccount = true
|
||||
} label: {
|
||||
Text("Account löschen…")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.padding(.top, 48)
|
||||
}
|
||||
.navigationTitle("Account")
|
||||
#if os(iOS)
|
||||
|
|
@ -98,7 +52,132 @@ struct AccountView: View {
|
|||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var signedInContent: some View {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "person.crop.circle.fill")
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.foregroundStyle(CardsTheme.primary)
|
||||
|
||||
if let email = auth.currentEmail {
|
||||
Text(email)
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
}
|
||||
|
||||
VStack(spacing: 12) {
|
||||
NavigationLink {
|
||||
SettingsView()
|
||||
} label: {
|
||||
rowLabel("Einstellungen", systemImage: "gear")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button { showChangeEmail = true } label: {
|
||||
rowLabel("Email ändern", systemImage: "envelope")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Button { showChangePassword = true } label: {
|
||||
rowLabel("Passwort ändern", systemImage: "key")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
ManaTwoFactorAccountRow(auth: auth)
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 16)
|
||||
.background(CardsTheme.surface, in: RoundedRectangle(cornerRadius: 8))
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(CardsTheme.border, lineWidth: 1)
|
||||
)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button(role: .destructive) {
|
||||
// Logout behält die Guest-Identity → App bleibt im
|
||||
// anonymen Modus nutzbar (lokale Decks, Marketplace
|
||||
// browsen). Wer „alles vergessen" will, nutzt
|
||||
// „Account löschen".
|
||||
Task { await auth.signOut(keepGuestMode: true) }
|
||||
} label: {
|
||||
Text("Abmelden")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 12)
|
||||
.background(CardsTheme.error.opacity(0.1), in: RoundedRectangle(cornerRadius: 8))
|
||||
.foregroundStyle(CardsTheme.error)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
// App-Store-Guideline 5.1.1(v): jede App mit Sign-Up MUSS
|
||||
// eine Account-Löschung anbieten.
|
||||
Button(role: .destructive) {
|
||||
showDeleteAccount = true
|
||||
} label: {
|
||||
Text("Account löschen…")
|
||||
.font(.footnote)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
}
|
||||
.padding(.bottom, 16)
|
||||
}
|
||||
.padding(.top, 48)
|
||||
}
|
||||
|
||||
private var guestContent: some View {
|
||||
VStack(spacing: 20) {
|
||||
Image(systemName: "person.crop.circle.dashed")
|
||||
.resizable()
|
||||
.frame(width: 80, height: 80)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
|
||||
VStack(spacing: 8) {
|
||||
Text("Du nutzt Cardecky anonym")
|
||||
.font(.headline)
|
||||
.foregroundStyle(CardsTheme.foreground)
|
||||
Text(
|
||||
"""
|
||||
Marketplace und lokale Decks funktionieren ohne Konto. \
|
||||
Für KI-Karten, eigene Decks im Cloud-Sync und Marketplace-\
|
||||
Veröffentlichung brauchst du ein Konto.
|
||||
"""
|
||||
)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(CardsTheme.mutedForeground)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
Button {
|
||||
// Trigger ohne pending-Action — wir wollen einfach
|
||||
// das Sign-In-Sheet öffnen. `require` mit no-op
|
||||
// schaltet die Sheet-Logik des Gates ein.
|
||||
authGate.require(reason: "account-tab") {}
|
||||
} label: {
|
||||
Text("Anmelden / Konto erstellen")
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 14)
|
||||
.background(CardsTheme.primary, in: RoundedRectangle(cornerRadius: 10))
|
||||
.foregroundStyle(.white)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
NavigationLink {
|
||||
SettingsView()
|
||||
} label: {
|
||||
rowLabel("Einstellungen", systemImage: "gear")
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.top, 48)
|
||||
}
|
||||
|
||||
private func rowLabel(_ title: String, systemImage: String) -> some View {
|
||||
Label(title, systemImage: systemImage)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue