import ManaAuthUI import ManaCore import SwiftUI /// Top-Level-View: TabBar mit drei Tabs (Decks / Entdecken / Account). /// Kein harter Login-Gate mehr — Cardecky läuft auch im Guest-Modus /// (lokale Decks lernen, Marketplace browsen). Schreibende Server- /// Aktionen werden über ``ManaAuthGate`` einzeln auf Login eskaliert. struct RootView: View { @Environment(AuthClient.self) private var auth @Environment(ManaAuthGate.self) private var authGate @State private var selectedTab: AppTab = .decks @State private var pendingDeepLinkSlug: String? @State private var showCreateDeck = false @State private var showSignUpSheet = false @State private var showForgotSheet = false @State private var resetPasswordToken: String? private let sourceAppUrl = URL(string: "https://cardecky.mana.how/auth/verify")! private let resetUniversalLink = URL(string: "https://cardecky.mana.how/auth/reset")! var body: some View { mainTabs .onOpenURL { url in handle(url: url) } .onContinueUserActivity(NSUserActivityTypeBrowsingWeb) { activity in if let url = activity.webpageURL { handle(url: url) } } .manaBrand(CardsBrand.manaBrand) .manaAuthGate(authGate) { gateSignInContent } .sheet(item: Binding( get: { resetPasswordToken.map(IdentifiedString.init) }, set: { resetPasswordToken = $0?.value } )) { token in ManaResetPasswordView( token: token.value, auth: auth, onDone: { resetPasswordToken = nil } ) .manaBrand(CardsBrand.manaBrand) } .task { // DEBUG: Auto-Login mit DebugCredentials, falls signedOut. // Release: no-op. Danach in Guest-Mode wechseln, wenn weder // signedIn noch eingebuchtet — Cardecky soll *immer* nutzbar // sein, auch ohne Account. await auth.ensureSignedIn() if case .signedOut = auth.status { do { _ = try auth.enterGuestMode() } catch { Log.auth.warning( "Guest-Mode konnte nicht aktiviert werden: \(String(describing: error), privacy: .public)" ) } } } } /// Content für das ``ManaAuthGate``-Sheet — wenn ein gegateter Button /// gedrückt wird, fliegt der User in den Sign-In-Flow. Sign-Up und /// Forgot-Password werden als verschachtelte Sheets aufgeklappt, /// damit aus dem Gate-Sheet alle Auth-Pfade erreichbar bleiben. private var gateSignInContent: some View { NavigationStack { ManaLoginView( auth: auth, onSignUpTapped: { showSignUpSheet = true }, onForgotTapped: { showForgotSheet = true } ) .manaBrand(CardsBrand.manaBrand) .sheet(isPresented: $showSignUpSheet) { ManaSignUpView( auth: auth, sourceAppUrl: sourceAppUrl, onDone: { showSignUpSheet = false } ) .manaBrand(CardsBrand.manaBrand) } .sheet(isPresented: $showForgotSheet) { ManaForgotPasswordView( auth: auth, resetUniversalLink: resetUniversalLink, onDone: { showForgotSheet = false } ) .manaBrand(CardsBrand.manaBrand) } } } private var mainTabs: some View { TabView(selection: $selectedTab) { DeckListView(showCreate: $showCreateDeck) .tabItem { Label("Decks", systemImage: "rectangle.stack") } .tag(AppTab.decks) ExploreView(deepLinkSlug: $pendingDeepLinkSlug) .tabItem { Label("Entdecken", systemImage: "sparkles") } .tag(AppTab.explore) NavigationStack { AccountView() } .tabItem { Label("Account", systemImage: "person.crop.circle") } .tag(AppTab.account) } .decksCreateAccessory(visible: selectedTab == .decks) { authGate.require(reason: "deck-create-accessory") { showCreateDeck = true } } } /// Universal-Link- und URL-Scheme-Handler: /// - `https://cardecky.mana.how/d/` → Explore-Tab + PublicDeckView /// - `https://cardecky.mana.how/auth/reset?token=…` → ManaResetPasswordView /// - `cards://study/` → später (β-6 Notifications) private func handle(url: URL) { Log.app.info("Open URL: \(url.absoluteString, privacy: .public)") guard url.host == "cardecky.mana.how" || url.scheme == "cards" else { return } let parts = url.pathComponents.filter { $0 != "/" } // Auth-Reset-Link aus der Passwort-Vergessen-Email. if parts == ["auth", "reset"] { let components = URLComponents(url: url, resolvingAgainstBaseURL: false) if let token = components?.queryItems?.first(where: { $0.name == "token" })?.value { resetPasswordToken = token return } } if parts.count >= 2, parts[0] == "d" { pendingDeepLinkSlug = parts[1] selectedTab = .explore } } } /// Helper für `.sheet(item:)` mit einem String-Value (Reset-Token). private struct IdentifiedString: Identifiable { let value: String var id: String { value } } enum AppTab: Hashable { case decks case explore case account } private extension View { /// iOS 26: floating „Neues Deck"-Pille via `.tabViewBottomAccessory`, /// nur sichtbar wenn der Decks-Tab aktiv ist. iOS 18 fällt auf den /// bestehenden `.bottomBar`-„+"-Toolbar-Button in `DeckListView` zurück. /// /// Den Modifier nur konditional anwenden — sonst rendert das System /// auch bei leerem Inhalt die leere Glass-Hülle (sichtbar als toter /// Streifen über der TabBar auf Entdecken/Account). @ViewBuilder func decksCreateAccessory(visible: Bool, onTap: @escaping () -> Void) -> some View { if #available(iOS 26.0, *), visible { tabViewBottomAccessory { DeckCreateAccessoryPill(action: onTap) } } else { self } } } @available(iOS 26.0, *) private struct DeckCreateAccessoryPill: View { let action: () -> Void var body: some View { Button(action: action) { Label("Neues Deck", systemImage: "plus") .font(.subheadline.weight(.semibold)) } .buttonStyle(.glass) .tint(CardsTheme.primary) .accessibilityLabel("Neues Deck erstellen") } }