This commit is contained in:
Trey t
2025-11-05 20:52:51 -06:00
parent bc289d6c88
commit a8083380aa
12 changed files with 720 additions and 78 deletions

View File

@@ -4,7 +4,8 @@ struct LoginView: View {
@StateObject private var viewModel = LoginViewModel()
@FocusState private var focusedField: Field?
@State private var showingRegister = false
@State private var showingVerifyEmail = false
@State private var showMainTab = false
@State private var showVerification = false
enum Field {
case username, password
@@ -98,22 +99,40 @@ struct LoginView: View {
}
.navigationTitle("Welcome Back")
.navigationBarTitleDisplayMode(.large)
.fullScreenCover(isPresented: $viewModel.isAuthenticated) {
if viewModel.isVerified {
MainTabView()
} else {
VerifyEmailView(
onVerifySuccess: {
// After verification, show main tab view
viewModel.isVerified = true
},
onLogout: {
// Logout and dismiss verification screen
viewModel.logout()
}
)
.onChange(of: viewModel.isAuthenticated) { _, isAuth in
if isAuth {
print("isAuthenticated changed to true, isVerified = \(viewModel.isVerified)")
if viewModel.isVerified {
showMainTab = true
} else {
showVerification = true
}
}
}
.onChange(of: viewModel.isVerified) { _, isVerified in
print("isVerified changed to \(isVerified)")
if isVerified && viewModel.isAuthenticated {
showVerification = false
showMainTab = true
}
}
.fullScreenCover(isPresented: $showMainTab) {
MainTabView()
}
.fullScreenCover(isPresented: $showVerification) {
VerifyEmailView(
onVerifySuccess: {
// After verification, show main tab view
viewModel.isVerified = true
},
onLogout: {
// Logout and dismiss verification screen
viewModel.logout()
showVerification = false
showMainTab = false
}
)
}
.sheet(isPresented: $showingRegister) {
RegisterView()
}

View File

@@ -82,16 +82,21 @@ class LoginViewModel: ObservableObject {
// Store user data and verification status
self.currentUser = user
self.isVerified = user.verified
// Initialize lookups repository after successful login
LookupsManager.shared.initialize()
// Update authentication state
self.isAuthenticated = true
self.isLoading = false
print("Login successful! Token: token")
print("User: \(user.username), Verified: \(user.verified)")
print("isVerified set to: \(self.isVerified)")
// Initialize lookups repository after successful login
LookupsManager.shared.initialize()
// Update authentication state AFTER setting verified status
// Small delay to ensure state updates are processed
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
self.isAuthenticated = true
print("isAuthenticated set to true, isVerified is: \(self.isVerified)")
}
}
}
@@ -113,9 +118,13 @@ class LoginViewModel: ObservableObject {
// Reset state
isAuthenticated = false
isVerified = false
currentUser = nil
username = ""
password = ""
errorMessage = nil
print("Logged out - all state reset")
}
func clearError() {

View File

@@ -20,77 +20,83 @@ struct MainTabView: View {
}
.tag(1)
ProfileView()
.tabItem {
Label("Profile", systemImage: "person.fill")
}
.tag(2)
NavigationView {
ProfileTabView()
}
.tabItem {
Label("Profile", systemImage: "person.fill")
}
.tag(2)
}
}
}
struct ProfileView: View {
struct ProfileTabView: View {
@StateObject private var loginViewModel = LoginViewModel()
@Environment(\.dismiss) var dismiss
@State private var showingProfileEdit = false
var body: some View {
NavigationView {
List {
Section {
HStack {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 60, height: 60)
.foregroundColor(.blue)
List {
Section {
HStack {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 60, height: 60)
.foregroundColor(.blue)
VStack(alignment: .leading, spacing: 4) {
Text("User Profile")
.font(.headline)
Text("Manage your account")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
Section("Settings") {
NavigationLink(destination: Text("Account Settings")) {
Label("Account Settings", systemImage: "gear")
}
NavigationLink(destination: Text("Notifications")) {
Label("Notifications", systemImage: "bell")
}
NavigationLink(destination: Text("Privacy")) {
Label("Privacy", systemImage: "lock.shield")
}
}
Section {
Button(action: {
loginViewModel.logout()
}) {
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
.foregroundColor(.red)
}
}
Section {
VStack(alignment: .leading, spacing: 4) {
Text("MyCrib")
.font(.caption)
.fontWeight(.semibold)
Text("User Profile")
.font(.headline)
Text("Version 1.0.0")
.font(.caption2)
Text("Manage your account")
.font(.caption)
.foregroundColor(.secondary)
}
}
.padding(.vertical, 8)
}
.navigationTitle("Profile")
Section("Account") {
Button(action: {
showingProfileEdit = true
}) {
Label("Edit Profile", systemImage: "person.crop.circle")
.foregroundColor(.primary)
}
NavigationLink(destination: Text("Notifications")) {
Label("Notifications", systemImage: "bell")
}
NavigationLink(destination: Text("Privacy")) {
Label("Privacy", systemImage: "lock.shield")
}
}
Section {
Button(action: {
loginViewModel.logout()
}) {
Label("Log Out", systemImage: "rectangle.portrait.and.arrow.right")
.foregroundColor(.red)
}
}
Section {
VStack(alignment: .leading, spacing: 4) {
Text("MyCrib")
.font(.caption)
.fontWeight(.semibold)
Text("Version 1.0.0")
.font(.caption2)
.foregroundColor(.secondary)
}
}
}
.navigationTitle("Profile")
.sheet(isPresented: $showingProfileEdit) {
ProfileView()
}
}
}

View File

@@ -0,0 +1,142 @@
import SwiftUI
struct ProfileView: View {
@StateObject private var viewModel = ProfileViewModel()
@Environment(\.dismiss) var dismiss
@FocusState private var focusedField: Field?
enum Field {
case firstName, lastName, email
}
var body: some View {
NavigationView {
if viewModel.isLoadingUser {
VStack {
ProgressView()
Text("Loading profile...")
.font(.subheadline)
.foregroundColor(.secondary)
.padding(.top, 8)
}
} else {
Form {
Section {
VStack(spacing: 16) {
Image(systemName: "person.circle.fill")
.font(.system(size: 60))
.foregroundStyle(.blue.gradient)
Text("Profile Settings")
.font(.title2)
.fontWeight(.bold)
}
.frame(maxWidth: .infinity)
.padding(.vertical)
}
.listRowBackground(Color.clear)
Section {
TextField("First Name", text: $viewModel.firstName)
.textInputAutocapitalization(.words)
.autocorrectionDisabled()
.focused($focusedField, equals: .firstName)
.submitLabel(.next)
.onSubmit {
focusedField = .lastName
}
TextField("Last Name", text: $viewModel.lastName)
.textInputAutocapitalization(.words)
.autocorrectionDisabled()
.focused($focusedField, equals: .lastName)
.submitLabel(.next)
.onSubmit {
focusedField = .email
}
} header: {
Text("Personal Information")
}
Section {
TextField("Email", text: $viewModel.email)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
.focused($focusedField, equals: .email)
.submitLabel(.done)
.onSubmit {
viewModel.updateProfile()
}
} header: {
Text("Contact")
} footer: {
Text("Email is required and must be unique")
}
if let errorMessage = viewModel.errorMessage {
Section {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text(errorMessage)
.foregroundColor(.red)
.font(.subheadline)
}
}
}
if let successMessage = viewModel.successMessage {
Section {
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
Text(successMessage)
.foregroundColor(.green)
.font(.subheadline)
}
}
}
Section {
Button(action: viewModel.updateProfile) {
HStack {
Spacer()
if viewModel.isLoading {
ProgressView()
} else {
Text("Save Changes")
.fontWeight(.semibold)
}
Spacer()
}
}
.disabled(viewModel.isLoading || viewModel.email.isEmpty)
}
}
.navigationTitle("Profile")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .navigationBarLeading) {
Button("Cancel") {
dismiss()
}
}
}
.onChange(of: viewModel.firstName) { _, _ in
viewModel.clearMessages()
}
.onChange(of: viewModel.lastName) { _, _ in
viewModel.clearMessages()
}
.onChange(of: viewModel.email) { _, _ in
viewModel.clearMessages()
}
}
}
}
}
#Preview {
ProfileView()
}

View File

@@ -0,0 +1,121 @@
import Foundation
import ComposeApp
import Combine
@MainActor
class ProfileViewModel: ObservableObject {
// MARK: - Published Properties
@Published var firstName: String = ""
@Published var lastName: String = ""
@Published var email: String = ""
@Published var isLoading: Bool = false
@Published var isLoadingUser: Bool = true
@Published var errorMessage: String?
@Published var successMessage: String?
// MARK: - Private Properties
private let authApi: AuthApi
private let tokenStorage: TokenStorage
// MARK: - Initialization
init() {
self.authApi = AuthApi(client: ApiClient_iosKt.createHttpClient())
self.tokenStorage = TokenStorage()
// Initialize TokenStorage with platform-specific manager
self.tokenStorage.initialize(manager: TokenManager.init())
// Load current user data
loadCurrentUser()
}
// MARK: - Public Methods
func loadCurrentUser() {
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
isLoadingUser = false
return
}
isLoadingUser = true
errorMessage = nil
authApi.getCurrentUser(token: token) { result, error in
if let successResult = result as? ApiResultSuccess<User> {
self.handleLoadSuccess(user: successResult.data!)
} else if let error = error {
self.errorMessage = error.localizedDescription
self.isLoadingUser = false
} else {
self.errorMessage = "Failed to load user data"
self.isLoadingUser = false
}
}
}
func updateProfile() {
guard !email.isEmpty else {
errorMessage = "Email is required"
return
}
guard let token = tokenStorage.getToken() else {
errorMessage = "Not authenticated"
return
}
isLoading = true
errorMessage = nil
successMessage = nil
let request = UpdateProfileRequest(
firstName: firstName.isEmpty ? nil : firstName,
lastName: lastName.isEmpty ? nil : lastName,
email: email
)
authApi.updateProfile(token: token, request: request) { result, error in
if let successResult = result as? ApiResultSuccess<User> {
self.handleUpdateSuccess(user: successResult.data!)
} else if let error = error {
self.handleError(message: error.localizedDescription)
} else {
self.handleError(message: "Failed to update profile")
}
}
}
func clearMessages() {
errorMessage = nil
successMessage = nil
}
// MARK: - Private Methods
@MainActor
private func handleLoadSuccess(user: User) {
firstName = user.firstName ?? ""
lastName = user.lastName ?? ""
email = user.email
isLoadingUser = false
errorMessage = nil
}
@MainActor
private func handleUpdateSuccess(user: User) {
firstName = user.firstName ?? ""
lastName = user.lastName ?? ""
email = user.email
isLoading = false
errorMessage = nil
successMessage = "Profile updated successfully"
print("Profile updated: \(user.firstName ?? "") \(user.lastName ?? "")")
}
@MainActor
private func handleError(message: String) {
isLoading = false
errorMessage = message
successMessage = nil
}
}