Settings: echtes Profil via /api/auth/me (Rolle, Founder, Abo, Avatar)
Login liefert nur {token, name, is_premium}. Für Admin-/Founder-/Tier-Info
holen wir nach Login (und beim Erscheinen von MainTabView) /api/auth/me und
zeigen ein echtes Profil mit Avatar, Email, Rolle und nur dann Premium-Status,
wenn das relevant ist.
This commit is contained in:
parent
81681130e6
commit
bfd327bd40
4 changed files with 126 additions and 3 deletions
|
|
@ -13,6 +13,22 @@ struct LoginResponse: Decodable {
|
|||
let isPremium: Bool
|
||||
}
|
||||
|
||||
struct UserProfile: Decodable {
|
||||
let id: Int
|
||||
let name: String
|
||||
let email: String
|
||||
let realName: String?
|
||||
let rolle: String?
|
||||
let isPremium: Bool?
|
||||
let isFounder: Bool?
|
||||
let isPartner: Bool?
|
||||
let founderNumber: Int?
|
||||
let subscriptionTier: String?
|
||||
let avatarUrl: String?
|
||||
let wohnort: String?
|
||||
let bio: String?
|
||||
}
|
||||
|
||||
// MARK: - Dogs
|
||||
|
||||
struct Dog: Decodable, Identifiable {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ final class AuthSession {
|
|||
var token: String?
|
||||
var userName: String?
|
||||
var isPremium: Bool = false
|
||||
var profile: UserProfile?
|
||||
var isLoggingIn: Bool = false
|
||||
var errorMessage: String?
|
||||
|
||||
|
|
@ -46,5 +47,21 @@ final class AuthSession {
|
|||
token = nil
|
||||
userName = nil
|
||||
isPremium = false
|
||||
profile = nil
|
||||
}
|
||||
|
||||
/// Fetches the full user profile from /api/auth/me. Called after login and
|
||||
/// when MainTabView appears, so admin/founder/role info shows up.
|
||||
func loadProfile() async {
|
||||
guard token != nil else { return }
|
||||
do {
|
||||
let me: UserProfile = try await APIClient.shared.get("/api/auth/me")
|
||||
self.profile = me
|
||||
if let premium = me.isPremium {
|
||||
self.isPremium = premium
|
||||
}
|
||||
} catch {
|
||||
// Profil-Refresh ist non-critical; Settings zeigt sonst nur die Basics.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import SwiftUI
|
||||
|
||||
struct MainTabView: View {
|
||||
@Environment(AuthSession.self) private var auth
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
RoutesListView()
|
||||
|
|
@ -12,5 +14,6 @@ struct MainTabView: View {
|
|||
SettingsView()
|
||||
.tabItem { Label("Mehr", systemImage: "person.crop.circle") }
|
||||
}
|
||||
.task { await auth.loadProfile() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,50 @@ struct SettingsView: View {
|
|||
var body: some View {
|
||||
NavigationStack {
|
||||
Form {
|
||||
Section("Account") {
|
||||
LabeledContent("Name", value: auth.userName ?? "—")
|
||||
LabeledContent("Premium", value: auth.isPremium ? "Ja" : "Nein")
|
||||
Section {
|
||||
HStack(spacing: 14) {
|
||||
avatarView
|
||||
.frame(width: 60, height: 60)
|
||||
.clipShape(Circle())
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(displayName)
|
||||
.font(.headline)
|
||||
if let email = auth.profile?.email {
|
||||
Text(email)
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
|
||||
Section("Account") {
|
||||
LabeledContent("Rolle", value: rolleLabel)
|
||||
if auth.profile?.isFounder == true {
|
||||
LabeledContent("Founder", value: founderLabel)
|
||||
}
|
||||
if auth.profile?.isPartner == true {
|
||||
LabeledContent("Partner", value: "Ja")
|
||||
}
|
||||
if let tier = auth.profile?.subscriptionTier, !tier.isEmpty {
|
||||
LabeledContent("Abo", value: tier.capitalized)
|
||||
}
|
||||
LabeledContent("Premium", value: premiumValue ? "Ja" : "Nein")
|
||||
}
|
||||
|
||||
if let wohnort = auth.profile?.wohnort, !wohnort.isEmpty {
|
||||
Section("Ort") {
|
||||
Text(wohnort)
|
||||
}
|
||||
}
|
||||
|
||||
Section {
|
||||
Button("Abmelden", role: .destructive) {
|
||||
auth.logout()
|
||||
}
|
||||
}
|
||||
|
||||
Section("Über") {
|
||||
Text("Ban Yaro Go ist die native iOS-Ergänzung zur banyaro.app PWA. Phase 1: deine Touren ansehen.")
|
||||
.font(.footnote)
|
||||
|
|
@ -22,6 +57,58 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
.navigationTitle("Mehr")
|
||||
.refreshable { await auth.loadProfile() }
|
||||
}
|
||||
}
|
||||
|
||||
private var displayName: String {
|
||||
auth.profile?.name ?? auth.userName ?? "—"
|
||||
}
|
||||
|
||||
private var premiumValue: Bool {
|
||||
auth.profile?.isPremium ?? auth.isPremium
|
||||
}
|
||||
|
||||
private var rolleLabel: String {
|
||||
switch auth.profile?.rolle?.lowercased() {
|
||||
case "admin": return "Admin"
|
||||
case "moderator": return "Moderator"
|
||||
case nil: return "—"
|
||||
default: return "Mitglied"
|
||||
}
|
||||
}
|
||||
|
||||
private var founderLabel: String {
|
||||
if let n = auth.profile?.founderNumber { return "#\(n)" }
|
||||
return "Ja"
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var avatarView: some View {
|
||||
if let path = auth.profile?.avatarUrl, !path.isEmpty,
|
||||
let url = avatarURL(path) {
|
||||
AsyncImage(url: url) { phase in
|
||||
switch phase {
|
||||
case .success(let img): img.resizable().scaledToFill()
|
||||
default: avatarPlaceholder
|
||||
}
|
||||
}
|
||||
} else {
|
||||
avatarPlaceholder
|
||||
}
|
||||
}
|
||||
|
||||
private var avatarPlaceholder: some View {
|
||||
ZStack {
|
||||
Color.accentColor.opacity(0.2)
|
||||
Image(systemName: "person.crop.circle.fill")
|
||||
.font(.system(size: 36))
|
||||
.foregroundStyle(Color.accentColor)
|
||||
}
|
||||
}
|
||||
|
||||
private func avatarURL(_ path: String) -> URL? {
|
||||
if path.hasPrefix("http") { return URL(string: path) }
|
||||
return URL(string: "https://banyaro.app\(path)")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue