Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ private struct ContentView: View {
}
.scrollIndicators(.hidden)
.animation(.easeIn(duration: animationDuration),
value: viewModel.stationItems.values.reduce(into: 0) { $0 = $0 + ($1.isExpanded ? 1 : 0) })
value: viewModel.stationItems.values.reduce(into: 0) { $0 += ($1.isExpanded ? 1 : 0) })
}
}

Expand Down Expand Up @@ -235,7 +235,7 @@ private struct ContentView: View {
}

if isExpanded, let stationItem {
expandedView(for: deviceId , stationItem: stationItem)
expandedView(for: deviceId, stationItem: stationItem)
}
}
.WXMCardStyle()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@ private extension OverviewView {
}
.cornerRadius(CGFloat(.cardCornerRadius))
}
.wxmShadow()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ class OverviewViewModel: ObservableObject {
@Published private(set) var ctaObject: CTAContainerView.CTAObject?
@Published private(set) var showNoDataInfo: Bool = false
@Published var showStationHealthInfo: Bool = false
// Keep a ref on the created view model in order to keep it in memory as long as the screen is visible.
// We do this for caching purposes.
private var stationSupportViewModel: StationSupportViewModel?

var weatherNoDataText: LocalizableString {
followState.weatherNoDataText
Expand Down Expand Up @@ -91,8 +94,9 @@ class OverviewViewModel: ObservableObject {
guard let name = device?.name else {
return
}
let vm = ViewModelsFactory.getStationSupportViewModel(deviceName: name)
let vm = stationSupportViewModel ?? ViewModelsFactory.getStationSupportViewModel(deviceName: name)
Router.shared.showBottomSheet(.stationSupport(vm), bgColor: .top)
stationSupportViewModel = vm
Comment on lines +97 to +99
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Major issue: Cached view model is reused without validating its state or matching device.

The caching logic has two critical flaws:

  1. Reuses error states: If the cached VM entered .error mode, tapping the AI button again shows the same error. Since refresh() is now private (StationSupportViewModel.swift line 27), there's no way to retry the fetch. The user is stuck with the error until they navigate away from the station details entirely.

  2. Doesn't validate device match: If the device changes via refreshWithDevice (line 171), the cached VM may contain data for a different station, leading to incorrect information being displayed.

Apply this diff to validate state and device before reusing:

 func handleAISupportTap() {
     guard let name = device?.name else {
         return
     }
-    let vm = stationSupportViewModel ?? ViewModelsFactory.getStationSupportViewModel(deviceName: name)
+    
+    // Validate cached VM is for the same device and not in error state
+    let vm: StationSupportViewModel
+    if let cached = stationSupportViewModel,
+       case .error = cached.mode {
+        // Don't reuse if in error state - create fresh VM to retry
+        vm = ViewModelsFactory.getStationSupportViewModel(deviceName: name)
+    } else {
+        vm = stationSupportViewModel ?? ViewModelsFactory.getStationSupportViewModel(deviceName: name)
+    }
+    
     Router.shared.showBottomSheet(.stationSupport(vm), bgColor: .top)
     stationSupportViewModel = vm
 }

Additionally, clear the cache in refreshWithDevice when the device changes:

@MainActor
func refreshWithDevice(_ device: DeviceDetails?, followState: UserDeviceFollowState?, error: NetworkErrorResponse?) async {
+   // Clear cache if device changes to prevent showing wrong station's data
+   if self.device?.name != device?.name {
+       stationSupportViewModel = nil
+   }
+   
    self.device = device
    // ... rest of method
}
🤖 Prompt for AI Agents
In
PresentationLayer/UIComponents/Screens/WeatherStations/StationDetails/Overview/OverviewViewModel.swift
around lines 97-99, the code reuses a cached stationSupportViewModel
unconditionally which can reuse an .error state or a VM tied to a different
device; update the logic to validate the cached VM before reuse by ensuring its
device identity matches the current station name and its state is not an error
(create a fresh ViewModel via
ViewModelsFactory.getStationSupportViewModel(deviceName: name) when the cached
VM fails these checks), then assign and present that VM; additionally, in
refreshWithDevice (around line 171) clear stationSupportViewModel (set to nil)
whenever the incoming device name differs from the existing one to avoid showing
another station’s data.

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ struct StationDetailsContainerView: View {
.frame(width: 30.0, height: 30.0)
}
.wxmPopOver(show: $showSettingsPopOver) {
VStack(alignment:.leading, spacing: CGFloat(.mediumSpacing)) {
VStack(alignment: .leading, spacing: CGFloat(.mediumSpacing)) {
Button { [weak viewModel] in
showSettingsPopOver = false
viewModel?.handleShareButtonTap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ struct StationSupportView: View {
.padding()
}
}
.task {
viewModel.refresh()
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ class StationSupportViewModel: ObservableObject {
init(stationName: String, useCase: MeUseCaseApi) {
self.stationName = stationName
self.useCase = useCase

refresh()
}

func refresh() {
Expand Down
4 changes: 2 additions & 2 deletions wxm-ios.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -4965,7 +4965,7 @@
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "wxm-ios/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "WXM Dev";
INFOPLIST_KEY_CFBundleDisplayName = "WXM Mock";
INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
Expand Down Expand Up @@ -5192,7 +5192,7 @@
);
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "wxm-ios/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "WXM Dev";
INFOPLIST_KEY_CFBundleDisplayName = WeatherXM;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Release display name → WeatherXM — LGTM.

Branding for production looks correct. Ensure there’s no conflicting CFBundleDisplayName in Info.plist at build time.


🏁 Script executed:

#!/bin/bash
#!/bin/bash
# Double-check effective names across configs (Debug/Mock/Release)
rg -n 'INFOPLIST_KEY_CFBundleDisplayName' wxm-ios.xcodeproj/project.pbxproj -C1

Length of output: 1545


Fix missing quotes around WeatherXM at line 5195 for consistency.

The Release configuration is missing quotes that are present in Debug ("WXM Dev") and Mock ("WXM Mock") configurations. Line 5195 should be:

INFOPLIST_KEY_CFBundleDisplayName = "WeatherXM";

Without quotes, the value may be incorrectly parsed as an identifier rather than a string literal, causing potential build or runtime issues.

🤖 Prompt for AI Agents
In wxm-ios.xcodeproj/project.pbxproj around line 5195, the
INFOPLIST_KEY_CFBundleDisplayName value for the Release configuration is missing
quotes; update the line to wrap WeatherXM in double quotes so it reads as a
string literal (matching Debug/Mock entries) to avoid parsing issues and ensure
consistent formatting.

INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO;
INFOPLIST_KEY_LSApplicationCategoryType = "";
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
Expand Down
Loading