- Fix RegisterView to call AuthenticationManager.login() after email verification so user is properly transitioned to home screen instead of returning to login - Fix ResidencesListView to load data when authentication state becomes true, ensuring residences load after registration/login - Add accessibility identifier to verification code field for UI testing - Add NSAppTransportSecurity exceptions for localhost/127.0.0.1 for local dev - Add comprehensive XCUITest suite for registration flow including: - Form validation tests (empty fields, invalid email, mismatched passwords) - Full registration and verification flow test - Logout from verification screen test - Helper scripts for test user cleanup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
179 lines
6.6 KiB
Swift
179 lines
6.6 KiB
Swift
import SwiftUI
|
|
import ComposeApp
|
|
|
|
struct ResidencesListView: View {
|
|
@StateObject private var viewModel = ResidenceViewModel()
|
|
@State private var showingAddResidence = false
|
|
@State private var showingJoinResidence = false
|
|
@State private var showingUpgradePrompt = false
|
|
@StateObject private var authManager = AuthenticationManager.shared
|
|
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
|
|
|
|
var body: some View {
|
|
ZStack {
|
|
Color.appBackgroundPrimary
|
|
.ignoresSafeArea()
|
|
|
|
if let response = viewModel.myResidences {
|
|
ListAsyncContentView(
|
|
items: response.residences,
|
|
isLoading: viewModel.isLoading,
|
|
errorMessage: viewModel.errorMessage,
|
|
content: { residences in
|
|
ResidencesContent(
|
|
response: response,
|
|
residences: residences
|
|
)
|
|
},
|
|
emptyContent: {
|
|
EmptyResidencesView()
|
|
},
|
|
onRefresh: {
|
|
viewModel.loadMyResidences(forceRefresh: true)
|
|
},
|
|
onRetry: {
|
|
viewModel.loadMyResidences()
|
|
}
|
|
)
|
|
} else if viewModel.isLoading {
|
|
DefaultLoadingView()
|
|
} else if let error = viewModel.errorMessage {
|
|
DefaultErrorView(message: error, onRetry: {
|
|
viewModel.loadMyResidences()
|
|
})
|
|
}
|
|
}
|
|
.navigationTitle("My Properties")
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItemGroup(placement: .navigationBarTrailing) {
|
|
Button(action: {
|
|
// Check if we should show upgrade prompt before joining
|
|
let currentCount = viewModel.myResidences?.residences.count ?? 0
|
|
if subscriptionCache.shouldShowUpgradePrompt(currentCount: currentCount, limitKey: "properties") {
|
|
showingUpgradePrompt = true
|
|
} else {
|
|
showingJoinResidence = true
|
|
}
|
|
}) {
|
|
Image(systemName: "person.badge.plus")
|
|
.font(.system(size: 18, weight: .semibold))
|
|
.foregroundColor(Color.appPrimary)
|
|
}
|
|
|
|
Button(action: {
|
|
// Check if we should show upgrade prompt before adding
|
|
let currentCount = viewModel.myResidences?.residences.count ?? 0
|
|
if subscriptionCache.shouldShowUpgradePrompt(currentCount: currentCount, limitKey: "properties") {
|
|
showingUpgradePrompt = true
|
|
} else {
|
|
showingAddResidence = true
|
|
}
|
|
}) {
|
|
Image(systemName: "plus.circle.fill")
|
|
.font(.system(size: 22, weight: .semibold))
|
|
.foregroundColor(Color.appPrimary)
|
|
}
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Residence.addButton)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showingAddResidence) {
|
|
AddResidenceView(
|
|
isPresented: $showingAddResidence,
|
|
onResidenceCreated: {
|
|
viewModel.loadMyResidences(forceRefresh: true)
|
|
}
|
|
)
|
|
}
|
|
.sheet(isPresented: $showingJoinResidence) {
|
|
JoinResidenceView(onJoined: {
|
|
viewModel.loadMyResidences()
|
|
})
|
|
}
|
|
.sheet(isPresented: $showingUpgradePrompt) {
|
|
UpgradePromptView(triggerKey: "add_second_property", isPresented: $showingUpgradePrompt)
|
|
}
|
|
.onAppear {
|
|
if authManager.isAuthenticated {
|
|
viewModel.loadMyResidences()
|
|
}
|
|
}
|
|
.fullScreenCover(isPresented: $authManager.isAuthenticated.negated) {
|
|
LoginView(onLoginSuccess: {
|
|
authManager.isAuthenticated = true
|
|
viewModel.loadMyResidences()
|
|
})
|
|
.interactiveDismissDisabled()
|
|
}
|
|
.onChange(of: authManager.isAuthenticated) { isAuth in
|
|
if isAuth {
|
|
// User just logged in or registered - load their residences
|
|
viewModel.loadMyResidences()
|
|
} else {
|
|
// User logged out - clear data
|
|
viewModel.myResidences = nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Residences Content View
|
|
|
|
private struct ResidencesContent: View {
|
|
let response: MyResidencesResponse
|
|
let residences: [ResidenceWithTasks]
|
|
|
|
var body: some View {
|
|
ScrollView(showsIndicators: false) {
|
|
VStack(spacing: AppSpacing.lg) {
|
|
// Summary Card
|
|
SummaryCard(summary: response.summary)
|
|
.padding(.horizontal, AppSpacing.md)
|
|
.padding(.top, AppSpacing.sm)
|
|
|
|
// Properties Header
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
|
Text("Your Properties")
|
|
.font(.title3.weight(.semibold))
|
|
.foregroundColor(Color.appTextPrimary)
|
|
Text("\(residences.count) \(residences.count == 1 ? "property" : "properties")")
|
|
.font(.callout)
|
|
.foregroundColor(Color.appTextSecondary)
|
|
}
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, AppSpacing.md)
|
|
|
|
// Residences List
|
|
ForEach(residences, id: \.id) { residence in
|
|
NavigationLink(destination: ResidenceDetailView(residenceId: residence.id)) {
|
|
ResidenceCard(residence: residence)
|
|
.padding(.horizontal, AppSpacing.md)
|
|
}
|
|
.buttonStyle(PlainButtonStyle())
|
|
}
|
|
}
|
|
.padding(.bottom, AppSpacing.xxxl)
|
|
}
|
|
.safeAreaInset(edge: .bottom) {
|
|
Color.clear.frame(height: 0)
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NavigationView {
|
|
ResidencesListView()
|
|
}
|
|
}
|
|
|
|
extension Binding where Value == Bool {
|
|
var negated: Binding<Bool> {
|
|
Binding<Bool>(
|
|
get: { !self.wrappedValue },
|
|
set: { self.wrappedValue = !$0 }
|
|
)
|
|
}
|
|
}
|