Files
honeyDueKMP/iosApp/iosApp/Documents/DocumentsWarrantiesView.swift
Trey T af73f8861b iOS VoiceOver accessibility overhaul — 67 files
New framework:
- AccessibilityLabels.swift: centralized A11y struct with VoiceOver strings
- AccessibilityModifiers.swift: reusable .a11yHeader, .a11yDecorative,
  .a11yButton, .a11yCard, .a11yStatValue View extensions

Shared components: decorative elements hidden, stat views combined,
status/priority badges labeled, error views announced, empty states grouped

Cards: ResidenceCard, TaskCard, DynamicTaskCard, ContractorCard,
DocumentCard, WarrantyCard — all grouped with combined labels,
chevrons hidden, action buttons labeled

Main screens: Login, Register, Residences, Tasks, Contractors, Documents —
toolbar buttons labeled, section headers marked, form field hints added

Onboarding: all 10 views — header traits, button hints, task selection
state, progress indicator, decorative backgrounds hidden

Profile/Subscription: toggle hints, theme selection state, feature
comparison table accessibility, subscription button labels

iOS build verified: BUILD SUCCEEDED
2026-03-26 14:51:29 -05:00

420 lines
15 KiB
Swift

import SwiftUI
import ComposeApp
enum DocumentWarrantyTab {
case warranties
case documents
}
struct DocumentsWarrantiesView: View {
@StateObject private var documentViewModel = DocumentViewModel()
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
@State private var selectedTab: DocumentWarrantyTab = .warranties
@State private var searchText = ""
@State private var selectedCategory: String? = nil
@State private var selectedDocType: String? = nil
@State private var showActiveOnly = true
@State private var showFilterMenu = false
@State private var showAddSheet = false
@State private var showingUpgradePrompt = false
@State private var pushTargetDocumentId: Int32?
@State private var navigateToPushDocument = false
let residenceId: Int32?
var warranties: [Document] {
documentViewModel.documents.filter { doc in
guard doc.documentType == "warranty" else { return false }
if showActiveOnly && doc.isActive != true {
return false
}
if let category = selectedCategory, doc.category?.lowercased() != category.lowercased() {
return false
}
return true
}
}
var documents: [Document] {
documentViewModel.documents.filter { doc in
guard doc.documentType != "warranty" else { return false }
if let docType = selectedDocType, doc.documentType.lowercased() != docType.lowercased() {
return false
}
return true
}
}
var body: some View {
ZStack {
WarmGradientBackground()
VStack(spacing: 0) {
// Segmented Control
OrganicSegmentedControl(selection: $selectedTab)
.padding(.horizontal, 16)
.padding(.top, 8)
// Search Bar
OrganicDocSearchBar(text: $searchText, placeholder: L10n.Documents.searchPlaceholder)
.padding(.horizontal, 16)
.padding(.top, 8)
// Active Filters
if selectedCategory != nil || selectedDocType != nil || (selectedTab == .warranties && showActiveOnly) {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
if selectedTab == .warranties && showActiveOnly {
OrganicDocFilterChip(
title: L10n.Documents.activeOnly,
icon: "checkmark.circle.fill",
onRemove: { showActiveOnly = false }
)
}
if let category = selectedCategory, selectedTab == .warranties {
OrganicDocFilterChip(
title: category,
onRemove: { selectedCategory = nil }
)
}
if let docType = selectedDocType, selectedTab == .documents {
OrganicDocFilterChip(
title: docType,
onRemove: { selectedDocType = nil }
)
}
}
.padding(.horizontal, 16)
}
.padding(.vertical, 8)
}
// Content
if selectedTab == .warranties {
WarrantiesTabContent(
viewModel: documentViewModel,
searchText: searchText
)
} else {
DocumentsTabContent(
viewModel: documentViewModel,
searchText: searchText
)
}
}
}
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
HStack(spacing: 12) {
// Active Filter (for warranties)
if selectedTab == .warranties {
Button(action: {
showActiveOnly.toggle()
}) {
Image(systemName: showActiveOnly ? "checkmark.circle.fill" : "checkmark.circle")
.font(.system(size: 16, weight: .medium))
.foregroundColor(showActiveOnly ? Color.appPrimary : Color.appTextSecondary)
}
.accessibilityLabel(showActiveOnly ? "Show all warranties" : "Show active warranties only")
}
// Filter Menu
Menu {
if selectedTab == .warranties {
Button(action: {
selectedCategory = nil
}) {
Label(L10n.Documents.allCategories, systemImage: selectedCategory == nil ? "checkmark" : "")
}
Divider()
ForEach(DocumentCategory.allCases, id: \.self) { category in
Button(action: {
selectedCategory = category.displayName
}) {
Label(category.displayName, systemImage: selectedCategory == category.displayName ? "checkmark" : "")
}
}
} else {
Button(action: {
selectedDocType = nil
}) {
Label(L10n.Documents.allTypes, systemImage: selectedDocType == nil ? "checkmark" : "")
}
Divider()
ForEach(DocumentType.allCases, id: \.self) { type in
Button(action: {
selectedDocType = type.displayName
}) {
Label(type.displayName, systemImage: selectedDocType == type.displayName ? "checkmark" : "")
}
}
}
} label: {
Image(systemName: "line.3.horizontal.decrease.circle")
.font(.system(size: 16, weight: .medium))
.foregroundColor((selectedCategory != nil || selectedDocType != nil) ? Color.appPrimary : Color.appTextSecondary)
}
.accessibilityLabel("Filter documents")
// Add Button
Button(action: {
let currentCount = documentViewModel.documents.count
if subscriptionCache.shouldShowUpgradePrompt(currentCount: currentCount, limitKey: "documents") {
AnalyticsManager.shared.track(.documentsPaywallShown(currentCount: currentCount))
showingUpgradePrompt = true
} else {
showAddSheet = true
}
}) {
OrganicDocToolbarButton()
}
.accessibilityIdentifier(AccessibilityIdentifiers.Document.addButton)
.accessibilityLabel("Add document")
}
}
}
.onAppear {
AnalyticsManager.shared.trackScreen(.documents)
if let pendingDocumentId = PushNotificationManager.shared.pendingNavigationDocumentId {
navigateToDocumentFromPush(documentId: pendingDocumentId)
}
loadAllDocuments()
}
.sheet(isPresented: $showAddSheet, onDismiss: {
documentViewModel.loadDocuments(forceRefresh: true)
}) {
AddDocumentView(
residenceId: residenceId,
initialDocumentType: selectedTab == .warranties ? "warranty" : "general",
isPresented: $showAddSheet,
documentViewModel: documentViewModel
)
}
.sheet(isPresented: $showingUpgradePrompt) {
UpgradePromptView(triggerKey: "view_documents", isPresented: $showingUpgradePrompt)
}
.onReceive(NotificationCenter.default.publisher(for: .navigateToDocument)) { notification in
if let documentId = notification.userInfo?["documentId"] as? Int {
navigateToDocumentFromPush(documentId: documentId)
} else {
selectedTab = .warranties
}
}
.navigationDestination(isPresented: $navigateToPushDocument) {
if let documentId = pushTargetDocumentId {
DocumentDetailView(documentId: documentId)
} else {
Color.clear.onAppear { navigateToPushDocument = false }
}
}
}
private func loadAllDocuments(forceRefresh: Bool = false) {
documentViewModel.loadDocuments(forceRefresh: forceRefresh)
}
private func loadWarranties() {
loadAllDocuments()
}
private func loadDocuments() {
loadAllDocuments()
}
private func navigateToDocumentFromPush(documentId: Int) {
// Look up the document to determine the correct tab
if let document = documentViewModel.documents.first(where: { $0.id?.int32Value == Int32(documentId) }) {
selectedTab = document.documentType == "warranty" ? .warranties : .documents
} else {
// Default to warranties if document not found in cache
selectedTab = .warranties
}
pushTargetDocumentId = Int32(documentId)
navigateToPushDocument = true
PushNotificationManager.shared.pendingNavigationDocumentId = nil
}
}
// MARK: - Organic Segmented Control
private struct OrganicSegmentedControl: View {
@Binding var selection: DocumentWarrantyTab
var body: some View {
HStack(spacing: 0) {
OrganicSegmentButton(
title: L10n.Documents.warranties,
icon: "checkmark.shield",
isSelected: selection == .warranties,
action: { selection = .warranties }
)
OrganicSegmentButton(
title: L10n.Documents.documents,
icon: "doc.text",
isSelected: selection == .documents,
action: { selection = .documents }
)
}
.padding(4)
.background(Color.appBackgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 14, style: .continuous))
.naturalShadow(.subtle)
}
}
private struct OrganicSegmentButton: View {
let title: String
let icon: String
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 13, weight: .semibold))
Text(title)
.font(.system(size: 14, weight: .semibold))
}
.foregroundColor(isSelected ? Color.appTextOnPrimary : Color.appTextSecondary)
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
.background(isSelected ? Color.appPrimary : Color.clear)
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
}
.accessibilityLabel(title)
.accessibilityAddTraits(isSelected ? .isSelected : [])
}
}
// MARK: - Organic Doc Search Bar
private struct OrganicDocSearchBar: View {
@Binding var text: String
var placeholder: String
var body: some View {
HStack(spacing: 12) {
ZStack {
Circle()
.fill(Color.appPrimary.opacity(0.1))
.frame(width: 32, height: 32)
Image(systemName: "magnifyingglass")
.font(.system(size: 14, weight: .medium))
.foregroundColor(Color.appPrimary)
}
TextField(placeholder, text: $text)
.font(.system(size: 16, weight: .medium))
if !text.isEmpty {
Button(action: { text = "" }) {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 18))
.foregroundColor(Color.appTextSecondary)
}
}
}
.padding(14)
.background(Color.appBackgroundSecondary)
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
.naturalShadow(.subtle)
}
}
// MARK: - Organic Doc Filter Chip
private struct OrganicDocFilterChip: View {
let title: String
var icon: String? = nil
let onRemove: () -> Void
var body: some View {
HStack(spacing: 6) {
if let icon = icon {
Image(systemName: icon)
.font(.system(size: 12, weight: .semibold))
}
Text(title)
.font(.system(size: 13, weight: .semibold))
Button(action: onRemove) {
Image(systemName: "xmark")
.font(.system(size: 10, weight: .bold))
}
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
.background(Color.appPrimary.opacity(0.15))
.foregroundColor(Color.appPrimary)
.clipShape(Capsule())
}
}
// MARK: - Organic Doc Toolbar Button
private struct OrganicDocToolbarButton: View {
var body: some View {
ZStack {
Circle()
.fill(Color.appPrimary)
.frame(width: 32, height: 32)
Image(systemName: "plus")
.font(.system(size: 14, weight: .bold))
.foregroundColor(Color.appTextOnPrimary)
}
}
}
// MARK: - Supporting Types
extension DocumentCategory: CaseIterable {
public static var allCases: [DocumentCategory] {
return [.appliance, .hvac, .plumbing, .electrical, .roofing, .structural, .other]
}
var displayName: String {
switch self {
case .appliance: return "Appliance"
case .hvac: return "HVAC"
case .plumbing: return "Plumbing"
case .electrical: return "Electrical"
case .roofing: return "Roofing"
case .structural: return "Structural"
case .other: return "Other"
default: return "Unknown"
}
}
}
extension DocumentType: CaseIterable {
public static var allCases: [DocumentType] {
return [.warranty, .manual, .receipt, .inspection, .permit, .deed, .insurance, .contract, .photo, .other]
}
var displayName: String {
switch self {
case .warranty: return "Warranty"
case .manual: return "Manual"
case .receipt: return "Receipt"
case .inspection: return "Inspection"
case .permit: return "Permit"
case .deed: return "Deed"
case .insurance: return "Insurance"
case .contract: return "Contract"
case .photo: return "Photo"
case .other: return "Other"
default: return "Unknown"
}
}
}