Add API-backed liked items profile screen - #458
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughAdds an API-backed liked-items feature with asynchronous like/unlike state, cached products, a new page and route, profile navigation, product-card updates, and coverage for repository, view-model, page, and profile behavior. ChangesLiked items
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Sequence Diagram(s)sequenceDiagram
participant ProfilePage
participant AppRoutes
participant LikedItemsPage
participant LikedItemsViewModel
participant ProductRepository
ProfilePage->>AppRoutes: Navigate to /liked-items
AppRoutes->>LikedItemsPage: Build page
LikedItemsPage->>LikedItemsViewModel: Load liked products
LikedItemsViewModel->>ProductRepository: Fetch liked products
ProductRepository-->>LikedItemsViewModel: Return products or error
LikedItemsViewModel-->>LikedItemsPage: Render loading, empty, error, or loaded state
sequenceDiagram
participant ProductCard
participant ProductViewModel
participant ProductRepository
participant ApiService
ProductCard->>ProductViewModel: Toggle like
ProductViewModel->>ProductRepository: Like or unlike product
ProductRepository->>ApiService: POST product like endpoint
ApiService-->>ProductRepository: Return response
ProductRepository-->>ProductViewModel: Return Result<bool>
ProductViewModel-->>ProductCard: Update like state
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75e9aa6a82
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| _productFromLikedDocument( | ||
| likedDocument.data(), | ||
| reference.productId, | ||
| ) ?? | ||
| await _fetchProduct(reference.productId); |
There was a problem hiding this comment.
Revalidate snapshots before showing liked items
When a liked document contains productSnapshot, this path short-circuits the live product fetch, so the _isHiddenProduct check is never applied. Because likeProduct writes a snapshot for every new like, a user who liked an item before it was archived or deleted will still see and be able to open the stale item from Liked Items; fetch the live product/status first or validate the snapshot against current product state before returning it.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (5)
lib/features/products/product_repository.dart (2)
86-96: 🚀 Performance & Scalability | 🔵 TrivialConsider capping/paginating the liked-items query for scalability.
fetchLikedProductsloads the entirelikedProductssubcollection with nolimit(). For users with very large liked lists this grows unbounded read cost/latency on every page load; consider alimit()with incremental pagination as the feature scales.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/features/products/product_repository.dart` around lines 86 - 96, Update fetchLikedProducts to avoid loading the entire likedProducts subcollection in one request by applying a bounded Firestore limit and supporting incremental pagination with a cursor. Preserve the existing likedAt descending order and product conversion behavior, and expose or reuse the surrounding pagination contract so callers can request subsequent pages.
92-136: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftSequential per-item fetches and lost ordering for fallback-recovered products.
Two related issues in this loop:
- For every liked document without a usable embedded snapshot,
await _fetchProduct(reference.productId)(Line 114) runs sequentially inside theforloop — N missing snapshots means N serial network round trips before the batched home-feed fallback even starts.- Products recovered via
_fetchProductsFromHomeFeedare appended in a second pass (Lines 124-130) after the main loop, so they lose their originallikedAt-descending position — the final list is no longer fully ordered by like time whenever any items needed the fallback path.♻️ Suggested restructure (parallel fetch + order-preserving assembly)
- final products = <Product>[]; - final missingProductIds = <String>[]; - final seenProductIds = <String>{}; - - for (final likedDocument in likedSnapshot.docs) { - final reference = LikedProductReference.tryParse( - documentId: likedDocument.id, - data: likedDocument.data(), - ); - if (reference == null) { - continue; - } - - final product = - _productFromLikedDocument( - likedDocument.data(), - reference.productId, - ) ?? - await _fetchProduct(reference.productId); - if (product != null) { - if (seenProductIds.add(product.id)) { - products.add(product); - } - } else { - missingProductIds.add(reference.productId); - } - } - - final fallbackProducts = await _fetchProductsFromHomeFeed(missingProductIds); - for (final productId in missingProductIds) { - final product = fallbackProducts[productId]; - if (product != null && seenProductIds.add(product.id)) { - products.add(product); - } - } - - return Result.success(products); + final docs = likedSnapshot.docs; + final resolved = List<Product?>.filled(docs.length, null); + final pendingIndices = <int>[]; + final pendingIds = <String>[]; + + for (var i = 0; i < docs.length; i++) { + final reference = LikedProductReference.tryParse( + documentId: docs[i].id, + data: docs[i].data(), + ); + if (reference == null) continue; + + final snapshotProduct = _productFromLikedDocument(docs[i].data(), reference.productId); + if (snapshotProduct != null) { + resolved[i] = snapshotProduct; + } else { + pendingIndices.add(i); + pendingIds.add(reference.productId); + } + } + + final fetched = await Future.wait(pendingIds.map(_fetchProduct)); + final stillMissing = <String>[]; + for (var j = 0; j < pendingIndices.length; j++) { + resolved[pendingIndices[j]] = fetched[j]; + if (fetched[j] == null) stillMissing.add(pendingIds[j]); + } + + final fallbackProducts = await _fetchProductsFromHomeFeed(stillMissing); + for (var j = 0; j < pendingIndices.length; j++) { + final idx = pendingIndices[j]; + resolved[idx] ??= fallbackProducts[pendingIds[j]]; + } + + final seenProductIds = <String>{}; + final products = <Product>[ + for (final product in resolved) + if (product != null && seenProductIds.add(product.id)) product, + ]; + + return Result.success(products);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/features/products/product_repository.dart` around lines 92 - 136, Refactor the liked-products assembly in the repository method containing the shown loop to avoid awaiting _fetchProduct sequentially: collect unresolved references and fetch them concurrently, while retaining each liked document’s position. Then resolve fallback entries through _fetchProductsFromHomeFeed and assemble the final products in the original likedAt-descending iteration order, using seenProductIds for deduplication and preserving the existing Result behavior.lib/features/products/product_viewmodel.dart (1)
100-121: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUse value equality for liked-product snapshots.
_likedProductSnapshots[product.id] != productis identity-based here becauseProductdoesn’t define==/hashCode. SinceloadLikedProducts()passes freshly fetchedProductinstances intocacheLikedProducts(), unchanged items still look different andnotifyListeners()fires on each refresh. Compare a stable field or add value equality toProduct.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/features/products/product_viewmodel.dart` around lines 100 - 121, Update cacheLikedProducts to compare liked-product snapshots by stable product values rather than Product identity, so freshly fetched but unchanged products do not set changed or trigger notifyListeners; use an existing stable field comparison or implement Product value equality via ==/hashCode.test/liked_product_reference_test.dart (1)
1-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd coverage for a populated
likedAtvalue.All three tests omit
likedAtfrom the input data, so_parseLikedAt'sTimestamp/DateTimebranches are never exercised — only thenullcase is covered. SincelikedAtis a core part of the persisted schema this PR introduces, add a case asserting successful parsing of a real value (e.g.,Timestamp.fromDate(...)).✅ Suggested additional test
+ test('parses a populated likedAt timestamp', () { + final likedAt = DateTime(2026, 1, 1); + final reference = LikedProductReference.tryParse( + documentId: 'product-4', + data: {'productId': 'product-4', 'likedAt': Timestamp.fromDate(likedAt)}, + ); + + expect(reference, isNotNull); + expect(reference!.likedAt, likedAt); + });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/liked_product_reference_test.dart` around lines 1 - 36, The LikedProductReference tests lack coverage for parsing a populated likedAt value. Add a test in the LikedProductReference group that passes a real Timestamp.fromDate value through tryParse, asserts the reference is created, and verifies likedAt matches the expected parsed DateTime.test/mvp_profile_stats_test.dart (1)
93-119: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winMissing coverage for Profile → Liked Items navigation.
This test confirms the "Liked" tile renders but never taps it to verify it actually navigates to
LikedItemsPage, which is the PR's core new user flow. Mirror the routing setup fromtest/liked_items_page_test.dart(onGenerateRoute: AppRoutes.generateRoute+navigatorKey) and assertfind.byType(LikedItemsPage)after tapping the "Liked" tile.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@test/mvp_profile_stats_test.dart` around lines 93 - 119, The ProfilePage widget test currently verifies only that the Liked shortcut renders, not that it navigates correctly. Update the test setup around ProfilePage to mirror liked_items_page_test.dart by configuring AppRoutes.generateRoute and the shared navigatorKey, then tap the profileUserLiked tile and assert that LikedItemsPage is present while preserving the existing visibility assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@lib/features/liked_items/liked_items_page.dart`:
- Line 136: Update the onLikePressed callback in the liked-items page to await
unlikeProduct’s boolean result, and show a SnackBar when it returns false so the
view model’s errorMessage is surfaced to the user. Preserve the existing
successful unlike behavior and use the page’s current BuildContext and
established error-message access.
In `@lib/features/liked_items/liked_items_view_model.dart`:
- Around line 55-74: Update unlikeProduct to avoid reusing the pre-await index
after setProductLiked completes: locate the product by id against the current
_products when applying the successful removal, preferably filtering out
product.id. Preserve the existing success/error state updates and safely handle
concurrent unlikeProduct calls without removeAt index failures.
---
Nitpick comments:
In `@lib/features/products/product_repository.dart`:
- Around line 86-96: Update fetchLikedProducts to avoid loading the entire
likedProducts subcollection in one request by applying a bounded Firestore limit
and supporting incremental pagination with a cursor. Preserve the existing
likedAt descending order and product conversion behavior, and expose or reuse
the surrounding pagination contract so callers can request subsequent pages.
- Around line 92-136: Refactor the liked-products assembly in the repository
method containing the shown loop to avoid awaiting _fetchProduct sequentially:
collect unresolved references and fetch them concurrently, while retaining each
liked document’s position. Then resolve fallback entries through
_fetchProductsFromHomeFeed and assemble the final products in the original
likedAt-descending iteration order, using seenProductIds for deduplication and
preserving the existing Result behavior.
In `@lib/features/products/product_viewmodel.dart`:
- Around line 100-121: Update cacheLikedProducts to compare liked-product
snapshots by stable product values rather than Product identity, so freshly
fetched but unchanged products do not set changed or trigger notifyListeners;
use an existing stable field comparison or implement Product value equality via
==/hashCode.
In `@test/liked_product_reference_test.dart`:
- Around line 1-36: The LikedProductReference tests lack coverage for parsing a
populated likedAt value. Add a test in the LikedProductReference group that
passes a real Timestamp.fromDate value through tryParse, asserts the reference
is created, and verifies likedAt matches the expected parsed DateTime.
In `@test/mvp_profile_stats_test.dart`:
- Around line 93-119: The ProfilePage widget test currently verifies only that
the Liked shortcut renders, not that it navigates correctly. Update the test
setup around ProfilePage to mirror liked_items_page_test.dart by configuring
AppRoutes.generateRoute and the shared navigatorKey, then tap the
profileUserLiked tile and assert that LikedItemsPage is present while preserving
the existing visibility assertions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ddd4646d-0675-4781-9486-790d1ea58088
📒 Files selected for processing (16)
lib/core/config/app_strings.dartlib/core/config/firestore_constants.dartlib/core/router/nav_routes.dartlib/core/utils/dependency.dartlib/features/liked_items/liked_items_page.dartlib/features/liked_items/liked_items_view_model.dartlib/features/liked_items/models/liked_product_reference.dartlib/features/products/product_card.dartlib/features/products/product_repository.dartlib/features/products/product_viewmodel.dartlib/features/profile/profile_page.dartlib/features/profile/widgets/user_order_details.darttest/liked_items_page_test.darttest/liked_items_view_model_test.darttest/liked_product_reference_test.darttest/mvp_profile_stats_test.dart
| key: ValueKey(product.id), | ||
| product: product, | ||
| onTap: () => context.read<ProductViewModel>().goToProductPage(product), | ||
| onLikePressed: () => context.read<LikedItemsViewModel>().unlikeProduct(product), |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Handle the potential failure when unliking a product.
Currently, if unlikeProduct fails (e.g., due to a network error), it returns false and sets an errorMessage in the view model. However, because the page status remains LikedItemsStatus.loaded, the UI ignores this error silently and leaves the user wondering why the item wasn't removed.
Please await the result and display a SnackBar to inform the user if the action fails.
🐛 Proposed fix
- onLikePressed: () => context.read<LikedItemsViewModel>().unlikeProduct(product),
+ onLikePressed: () async {
+ final viewModel = context.read<LikedItemsViewModel>();
+ final success = await viewModel.unlikeProduct(product);
+ if (!success && context.mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(viewModel.errorMessage ?? AppStrings.genericError),
+ ),
+ );
+ }
+ },📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| onLikePressed: () => context.read<LikedItemsViewModel>().unlikeProduct(product), | |
| onLikePressed: () async { | |
| final viewModel = context.read<LikedItemsViewModel>(); | |
| final success = await viewModel.unlikeProduct(product); | |
| if (!success && context.mounted) { | |
| ScaffoldMessenger.of(context).showSnackBar( | |
| SnackBar( | |
| content: Text(viewModel.errorMessage ?? AppStrings.genericError), | |
| ), | |
| ); | |
| } | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/features/liked_items/liked_items_page.dart` at line 136, Update the
onLikePressed callback in the liked-items page to await unlikeProduct’s boolean
result, and show a SnackBar when it returns false so the view model’s
errorMessage is surfaced to the user. Preserve the existing successful unlike
behavior and use the page’s current BuildContext and established error-message
access.
| Future<bool> unlikeProduct(Product product) async { | ||
| final index = _products.indexWhere((item) => item.id == product.id); | ||
| if (index == -1) { | ||
| return true; | ||
| } | ||
|
|
||
| final result = await productViewModel.setProductLiked(product, false); | ||
| if (!result.isSuccess) { | ||
| _errorMessage = result.error ?? 'Unable to remove this liked item.'; | ||
| notifyListeners(); | ||
| return false; | ||
| } | ||
|
|
||
| final nextProducts = List<Product>.from(_products)..removeAt(index); | ||
| _products = List.unmodifiable(nextProducts); | ||
| _status = _products.isEmpty ? LikedItemsStatus.empty : LikedItemsStatus.loaded; | ||
| _errorMessage = null; | ||
| notifyListeners(); | ||
| return true; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win
TOCTOU: stale index reused against _products after an await, risking wrong-item removal or a crash.
index is computed from _products before the await productViewModel.setProductLiked(...) call, then reused against _products (which may have been reassigned in the meantime) to removeAt(index). Two near-simultaneous unlikeProduct calls on different items reproduce this without any external trigger:
- Call A captures
index=1(item B) and awaits. - Call B captures
index=2(item C) and awaits, still against the original list. - Call A resolves, removes index 1 →
_productsnow has 2 items. - Call B resolves and does
removeAt(2)against the now-shorter_products→RangeError, or removes the wrong item if lengths still happen to align.
🐛 Suggested fix (filter by id at removal time instead of a captured index)
Future<bool> unlikeProduct(Product product) async {
- final index = _products.indexWhere((item) => item.id == product.id);
- if (index == -1) {
+ final wasPresent = _products.any((item) => item.id == product.id);
+ if (!wasPresent) {
return true;
}
final result = await productViewModel.setProductLiked(product, false);
if (!result.isSuccess) {
_errorMessage = result.error ?? 'Unable to remove this liked item.';
notifyListeners();
return false;
}
- final nextProducts = List<Product>.from(_products)..removeAt(index);
- _products = List.unmodifiable(nextProducts);
+ _products = List.unmodifiable(
+ _products.where((item) => item.id != product.id),
+ );
_status = _products.isEmpty ? LikedItemsStatus.empty : LikedItemsStatus.loaded;
_errorMessage = null;
notifyListeners();
return true;
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| Future<bool> unlikeProduct(Product product) async { | |
| final index = _products.indexWhere((item) => item.id == product.id); | |
| if (index == -1) { | |
| return true; | |
| } | |
| final result = await productViewModel.setProductLiked(product, false); | |
| if (!result.isSuccess) { | |
| _errorMessage = result.error ?? 'Unable to remove this liked item.'; | |
| notifyListeners(); | |
| return false; | |
| } | |
| final nextProducts = List<Product>.from(_products)..removeAt(index); | |
| _products = List.unmodifiable(nextProducts); | |
| _status = _products.isEmpty ? LikedItemsStatus.empty : LikedItemsStatus.loaded; | |
| _errorMessage = null; | |
| notifyListeners(); | |
| return true; | |
| } | |
| Future<bool> unlikeProduct(Product product) async { | |
| final wasPresent = _products.any((item) => item.id == product.id); | |
| if (!wasPresent) { | |
| return true; | |
| } | |
| final result = await productViewModel.setProductLiked(product, false); | |
| if (!result.isSuccess) { | |
| _errorMessage = result.error ?? 'Unable to remove this liked item.'; | |
| notifyListeners(); | |
| return false; | |
| } | |
| _products = List.unmodifiable( | |
| _products.where((item) => item.id != product.id), | |
| ); | |
| _status = _products.isEmpty ? LikedItemsStatus.empty : LikedItemsStatus.loaded; | |
| _errorMessage = null; | |
| notifyListeners(); | |
| return true; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/features/liked_items/liked_items_view_model.dart` around lines 55 - 74,
Update unlikeProduct to avoid reusing the pre-await index after setProductLiked
completes: locate the product by id against the current _products when applying
the successful removal, preferably filtering out product.id. Preserve the
existing success/error state updates and safely handle concurrent unlikeProduct
calls without removeAt index failures.
|
Thanks for working on this, please let's switch this to use the api backend to reduce out direct firestore logic, there is already an api for it. we also have an api for like/unlike items, i think it has been integrated already, but its good to confirm - if not we might need to integrate that as well |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
lib/features/products/product_repository.dart (1)
22-66: 🚀 Performance & Scalability | 🔵 TrivialConsider supporting pagination for liked items.
Currently,
fetchLikedProductshardcodes a limit of50items and does not process the pagination metadata (such asnextCursor) returned by the API. Users with more than 50 liked items will only see their 50 most recent likes.As the feature matures and user activity grows, consider updating this method to accept a cursor and returning the pagination metadata alongside the products, enabling the UI to implement infinite scrolling.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/features/products/product_repository.dart` around lines 22 - 66, The fetchLikedProducts method currently retrieves only one fixed-size page and discards pagination metadata. Update its API and implementation to accept an optional cursor, pass that cursor with the existing limit to ApiEndpoints.likedProducts, and return the extracted products together with the response’s nextCursor (using the project’s established pagination result type), so callers can request subsequent pages.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@lib/features/products/product_repository.dart`:
- Around line 22-66: The fetchLikedProducts method currently retrieves only one
fixed-size page and discards pagination metadata. Update its API and
implementation to accept an optional cursor, pass that cursor with the existing
limit to ApiEndpoints.likedProducts, and return the extracted products together
with the response’s nextCursor (using the project’s established pagination
result type), so callers can request subsequent pages.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ed6fea7c-2d68-4ada-af71-069a73affc1c
📒 Files selected for processing (10)
lib/core/config/firestore_constants.dartlib/core/services/network/api_endpoints.dartlib/core/utils/dependency.dartlib/features/products/product_repository.dartlib/features/products/product_viewmodel.darttest/home_screen_mvp_test.darttest/liked_items_page_test.darttest/liked_items_view_model_test.darttest/product_repository_test.darttest/support/unexpected_api_service.dart
💤 Files with no reviewable changes (1)
- lib/core/config/firestore_constants.dart
🚧 Files skipped from review as they are similar to previous changes (3)
- lib/features/products/product_viewmodel.dart
- test/liked_items_page_test.dart
- test/liked_items_view_model_test.dart
|
@yusuphjoluwasen thank you for your advice, I have since made changes that use the api backend so we may reduce our direct firestore logic. The liked-items flow now: Validation: |
Manual testing resultEnvironment
Liked Items API flow — FailedSteps
Expected result Actual result
The immediate heart-state update worked, but the liked product could not be loaded through the live API flow. Evidences like server unavailaible LOGS.txt
Logout flow — Retest passedDuring an earlier test, the app returned to Profile after logout and Home displayed “No products available.” I repeated the logout flow, but the issue did not reproduce:
I am therefore treating the earlier result as an intermittent, currently unconfirmed observation. Automated tests
The failures match the documented unrelated baseline failures:
Could you please confirm whether |
|
Please retest @VitaliiKoliuka, The issue was from firebase. |
Retest blocked by
|
Device retest — liked state is not restored and like count is double-countedEnvironment
I reproduced an inconsistency between the current user’s liked state and the displayed like counter. Steps to reproduce
Actual result
Expected result
Evidencevideo.liked.state.mp4 |
Thank you, @yusuphjoluwasen. I retested , issue was resolved. The previous “Server is currently unavailable” error did not reproduce, and Profile → Liked loaded successfully. |
IMG_2035.movThank you @VitaliiKoliuka for the review and for re-checking! It seems to be working on my end and with the approval of @yusuphjoluwasen and your recent successful test, the PR can be merged with confidence. Thank you both very much; your hard work, contributions and support are greatly appreciated. |


Summary
Adds a profile-accessible Liked Items screen and makes the existing cherry product API the source of truth for likes.
This PR:
LikedItemsPagefrom the Profile Liked shortcutProductCardcomponent and links cards to the existing product pageGET /api/products/my-liked-itemsPOST /api/products/{id}/likewith{ "like": true|false }users/{uid}/likedProductsand removes Firestore product snapshot hydrationWhy
The first implementation persisted and hydrated likes directly through Firestore. The backend now exposes dedicated authenticated endpoints for this flow. Using those endpoints keeps ownership of liked-item data in the backend and prevents the app from maintaining a second client-side Firestore data contract.
This also removes the mismatch where homepage products came from the API but the liked screen attempted to rebuild them from Firestore product documents.
Change type
UI, API/data-flow and test changes. No backend endpoint, Firestore rule or dependency changes are included.
Reviewer notes and risks
DioApiServicesupplies the Firebase bearer token. This repository no longer accessesFirebaseAuthorFirebaseFirestoredirectly.success: trueand confirms the requesteddata.likedvalue.data.productsis treated as an empty result. Reviewers should confirm this fallback is preferred over an error state.main, including product search and pagination from Add product search and pagination #461.Testing
Passed:
flutter analyzeflutter test test/product_repository_test.dart test/liked_items_view_model_test.dart test/liked_items_page_test.dart test/home_screen_mvp_test.dart(27 tests)The full
flutter testrun completed with 57 passing and 2 failing tests. Both failures are unchanged fromorigin/mainand outside this PR scope:test/donation_model_test.dartexpectspostage_size == medium, while current serialisation returns nopostage_sizefieldtest/settings_legal_information_test.dartexpectsDate last updated: 2024-13-11, which is not present in the rendered documentNot completed:
Existing UI evidence
These Android emulator screenshots were captured before the API migration. They demonstrate the screen and navigation only, not the new backend integration.
Related work
Summary by CodeRabbit