KMM (Android/Shared): - Add strings.xml with 200+ localized strings - Add translation files for es, fr, de, pt languages - Update all screens to use stringResource() for i18n - Add Accept-Language header to API client for all platforms iOS: - Add L10n.swift helper with type-safe string accessors - Add Localizable.xcstrings with translations for all 5 languages - Update all SwiftUI views to use L10n.* for localized strings - Localize Auth, Residence, Task, Contractor, Document, and Profile views Supported languages: English, Spanish, French, German, Portuguese 🤖 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(L10n.Residences.title)
|
|
.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: [ResidenceResponse]
|
|
|
|
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(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 }
|
|
)
|
|
}
|
|
}
|