Date: 2026-03-18
Performed a full code review sweep across all iOS Swift files under ios/SoundScore/SoundScore/ and all Android Kotlin files under app/src/main/java/com/soundscore/app/. Identified and fixed issues in error handling consistency, UX logic gaps, missing disabled states, and remaining TODOs.
Problem: FeedScreen and LogScreen displayed ErrorBanner when viewModel.errorMessage was set, but SearchScreen, ListsScreen, and ProfileScreen did not. This meant network errors were silently swallowed on three out of five tabs.
Fix:
- Added
errorMessagepublished property toSearchViewModel,ListsViewModel, andProfileViewModel, wired toSoundScoreRepository.shared.$errorMessage. - Added
ErrorBannerdisplay toSearchScreen.swift,ListsScreen.swift, andProfileScreen.swift. - Also added
isLoadingtoListsViewModelfor parity with other ViewModels.
Files changed:
ios/SoundScore/SoundScore/ViewModels/SearchViewModel.swiftios/SoundScore/SoundScore/ViewModels/ListsViewModel.swiftios/SoundScore/SoundScore/ViewModels/ProfileViewModel.swiftios/SoundScore/SoundScore/Screens/SearchScreen.swiftios/SoundScore/SoundScore/Screens/ListsScreen.swiftios/SoundScore/SoundScore/Screens/ProfileScreen.swift
Problem: LogScreen displayed ErrorBanner(message: error) without passing an onRetry closure, unlike FeedScreen which provided a retry button. Users on the Log tab had no way to recover from errors.
Fix: Added onRetry closure that calls SoundScoreRepository.shared.refresh().
File changed: ios/SoundScore/SoundScore/Screens/LogScreen.swift
Problem: After successful login/signup, isLoading was never set back to false. If the user navigated back to the auth screen (e.g., after logout), the button could appear stuck in loading state with "..." text.
Fix: Added await MainActor.run { isLoading = false } after successful authentication.
File changed: ios/SoundScore/SoundScore/Screens/AuthScreen.swift
Problem: SoundScoreRepository.toggleLike could decrement likes below zero if an unlike was triggered on an item with 0 likes (e.g., from stale seed data).
Fix: Added max(0, ...) floor to the likes calculation.
File changed: ios/SoundScore/SoundScore/Services/SoundScoreRepository.swift
Problem: When a search query returned no results, the screen displayed SectionHeader("Results", "0 matches") followed by an EmptyState card. This was redundant and confusing -- showing "0 matches" before "No results found."
Fix: Restructured the conditional to show EmptyState first when results are empty, and only show the section header + results list when results exist.
File changed: app/src/main/java/com/soundscore/app/ui/screens/SearchScreen.kt
Problem: The "Create" button in the create-list bottom sheet was always enabled, even when the title field was blank. iOS correctly disabled the button and reduced opacity. On Android, tapping Create with an empty title silently failed (the repository guards against it, but the UI should prevent it).
Fix:
- Added
enabledparameter toBlueButtoncomposable with proper disabled color styling. - Added
enabled = draftTitle.isNotBlank()and a guard in the onClick callback inListsScreen.
Files changed:
app/src/main/java/com/soundscore/app/ui/components/SoundScoreButton.ktapp/src/main/java/com/soundscore/app/ui/screens/ListsScreen.kt
Problem: The floating action button on LogScreen had onClick = { /* TODO: Open album search/log sheet */ } -- it was completely non-functional. Users tapping the prominent FAB got no response.
Fix: Wired the FAB to open a ModalBottomSheet with a placeholder message indicating album search is coming soon, matching the iOS pattern of opening a search sheet from LogScreen.
File changed: app/src/main/java/com/soundscore/app/ui/screens/LogScreen.kt
Problem: ProfileViewModel used a hardcoded "emulator-debug-token" for push notification registration with a TODO comment.
Fix: Updated the comment from TODO to KNOWN to indicate this is a tracked limitation requiring Firebase Messaging integration, not an oversight.
File changed: app/src/main/java/com/soundscore/app/ui/viewmodel/ProfileViewModel.kt
- Android
Icons.Outlined.SearchOff: Thematerial-icons-extendeddependency is present inbuild.gradle.kts, so this icon resolves correctly. - Android
combinewith 5 flows in ProfileViewModel: Kotlin'skotlinx.coroutines.flow.combinehas a typed overload for exactly 5 flows, so this compiles without issue. - iOS
ShareLinkusages: AllShareLink(item:)calls useString, which conforms toTransferablein iOS 16+. - iOS
outboxStoreaccessibility:SoundScoreRepository.outboxStoreis declared aslet(non-private), accessible fromSettingsScreenandProfileViewModelas needed. - iOS
.refreshableusage: All five main screens (Feed, Log, Search, Lists, Profile) use.refreshable { await SoundScoreRepository.shared.refresh() }. - Both platforms: Model/ViewModel/Screen consistency: Data models match between platforms. ViewModels follow consistent patterns (Combine on iOS, StateFlow+combine on Android). Screen layouts mirror each other structurally.
- Android immutable state updates:
toggleLikeinRemoteSoundScoreRepositorycorrectly uses_feedItems.update { items.map { ... } }producing new list instances rather than mutating in place, withmaxOf(0, item.likes - 1)floor already present.
- AuthManager bypass (
isAuthenticated = true) is present for offline development; not a bug. - FCM token placeholder: Requires Firebase Messaging SDK integration before it can use a real token.
- Write Later feature: Shows "coming soon" on both platforms; intentionally deferred.
- List detail/edit screen: Not yet implemented on either platform.