Contractor Sharing: - Add .casera file format for sharing contractors between users - Create SharedContractor model with JSON serialization - Implement ContractorSharingManager for iOS (Swift) and Android (Kotlin) - Register .casera file type in iOS Info.plist and Android manifest - Add share button to ContractorDetailView (iOS) and ContractorDetailScreen (Android) - Add import confirmation, success, and error dialogs - Create expect/actual platform implementations for sharing and import handling Navigation Changes: - Remove Profile tab from bottom tab bar (iOS and Android) - Add settings gear icon to left side of "My Properties" title - Settings gear opens Profile/Settings screen as sheet (iOS) or navigates (Android) - Add property button to top right action bar Bug Fixes: - Fix ResidenceUsersResponse to match API's flat array response format - Fix GenerateShareCodeResponse handling to access nested shareCode property - Update ManageUsersDialog to accept residenceOwnerId parameter 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
196 lines
7.3 KiB
Swift
196 lines
7.3 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
|
|
@State private var showingSettings = 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(
|
|
summary: viewModel.totalSummary ?? response.summary,
|
|
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(L10n.Residences.title)
|
|
.navigationBarTitleDisplayMode(.inline)
|
|
.toolbar {
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
Button(action: {
|
|
showingSettings = true
|
|
}) {
|
|
Image(systemName: "gearshape.fill")
|
|
.font(.system(size: 18, weight: .semibold))
|
|
.foregroundColor(Color.appPrimary)
|
|
}
|
|
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.settingsButton)
|
|
}
|
|
|
|
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)
|
|
}
|
|
.sheet(isPresented: $showingSettings) {
|
|
NavigationView {
|
|
ProfileTabView()
|
|
}
|
|
}
|
|
.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 summary: TotalSummary
|
|
let residences: [ResidenceResponse]
|
|
|
|
var body: some View {
|
|
ScrollView(showsIndicators: false) {
|
|
VStack(spacing: AppSpacing.lg) {
|
|
// Summary Card
|
|
SummaryCard(summary: summary)
|
|
.padding(.horizontal, AppSpacing.md)
|
|
.padding(.top, AppSpacing.sm)
|
|
|
|
// Properties Header
|
|
HStack {
|
|
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
|
|
Text(L10n.Residences.yourProperties)
|
|
.font(.title3.weight(.semibold))
|
|
.foregroundColor(Color.appTextPrimary)
|
|
Text("\(residences.count) \(residences.count == 1 ? L10n.Residences.property : L10n.Residences.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 }
|
|
)
|
|
}
|
|
}
|