Repository navigation
feat: riverpod migration - #1843
Nirajn2311 wants to merge 37 commits into
Conversation
99a23d6 to
23ba0f9
Compare
|
Every scenario across the 3 sections have been tested in both Android and iOS simulators so those sections should be good for a review |
There was a problem hiding this comment.
We should use this image instead (no underscore below the emblem): https://cdn.freecodecamp.org/platform/universal/fcc_banner_new.png
There was a problem hiding this comment.
I think we could remove these Flutter icons?
- mobile-app-new/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
- mobile-app-new/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
- mobile-app-new/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
- mobile-app-new/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
- mobile-app-new/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
There was a problem hiding this comment.
For this one, we can grab the SVG from https://github.com/freeCodeCamp/freeCodeCamp/blob/main/client/src/assets/icons/default-avatar.tsx.
The new default image looks like this:
(Sorry, I know this is an existing issue, but I didn't catch it until now 😅 )
There was a problem hiding this comment.
Could we remove these Flutter icons?
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
- mobile-app-new/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
|
freeCodeCamp mobile PR #1843 review: Stacked to Riverpod PR: #1843 Reviewed commit: Scope: News, Podcasts, Code Radio, shared audio, persistence, and provider lifecycles in Severity: P1 means a high-priority issue to resolve before shipping the replacement app because existing user data is at risk. P2 means a functional correctness issue requiring a fix. Validation: Eight findings were exercised with focused tests using the application code and mocked platform or network boundaries. The Code Radio notification Stop/resume finding was verified by tracing the application and installed Finding 1 (P1): Import SQLite data before treating missing JSON as an empty library Locations: podcasts/repositories/storage_repository.dart:148–150, news/repositories/bookmark_repository.dart:37–39, and podcasts/services/library_service.dart:101–110. Problem: The new repositories read only the JSON stores. If an existing user still has SQLite-only bookmarks or downloads, the new app presents an empty library. The podcast cleanup path can subsequently delete that user's downloaded audio. Reproduction: Start with a legacy Cause: Both repositories treat absent JSON as no usable records without importing the old database. The podcast sweep correctly skips cleanup while the store is missing or unreadable, but after the first new download the incomplete JSON becomes readable. Cleanup then mistakes legacy audio for orphaned files. Impact: Existing bookmarks become inaccessible, and existing podcast audio can be permanently deleted. The bookmark SQLite file itself is not deleted by the reproduced path. Installing the intermediate JSON-storage release is not sufficient for every user: bookmark migration in the old app is lazy, so someone who never opened the relevant UI can still have SQLite-only data. Suggested fix: Preserve the legacy import path, make it idempotent, and merge legacy entries safely if JSON already exists. Gate orphan cleanup on completed migration, not merely on readable JSON. Test a direct SQLite upgrade, an intermediate release whose lazy migration never ran, and a mixed SQLite/JSON state. Validation: Real legacy database fixtures and the actual new repositories/services were used with isolated storage directories. The podcast fixture lost A's audio after creating B's JSON entry and running the actual sweep. The bookmark fixture returned an empty list and then wrote only the new bookmark. Controls confirmed existing v7 JSON bookmarks remain readable and legacy podcast JSON can migrate while retaining records. Applicability: This is an upgrade blocker when the new implementation replaces the installed app and inherits its storage. A clean installation of the separate scaffold does not exercise that path. Finding 2 (P2): Keep download dependencies alive until transfers and persistence finish Locations: podcasts/controllers/downloads_controller.dart:40–56, podcasts/repositories/storage_repository.dart:51–53, and podcasts/controllers/library_controller.dart:13–17. Problem: The downloads notifier keeps itself alive during a transfer, but it reads the library service without retaining its provider dependency. Leaving Podcasts can dispose the service and storage providers while the original operation still holds their objects. Returning creates another storage repository operating on the same file. Reproduction: Start download A, leave Podcasts so its library subscriptions are removed, return, and start download B before A finishes. Allow both transfers to finish together. A and B can now persist through separate repository instances. A second variant is to start one download, leave the section, and let it finish in the background. Cause: Impact: Concurrent completion can lose a download entry. A successful background transfer can also be reported as Suggested fix: Retain the library service and its storage dependency throughout the operation, and ensure writes to the same store share one serialization mechanism. Refresh or invalidate the UI library in a way that does not await a state update on an unobserved disposable notifier. Do not classify a UI refresh failure as a failed transfer. Validation: Tests used the actual providers, controllers, library service, and file-backed storage with controlled transfer completion. Leaving and returning created two storage repositories; concurrent completion retained only one episode. The single-background-download case persisted the episode but reported failure. A control using one repository preserved both concurrent updates. Finding 3 (P2): Ignore superseded audio loads instead of stopping or relabeling the current source Locations: services/audio_service.dart:229–231 and services/audio_service.dart:179–180. Problem: Multiple source loads share one player, but their completion and error handlers do not check whether they still own it. An older interrupted request can stop or overwrite the state of a newer selection. Reproduction: Start loading podcast episode A, then select B before A finishes buffering. Replacing A's source interrupts A's pending load. A's catch block calls the shared Related reproduction: After loading podcast A, start a slow Code Radio load, then select podcast B before the radio load completes. When the radio request is interrupted, its catch restores the previously captured configuration for A, even though B has become the current media item. Cause: Impact: The latest selected podcast may fail to start. In the radio variant, B's displayed media and the handler's episode identity disagree, so controls and saved progress can refer to A. Suggested fix: Give source changes an operation identity or serialize them with explicit cancellation semantics. Before publishing media, restoring configuration, stopping, or continuing playback after an await, verify that the operation is still current. An interrupted old load must not mutate a newer load's state. Validation: Focused tests exercised the actual audio handler and installed Finding 4 (P2): Do not save the previous episode's position against the next episode Locations: services/audio_service.dart:217–221, podcasts/services/playback_service.dart:114–130, and podcasts/services/playback_service.dart:146–159. Problem: Reproduction: Play A at ten minutes, switch to an unplayed B, and stop B shortly after it starts. Later play B again. B can resume at ten minutes despite having played for less than a second. Cause: Impact: Resume progress is corrupted across episodes. The problem does not require overlapping user-initiated loads; an ordinary sequential switch can trigger it. Suggested fix: Keep each progress event associated with the source that produced it. Suppress persistence during a source transition or publish the new episode identity only after the corresponding source is active. Ensure an old-source event cannot write or clear progress for the new episode. Validation: The actual handler and playback service were exercised with Finding 5 (P2): Reinitialize Code Radio after notification Stop Locations: code_radio/ui/player/player_viewmodel.dart:61, services/audio_service.dart:110–131, services/audio_service.dart:274, and code_radio/services/code_radio_service.dart:66–78. Problem: The in-app toggle treats a stopped radio session as though it were merely paused. Reproduction: Keep the Code Radio screen open, press Stop in Android's media notification, then press Play in the app. Cause: Impact: Audio can restart while the screen continues showing Play. Further taps do not pause it, and normal notification and metadata updates are not restored. Suggested fix: Distinguish paused playback from a stopped or unloaded session. For the latter, restart the station subscription and reload the radio configuration/source before playing. Keep ordinary pause/resume behavior separate from that reinitialization. Validation: Verified by tracing the app's stop, subscription, toggle, and playback-event paths, plus the installed Finding 6 (P2): Bind episode controls to the episode displayed on the page Locations: podcasts/ui/episode/episode_view.dart:157–177 and podcasts/ui/episode/episode_view.dart:253. Problem: Opening an episode page does not load that episode, but its seek, rewind, forward, and speed controls operate on the shared player's current source. Reproduction: Load and pause A, open B's detail page without pressing Play, then drag B's scrubber. B's duration and a zero position are displayed, but the seek command changes A's position. Cause: Impact: A user manipulates an episode other than the one shown. Seeking B before starting it also does not establish B's requested starting position. Suggested fix: Disable source-specific controls until the displayed episode is loaded, or explicitly load that episode before applying the operation. Passing an episode identity through the controller provides a second guard against controlling the wrong source. Validation: A widget test used the actual episode view and player controller with a fake handler holding paused A. B's slider was enabled. Seeking halfway through B's ten-minute duration sent a five-minute seek to the handler while its episode identity remained A. Finding 7 (P2): Honor cancellation during the remainder of the download operation Locations: podcasts/services/library_service.dart:43–60 and podcasts/controllers/downloads_controller.dart:80–83. Problem: The UI still offers cancellation while artwork is downloading, but cancellation only controls the already-finished audio request. Reproduction: Download an episode whose audio completes before a slow artwork request. While artwork is pending and the operation is still displayed as running, press Cancel. Allow artwork to finish. Cause: The cancel token is passed to Impact: The episode is saved and completion is reported even though the user canceled the operation and its running state was cleared. Suggested fix: Define the cancellation boundary explicitly. While the UI advertises cancellation, propagate or check the token through subsequent stages and before persistence/notification. Clean up uncommitted files safely, or mark the operation as committed and remove the cancellation affordance once cancellation can no longer be honored. Validation: A test delayed artwork completion while using the actual library service and downloads controller. It canceled at that stage, released the artwork operation, and found the canceled episode committed to storage. Finding 8 (P2): Avoid leaving repeated searches in a loading state Location: news/ui/search/search_viewmodel.dart:25–32. Problem: Clearing the field changes the notifier's results but does not change the query retained by Algolia's searcher. Repeating the same term can therefore set loading without producing a new response. Reproduction: Search for Cause: The empty-term branch returns before calling the search service. Algolia still holds Impact: Search becomes stuck for this ordinary clear-and-repeat interaction until another query or provider reset changes the searcher's state. Suggested fix: Keep the searcher's query synchronized with the cleared field, or deliberately reuse/reissue a repeated query without entering a loading state that has no corresponding response. Preserve sensible behavior for empty queries and pending responses. Validation: The actual notifier/service and real Algolia 1.6.0 Finding 9 (P2): Reject pagination results after disposal or refresh Locations: news/controllers/feed_controller.dart:64–81 and the analogous path in podcasts/ui/episode_list/episode_list_viewmodel.dart:91–106. Problem: Reproduction A: Trigger pagination, navigate away so the provider is disposed, then let the request complete. The success-path state assignment throws; the catch attempts another state assignment and throws a disposed-reference exception too. Reproduction B: Trigger pagination, refresh the feed before the old append request finishes, and let the refreshed first page complete. Then complete the old append. Its captured pre-refresh state overwrites the refreshed state and cursor. Cause: The method has neither cancellation tied to provider disposal/rebuild nor an operation-generation check. A mounted check alone is insufficient for a refresh where the notifier survives and now represents a newer request generation. Impact: Navigating away can produce an unhandled async exception. Refresh can be undone by a late pagination response, bringing back stale results and pagination metadata. This review did not demonstrate a whole-app crash. Suggested fix: Cancel pending work on disposal or invalidation where possible, and capture a generation/token for every append. Reject results and errors from an obsolete generation before assigning state. The error path needs the same protection as the success path. Validation: Tests exercised the actual News notifier with a controlled fake API. One reproduced the disposed-reference exception. Another confirmed that a refreshed cursor was replaced by the old pagination cursor. Those test pages used empty post lists; replacement of visible articles follows from the same state-copy code. The matching podcast pattern was identified by inspection rather than a separate pagination runtime test. Additional validation observations
The shipped Focused review tests are separate from the PR's tests. Some deliberately assert the existing defective behavior and pass when it is reproduced; others assert the desired behavior and fail on this revision. These outcomes should not be described as a passing application test suite. There was no live Algolia request, native audio playback, or production-data mutation during validation. |



TODO: Update description