Harden iOS app: fix concurrency, animations, formatters, privacy, and logging

- Eliminate NumberFormatters shared singleton data race; use local formatters
- Add reduceMotion checks to empty-state animations in 3 list views
- Wrap 68+ print() statements in #if DEBUG across push notification code
- Remove redundant .receive(on: DispatchQueue.main) in SubscriptionCache
- Remove redundant initializeLookups() call from iOSApp.init()
- Clean up StoreKitManager Task capture in listenForTransactions()
- Add memory warning observer to AuthenticatedImage cache
- Cache parseContent result in UpgradePromptView init
- Add DiskSpace and FileTimestamp API declarations to Privacy Manifest
- Add FIXME for analytics debug/production API key separation
- Use static formatter in PropertyHeaderCard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-04 23:15:42 -06:00
parent bf5d60ca63
commit c5f2bee83f
22 changed files with 683 additions and 347 deletions

View File

@@ -9,7 +9,7 @@ struct ResidencesListView: View {
@State private var showingUpgradePrompt = false
@State private var showingSettings = false
@State private var pushTargetResidenceId: Int32?
@State private var navigateToPushResidence = false
@State private var showLoginCover = false
@StateObject private var authManager = AuthenticationManager.shared
@StateObject private var subscriptionCache = SubscriptionCacheWrapper.shared
@Environment(\.scenePhase) private var scenePhase
@@ -100,7 +100,7 @@ struct ResidencesListView: View {
UpgradePromptView(triggerKey: "add_second_property", isPresented: $showingUpgradePrompt)
}
.sheet(isPresented: $showingSettings) {
NavigationView {
NavigationStack {
ProfileTabView()
}
}
@@ -113,6 +113,8 @@ struct ResidencesListView: View {
viewModel.loadMyResidences()
// Also load tasks to populate summary stats
taskViewModel.loadTasks()
} else {
showLoginCover = true
}
}
.onChange(of: scenePhase) { newPhase in
@@ -122,9 +124,10 @@ struct ResidencesListView: View {
taskViewModel.loadTasks(forceRefresh: true)
}
}
.fullScreenCover(isPresented: $authManager.isAuthenticated.negated) {
.fullScreenCover(isPresented: $showLoginCover) {
LoginView(onLoginSuccess: {
authManager.isAuthenticated = true
showLoginCover = false
viewModel.loadMyResidences()
taskViewModel.loadTasks()
})
@@ -133,11 +136,13 @@ struct ResidencesListView: View {
.onChange(of: authManager.isAuthenticated) { isAuth in
if isAuth {
// User just logged in or registered - load their residences and tasks
showLoginCover = false
viewModel.loadMyResidences()
taskViewModel.loadTasks()
} else {
// User logged out - clear data
// User logged out - clear data and show login
viewModel.myResidences = nil
showLoginCover = true
}
}
.onReceive(NotificationCenter.default.publisher(for: .navigateToResidence)) { notification in
@@ -145,26 +150,18 @@ struct ResidencesListView: View {
navigateToResidenceFromPush(residenceId: residenceId)
}
}
.background(
NavigationLink(
destination: Group {
if let residenceId = pushTargetResidenceId {
ResidenceDetailView(residenceId: residenceId)
} else {
EmptyView()
}
},
isActive: $navigateToPushResidence
) {
EmptyView()
.navigationDestination(isPresented: Binding(
get: { pushTargetResidenceId != nil },
set: { if !$0 { pushTargetResidenceId = nil } }
)) {
if let residenceId = pushTargetResidenceId {
ResidenceDetailView(residenceId: residenceId)
}
.hidden()
)
}
}
private func navigateToResidenceFromPush(residenceId: Int) {
pushTargetResidenceId = Int32(residenceId)
navigateToPushResidence = true
PushNotificationManager.shared.pendingNavigationResidenceId = nil
}
}
@@ -271,6 +268,7 @@ private struct OrganicCardButtonStyle: ButtonStyle {
private struct OrganicEmptyResidencesView: View {
@State private var isAnimating = false
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
VStack(spacing: OrganicSpacing.comfortable) {
@@ -295,7 +293,9 @@ private struct OrganicEmptyResidencesView: View {
.frame(width: 160, height: 160)
.scaleEffect(isAnimating ? 1.1 : 1.0)
.animation(
Animation.easeInOut(duration: 3).repeatForever(autoreverses: true),
isAnimating && !reduceMotion
? Animation.easeInOut(duration: 3).repeatForever(autoreverses: true)
: .default,
value: isAnimating
)
@@ -313,7 +313,9 @@ private struct OrganicEmptyResidencesView: View {
.foregroundColor(Color.appPrimary)
.offset(y: isAnimating ? -2 : 2)
.animation(
Animation.easeInOut(duration: 2).repeatForever(autoreverses: true),
isAnimating && !reduceMotion
? Animation.easeInOut(duration: 2).repeatForever(autoreverses: true)
: .default,
value: isAnimating
)
}
@@ -347,20 +349,14 @@ private struct OrganicEmptyResidencesView: View {
.onAppear {
isAnimating = true
}
.onDisappear {
isAnimating = false
}
}
}
#Preview {
NavigationView {
NavigationStack {
ResidencesListView()
}
}
extension Binding where Value == Bool {
var negated: Binding<Bool> {
Binding<Bool>(
get: { !self.wrappedValue },
set: { self.wrappedValue = !$0 }
)
}
}