Add contractor sharing feature and move settings to navigation bar

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>
This commit is contained in:
Trey t
2025-12-05 22:30:19 -06:00
parent 2965ec4031
commit 859a6679ed
43 changed files with 1848 additions and 148 deletions

View File

@@ -5,10 +5,11 @@ struct ManageUsersView: View {
let residenceId: Int32
let residenceName: String
let isPrimaryOwner: Bool
let residenceOwnerId: Int32
@Environment(\.dismiss) private var dismiss
@State private var users: [ResidenceUserResponse] = []
@State private var ownerId: Int32?
private var ownerId: Int32 { residenceOwnerId }
@State private var shareCode: ShareCodeResponse?
@State private var isLoading = true
@State private var errorMessage: String?
@@ -97,10 +98,9 @@ struct ManageUsersView: View {
let result = try await APILayer.shared.getResidenceUsers(residenceId: Int32(Int(residenceId)))
await MainActor.run {
if let successResult = result as? ApiResultSuccess<ResidenceUsersResponse>,
let responseData = successResult.data as? ResidenceUsersResponse {
self.users = Array(responseData.users)
self.ownerId = Int32(responseData.owner.id)
if let successResult = result as? ApiResultSuccess<NSArray>,
let responseData = successResult.data as? [ResidenceUserResponse] {
self.users = responseData
self.isLoading = false
} else if let errorResult = result as? ApiResultError {
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
@@ -148,8 +148,9 @@ struct ManageUsersView: View {
let result = try await APILayer.shared.generateShareCode(residenceId: Int32(Int(residenceId)))
await MainActor.run {
if let successResult = result as? ApiResultSuccess<ShareCodeResponse> {
self.shareCode = successResult.data
if let successResult = result as? ApiResultSuccess<GenerateShareCodeResponse>,
let response = successResult.data {
self.shareCode = response.shareCode
self.isGeneratingCode = false
} else if let errorResult = result as? ApiResultError {
self.errorMessage = ErrorMessageParser.parse(errorResult.message)
@@ -195,5 +196,5 @@ struct ManageUsersView: View {
}
#Preview {
ManageUsersView(residenceId: 1, residenceName: "My Home", isPrimaryOwner: true)
ManageUsersView(residenceId: 1, residenceName: "My Home", isPrimaryOwner: true, residenceOwnerId: 1)
}

View File

@@ -120,7 +120,8 @@ struct ResidenceDetailView: View {
ManageUsersView(
residenceId: residence.id,
residenceName: residence.name,
isPrimaryOwner: isCurrentUserOwner(of: residence)
isPrimaryOwner: isCurrentUserOwner(of: residence),
residenceOwnerId: residence.ownerId
)
}
}

View File

@@ -6,6 +6,7 @@ struct ResidencesListView: View {
@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
@@ -46,6 +47,17 @@ struct ResidencesListView: View {
.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
@@ -93,6 +105,11 @@ struct ResidencesListView: View {
.sheet(isPresented: $showingUpgradePrompt) {
UpgradePromptView(triggerKey: "add_second_property", isPresented: $showingUpgradePrompt)
}
.sheet(isPresented: $showingSettings) {
NavigationView {
ProfileTabView()
}
}
.onAppear {
if authManager.isAuthenticated {
viewModel.loadMyResidences()