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:
@@ -11,6 +11,8 @@ struct ContractorDetailView: View {
|
||||
|
||||
@State private var showingEditSheet = false
|
||||
@State private var showingDeleteAlert = false
|
||||
@State private var showingShareSheet = false
|
||||
@State private var shareFileURL: URL?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
@@ -25,6 +27,10 @@ struct ContractorDetailView: View {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
if let contractor = viewModel.selectedContractor {
|
||||
Menu {
|
||||
Button(action: { shareContractor(contractor) }) {
|
||||
Label(L10n.Common.share, systemImage: "square.and.arrow.up")
|
||||
}
|
||||
|
||||
Button(action: { viewModel.toggleFavorite(id: contractorId) { _ in
|
||||
viewModel.loadContractorDetail(id: contractorId)
|
||||
}}) {
|
||||
@@ -50,6 +56,11 @@ struct ContractorDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingShareSheet) {
|
||||
if let url = shareFileURL {
|
||||
ShareSheet(activityItems: [url])
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingEditSheet) {
|
||||
ContractorFormSheet(
|
||||
contractor: viewModel.selectedContractor,
|
||||
@@ -88,6 +99,13 @@ struct ContractorDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func shareContractor(_ contractor: Contractor) {
|
||||
if let url = ContractorSharingManager.shared.createShareableFile(contractor: contractor) {
|
||||
shareFileURL = url
|
||||
showingShareSheet = true
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Content State View
|
||||
|
||||
@ViewBuilder
|
||||
|
||||
240
iosApp/iosApp/Contractor/ContractorSharingManager.swift
Normal file
240
iosApp/iosApp/Contractor/ContractorSharingManager.swift
Normal file
@@ -0,0 +1,240 @@
|
||||
import Foundation
|
||||
import ComposeApp
|
||||
|
||||
/// Manages contractor export and import via .casera files.
|
||||
/// Singleton that handles file creation for sharing and parsing for import.
|
||||
@MainActor
|
||||
class ContractorSharingManager: ObservableObject {
|
||||
|
||||
// MARK: - Singleton
|
||||
|
||||
static let shared = ContractorSharingManager()
|
||||
|
||||
// MARK: - Published Properties
|
||||
|
||||
@Published var isImporting: Bool = false
|
||||
@Published var importError: String?
|
||||
@Published var importSuccess: Bool = false
|
||||
@Published var importedContractorName: String?
|
||||
|
||||
// MARK: - Private Properties
|
||||
|
||||
private let jsonEncoder: JSONEncoder = {
|
||||
let encoder = JSONEncoder()
|
||||
encoder.outputFormatting = .prettyPrinted
|
||||
return encoder
|
||||
}()
|
||||
|
||||
private let jsonDecoder = JSONDecoder()
|
||||
|
||||
private init() {}
|
||||
|
||||
// MARK: - Export
|
||||
|
||||
/// Creates a shareable .casera file for a contractor.
|
||||
/// - Parameter contractor: The contractor to export
|
||||
/// - Returns: URL to the temporary file, or nil if creation failed
|
||||
func createShareableFile(contractor: Contractor) -> URL? {
|
||||
// Get current username for export metadata
|
||||
let currentUsername = DataManagerObservable.shared.currentUser?.username ?? "Unknown"
|
||||
|
||||
// Convert Contractor to SharedContractor using Kotlin extension
|
||||
let sharedContractor = contractor.toSharedContractor(exportedBy: currentUsername)
|
||||
|
||||
// Create Swift-compatible structure for JSON encoding
|
||||
let exportData = SharedContractorExport(from: sharedContractor)
|
||||
|
||||
guard let jsonData = try? jsonEncoder.encode(exportData) else {
|
||||
print("ContractorSharingManager: Failed to encode contractor to JSON")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create a safe filename
|
||||
let safeName = contractor.name
|
||||
.replacingOccurrences(of: " ", with: "_")
|
||||
.replacingOccurrences(of: "/", with: "-")
|
||||
.prefix(50)
|
||||
let fileName = "\(safeName).casera"
|
||||
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(fileName)
|
||||
|
||||
do {
|
||||
try jsonData.write(to: tempURL)
|
||||
return tempURL
|
||||
} catch {
|
||||
print("ContractorSharingManager: Failed to write .casera file: \(error)")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Import
|
||||
|
||||
/// Imports a contractor from a .casera file URL.
|
||||
/// - Parameters:
|
||||
/// - url: The URL to the .casera file
|
||||
/// - completion: Called with true on success, false on failure
|
||||
func importContractor(from url: URL, completion: @escaping (Bool) -> Void) {
|
||||
isImporting = true
|
||||
importError = nil
|
||||
|
||||
// Verify user is authenticated
|
||||
guard TokenStorage.shared.getToken() != nil else {
|
||||
importError = "You must be logged in to import a contractor"
|
||||
isImporting = false
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
// Start accessing security-scoped resource if needed (for files from Files app)
|
||||
let accessing = url.startAccessingSecurityScopedResource()
|
||||
defer {
|
||||
if accessing {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: url)
|
||||
let exportData = try jsonDecoder.decode(SharedContractorExport.self, from: data)
|
||||
|
||||
// Resolve specialty names to IDs
|
||||
let specialties = DataManagerObservable.shared.contractorSpecialties
|
||||
let specialtyIds = exportData.resolveSpecialtyIds(availableSpecialties: specialties)
|
||||
|
||||
// Create the request
|
||||
let createRequest = exportData.toCreateRequest(specialtyIds: specialtyIds)
|
||||
|
||||
// Call API to create contractor
|
||||
Task {
|
||||
do {
|
||||
let result = try await APILayer.shared.createContractor(request: createRequest)
|
||||
|
||||
if let success = result as? ApiResultSuccess<Contractor>,
|
||||
let contractor = success.data {
|
||||
self.importedContractorName = contractor.name
|
||||
self.importSuccess = true
|
||||
self.isImporting = false
|
||||
completion(true)
|
||||
} else if let error = result as? ApiResultError {
|
||||
self.importError = ErrorMessageParser.parse(error.message)
|
||||
self.isImporting = false
|
||||
completion(false)
|
||||
} else {
|
||||
self.importError = "Unknown error occurred"
|
||||
self.isImporting = false
|
||||
completion(false)
|
||||
}
|
||||
} catch {
|
||||
self.importError = error.localizedDescription
|
||||
self.isImporting = false
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
importError = "Failed to read contractor file: \(error.localizedDescription)"
|
||||
isImporting = false
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Resets the import state after showing success/error feedback
|
||||
func resetImportState() {
|
||||
importError = nil
|
||||
importSuccess = false
|
||||
importedContractorName = nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Swift Codable Structure
|
||||
|
||||
/// Swift-native Codable structure for .casera file format.
|
||||
/// This mirrors the Kotlin SharedContractor model for JSON serialization.
|
||||
struct SharedContractorExport: Codable {
|
||||
let version: Int
|
||||
let name: String
|
||||
let company: String?
|
||||
let phone: String?
|
||||
let email: String?
|
||||
let website: String?
|
||||
let notes: String?
|
||||
let streetAddress: String?
|
||||
let city: String?
|
||||
let stateProvince: String?
|
||||
let postalCode: String?
|
||||
let specialtyNames: [String]
|
||||
let rating: Double?
|
||||
let isFavorite: Bool
|
||||
let exportedAt: String?
|
||||
let exportedBy: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case version
|
||||
case name
|
||||
case company
|
||||
case phone
|
||||
case email
|
||||
case website
|
||||
case notes
|
||||
case streetAddress = "street_address"
|
||||
case city
|
||||
case stateProvince = "state_province"
|
||||
case postalCode = "postal_code"
|
||||
case specialtyNames = "specialty_names"
|
||||
case rating
|
||||
case isFavorite = "is_favorite"
|
||||
case exportedAt = "exported_at"
|
||||
case exportedBy = "exported_by"
|
||||
}
|
||||
|
||||
/// Initialize from Kotlin SharedContractor
|
||||
init(from sharedContractor: SharedContractor) {
|
||||
self.version = Int(sharedContractor.version)
|
||||
self.name = sharedContractor.name
|
||||
self.company = sharedContractor.company
|
||||
self.phone = sharedContractor.phone
|
||||
self.email = sharedContractor.email
|
||||
self.website = sharedContractor.website
|
||||
self.notes = sharedContractor.notes
|
||||
self.streetAddress = sharedContractor.streetAddress
|
||||
self.city = sharedContractor.city
|
||||
self.stateProvince = sharedContractor.stateProvince
|
||||
self.postalCode = sharedContractor.postalCode
|
||||
self.specialtyNames = sharedContractor.specialtyNames
|
||||
self.rating = sharedContractor.rating?.doubleValue
|
||||
self.isFavorite = sharedContractor.isFavorite
|
||||
self.exportedAt = sharedContractor.exportedAt
|
||||
self.exportedBy = sharedContractor.exportedBy
|
||||
}
|
||||
|
||||
/// Resolve specialty names to IDs using available specialties
|
||||
func resolveSpecialtyIds(availableSpecialties: [ContractorSpecialty]) -> [Int32] {
|
||||
return specialtyNames.compactMap { name in
|
||||
availableSpecialties.first { specialty in
|
||||
specialty.name.lowercased() == name.lowercased()
|
||||
}?.id
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert to ContractorCreateRequest for API call
|
||||
func toCreateRequest(specialtyIds: [Int32]) -> ContractorCreateRequest {
|
||||
let residenceIdValue: KotlinInt? = nil
|
||||
let ratingValue: KotlinDouble? = rating.map { KotlinDouble(double: $0) }
|
||||
let specialtyIdsValue: [KotlinInt]? = specialtyIds.isEmpty ? nil : specialtyIds.map { KotlinInt(int: $0) }
|
||||
|
||||
return ContractorCreateRequest(
|
||||
name: name,
|
||||
residenceId: residenceIdValue,
|
||||
company: company,
|
||||
phone: phone,
|
||||
email: email,
|
||||
website: website,
|
||||
streetAddress: streetAddress,
|
||||
city: city,
|
||||
stateProvince: stateProvince,
|
||||
postalCode: postalCode,
|
||||
rating: ratingValue,
|
||||
isFavorite: isFavorite,
|
||||
notes: notes,
|
||||
specialtyIds: specialtyIdsValue
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ struct AccessibilityIdentifiers {
|
||||
static let documentsTab = "TabBar.Documents"
|
||||
static let profileTab = "TabBar.Profile"
|
||||
static let backButton = "Navigation.BackButton"
|
||||
static let settingsButton = "Navigation.SettingsButton"
|
||||
}
|
||||
|
||||
// MARK: - Residence
|
||||
|
||||
@@ -582,6 +582,8 @@ enum L10n {
|
||||
static var yes: String { String(localized: "common_yes") }
|
||||
static var no: String { String(localized: "common_no") }
|
||||
static var ok: String { String(localized: "common_ok") }
|
||||
static var share: String { String(localized: "common_share") }
|
||||
static var `import`: String { String(localized: "common_import") }
|
||||
}
|
||||
|
||||
// MARK: - Errors
|
||||
|
||||
@@ -39,5 +39,43 @@
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Casera Contractor</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>com.casera.contractor</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>com.casera.contractor</string>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Casera Contractor</string>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>casera</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<string>application/json</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"%@ has been added to your contacts." : {
|
||||
"comment" : "A message displayed when a contractor is successfully imported to the user's contacts. The placeholder is replaced with the name of the imported contractor.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"%@, %@" : {
|
||||
"comment" : "A city and state combination.",
|
||||
"isCommentAutoGenerated" : true,
|
||||
@@ -4747,6 +4751,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_import" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Import"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_loading" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -5072,6 +5087,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_share" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Share"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"common_success" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -5229,6 +5255,9 @@
|
||||
},
|
||||
"Continue with Free" : {
|
||||
|
||||
},
|
||||
"Contractor Imported" : {
|
||||
|
||||
},
|
||||
"Contractors" : {
|
||||
"comment" : "A tab label for the contractors section.",
|
||||
@@ -17315,6 +17344,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Import" : {
|
||||
"comment" : "The text on a button that triggers the import action.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Import Contractor" : {
|
||||
"comment" : "The title of an alert dialog that appears when a user attempts to import a contractor.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Import Failed" : {
|
||||
"comment" : "A dialog title when importing a contractor fails.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"In Progress" : {
|
||||
"comment" : "A label displayed next to an image of a play button, indicating that a task is currently in progress.",
|
||||
"isCommentAutoGenerated" : true
|
||||
@@ -17381,6 +17422,10 @@
|
||||
"comment" : "A message displayed when no task templates match a search query.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"OK" : {
|
||||
"comment" : "A button that dismisses the success dialog.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"or" : {
|
||||
|
||||
},
|
||||
@@ -17406,10 +17451,6 @@
|
||||
"comment" : "The title of the \"Pro\" plan in the feature comparison view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Profile" : {
|
||||
"comment" : "A label for the \"Profile\" tab in the main tab view.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"profile_account" : {
|
||||
"extractionState" : "manual",
|
||||
"localizations" : {
|
||||
@@ -29636,6 +29677,10 @@
|
||||
"comment" : "The title of the welcome screen in the preview.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Would you like to import this contractor to your contacts?" : {
|
||||
"comment" : "A message displayed in an alert when a user imports a contractor.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"You now have full access to all Pro features!" : {
|
||||
"comment" : "A message displayed to users after successfully upgrading to the Pro version of the app.",
|
||||
"isCommentAutoGenerated" : true
|
||||
|
||||
0
iosApp/iosApp/Localizable.xcstrings.backup
Normal file
0
iosApp/iosApp/Localizable.xcstrings.backup
Normal file
@@ -47,16 +47,6 @@ struct MainTabView: View {
|
||||
}
|
||||
.tag(3)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.documentsTab)
|
||||
|
||||
NavigationView {
|
||||
ProfileTabView()
|
||||
}
|
||||
.id(refreshID)
|
||||
.tabItem {
|
||||
Label("Profile", systemImage: "person.fill")
|
||||
}
|
||||
.tag(4)
|
||||
.accessibilityIdentifier(AccessibilityIdentifiers.Navigation.profileTab)
|
||||
}
|
||||
.tint(Color.appPrimary)
|
||||
.onChange(of: authManager.isAuthenticated) { _ in
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -5,8 +5,11 @@ import ComposeApp
|
||||
struct iOSApp: App {
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
@StateObject private var themeManager = ThemeManager.shared
|
||||
@StateObject private var sharingManager = ContractorSharingManager.shared
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@State private var deepLinkResetToken: String?
|
||||
@State private var pendingImportURL: URL?
|
||||
@State private var showImportConfirmation: Bool = false
|
||||
|
||||
init() {
|
||||
// Initialize DataManager with platform-specific managers
|
||||
@@ -33,8 +36,9 @@ struct iOSApp: App {
|
||||
WindowGroup {
|
||||
RootView()
|
||||
.environmentObject(themeManager)
|
||||
.environmentObject(sharingManager)
|
||||
.onOpenURL { url in
|
||||
handleDeepLink(url: url)
|
||||
handleIncomingURL(url: url)
|
||||
}
|
||||
.onChange(of: scenePhase) { newPhase in
|
||||
if newPhase == .active {
|
||||
@@ -42,17 +46,84 @@ struct iOSApp: App {
|
||||
PushNotificationManager.shared.checkAndRegisterDeviceIfNeeded()
|
||||
}
|
||||
}
|
||||
// Import confirmation dialog
|
||||
.alert("Import Contractor", isPresented: $showImportConfirmation) {
|
||||
Button("Import") {
|
||||
if let url = pendingImportURL {
|
||||
sharingManager.importContractor(from: url) { _ in
|
||||
pendingImportURL = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) {
|
||||
pendingImportURL = nil
|
||||
}
|
||||
} message: {
|
||||
Text("Would you like to import this contractor to your contacts?")
|
||||
}
|
||||
// Import success dialog
|
||||
.alert("Contractor Imported", isPresented: $sharingManager.importSuccess) {
|
||||
Button("OK") {
|
||||
sharingManager.resetImportState()
|
||||
}
|
||||
} message: {
|
||||
Text("\(sharingManager.importedContractorName ?? "Contractor") has been added to your contacts.")
|
||||
}
|
||||
// Import error dialog
|
||||
.alert("Import Failed", isPresented: .init(
|
||||
get: { sharingManager.importError != nil },
|
||||
set: { if !$0 { sharingManager.resetImportState() } }
|
||||
)) {
|
||||
Button("OK") {
|
||||
sharingManager.resetImportState()
|
||||
}
|
||||
} message: {
|
||||
Text(sharingManager.importError ?? "An error occurred while importing the contractor.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Deep Link Handling
|
||||
private func handleDeepLink(url: URL) {
|
||||
print("Deep link received: \(url)")
|
||||
// MARK: - URL Handling
|
||||
|
||||
/// Handles all incoming URLs - both deep links and file opens
|
||||
private func handleIncomingURL(url: URL) {
|
||||
print("URL received: \(url)")
|
||||
|
||||
// Handle .casera file imports
|
||||
if url.pathExtension.lowercased() == "casera" {
|
||||
handleContractorImport(url: url)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle casera:// deep links
|
||||
if url.scheme == "casera" {
|
||||
handleDeepLink(url: url)
|
||||
return
|
||||
}
|
||||
|
||||
print("Unrecognized URL: \(url)")
|
||||
}
|
||||
|
||||
/// Handles .casera file imports
|
||||
private func handleContractorImport(url: URL) {
|
||||
print("Contractor file received: \(url)")
|
||||
|
||||
// Check if user is authenticated
|
||||
guard TokenStorage.shared.getToken() != nil else {
|
||||
sharingManager.importError = "You must be logged in to import a contractor"
|
||||
return
|
||||
}
|
||||
|
||||
// Store URL and show confirmation dialog
|
||||
pendingImportURL = url
|
||||
showImportConfirmation = true
|
||||
}
|
||||
|
||||
/// Handles casera:// deep links
|
||||
private func handleDeepLink(url: URL) {
|
||||
// Handle casera://reset-password?token=xxx
|
||||
guard url.scheme == "casera",
|
||||
url.host == "reset-password" else {
|
||||
print("Unrecognized deep link scheme or host")
|
||||
guard url.host == "reset-password" else {
|
||||
print("Unrecognized deep link host: \(url.host ?? "nil")")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user