Skip to content

Feature/offline support#529

Open
hiraljj05 wants to merge 10 commits intoCircuitVerse:masterfrom
hiraljj05:feature/offline-support
Open

Feature/offline support#529
hiraljj05 wants to merge 10 commits intoCircuitVerse:masterfrom
hiraljj05:feature/offline-support

Conversation

@hiraljj05
Copy link
Copy Markdown

@hiraljj05 hiraljj05 commented Mar 11, 2026

Fixes #528

Describe the changes you have made in this PR :
This PR adds offline reading support for Interactive Book chapters in the mobile app

Features implemented:

  • Chapters are cached locally after the first successful load
  • If the network request fails, the app loads the cached chapter
  • Added an "Offline Available" badge to indicate cached chapters
  • Implemented lifecycle safety checks to avoid render and scroll errors

Implementation details:

  • Added an IbOfflineService to store chapter markdown locally

  • Updated IbPageViewModel to:

    • save chapter content after loading
    • load cached content if API request fails
  • Updated IbPageView to display an Offline Available badge when cached content exists

Benefits:

  • Allows users to read previously opened chapters without internet
  • Improves usability for students in low-connectivity environments
  • Reduces repeated network requests for the same chapters

Screenshots of the changes (If any) -

Note: Please check Allow edits from maintainers. if you would like us to assist in the PR.

Summary by CodeRabbit

  • New Features

    • Offline chapter caching with an in-app "Offline Available" banner.
    • Background network connectivity check to adapt online/offline behavior.
  • Improvements

    • Streamlined password-reset flow with inline server error display and clearer success feedback.
    • Improved offline fallback: cached content used when live fetch fails.
  • Localization

    • Added "Reset instructions sent successfully" and Arabic/Hindi translations.
  • Chores

    • Updated ignore settings.

@netlify
Copy link
Copy Markdown

netlify Bot commented Mar 11, 2026

Deploy Preview for cv-mobile-app-web ready!

Name Link
🔨 Latest commit 819dbda
🔍 Latest deploy log https://app.netlify.com/projects/cv-mobile-app-web/deploys/69b1c68654275d000887a603
😎 Deploy Preview https://deploy-preview-529--cv-mobile-app-web.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 11, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds offline reading support for Interactive Books: new IbOfflineService caches chapter markdown; IbPageViewModel integrates offline loading, disposal tracking, saves cached content, and exposes isOfflineAvailable; ib_page_view renders an "Offline Available" banner, updates scrolling/hero tags, defers showcase init, and adds dispose logic. Adds NetworkUtils.isOnline and dependency connectivity_plus. Changes sendResetPasswordInstructions to return non-nullable Future and always return true on successful POST. Updates forgot-password UI to surface server errors inline and show a success SnackBar. Adds localization keys (forgot_password_success in en/ar/hi and ib_page_offline_available) and updates .gitignore.

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning Minor out-of-scope changes detected: forgot_password_success translations added to multiple localization files and improvements to forgot password view/viewmodel are unrelated to offline support objectives. Remove the forgot_password_success translation entries from app_ar.arb, app_en.arb, and app_hi.arb, and revert changes to forgot_password_view.dart and forgot_password_viewmodel.dart as these belong in a separate PR.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/offline support' directly corresponds to the main objective of adding offline reading support for Interactive Book chapters, clearly reflecting the primary change.
Linked Issues check ✅ Passed All core requirements from issue #528 are met: offline caching implemented via IbOfflineService [528], offline fallback on network failure [528], offline availability badge added to UI [528], and markdown rendering preserved [528].
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
lib/viewmodels/cv_landing_viewmodel.dart (1)

41-52: ⚠️ Potential issue | 🟠 Major

Make logout awaitable before resetting navigation.

onLogout() is declared as async void, which prevents awaiting the GoogleSignIn.signOut() call. At line 95, onLogout() is called without await, so the subsequent navigation (line 101) and success UI (lines 102-105) run before sign-out completes. If sign-out fails, the exception goes unhandled, leaving the app in an inconsistent logged-out state.

Change onLogout() to return Future<void> and await it at the call site:

Proposed fix
-  void onLogout() async {
+  Future<void> onLogout() async {
     _storage.isLoggedIn = false;
     _storage.currentUser = null;
     _storage.token = null;
     _currentUser = null; 
     // Perform google signout if auth type is google..
     if (_storage.authType == AuthType.GOOGLE) {
       await _googleSignIn.signOut();
     }

     notifyListeners();
   }
-      onLogout();
+      await onLogout();
lib/ui/views/authentication/forgot_password_view.dart (1)

5-5: ⚠️ Potential issue | 🟡 Minor

Remove the unused DialogService import.

The import is only referenced in a commented-out line (line 126), so it's not needed in the active code.

🧹 Nitpick comments (9)
lib/l10n/app_ar.arb (1)

45-45: Inconsistent key placement across locale files.

The forgot_password_success key is placed after how_to_support (line 44) in the contributors section, but in app_en.arb it's placed in the @forgot_password_view section (after forgot_password_link). Consider moving this key to the @forgot_password_view section (around line 97) for consistency with other locale files.

lib/viewmodels/authentication/forgot_password_viewmodel.dart (2)

24-27: Hardcoded error message should use localization.

The error message "Instructions couldn't be sent! Invalid Email" is hardcoded. For consistency with the rest of the app and to support internationalization, consider using a localized string or a constant from your constants file.


35-42: Fix inconsistent indentation in catch block.

The setErrorMessageFor call and its arguments have inconsistent indentation compared to the rest of the codebase.

🔧 Suggested formatting fix
     catch(_){
       setStateFor(SEND_RESET_INSTRUCTIONS, ViewState.Error);
-    setErrorMessageFor(
-      SEND_RESET_INSTRUCTIONS,
-      "Something went wrong",
-    );
-    return false;
+      setErrorMessageFor(
+        SEND_RESET_INSTRUCTIONS,
+        "Something went wrong",
+      );
+      return false;
     }
lib/ui/views/authentication/forgot_password_view.dart (2)

41-55: Remove commented-out code.

This commented-out code block appears to be the old implementation that has been replaced. It should be removed before merging to keep the codebase clean.

🧹 Proposed cleanup
-  // Widget _buildEmailInput() {
-  //   return CVTextField(
-  //     label: AppLocalizations.of(context)!.forgot_password_email,
-  //     type: TextInputType.emailAddress,
-  //     validator:
-  //         (value) =>
-  //             Validators.isEmailValid(value)
-  //                 ? null
-  //                 : AppLocalizations.of(
-  //                   context,
-  //                 )!.forgot_password_email_validation_error,
-  //     onSaved: (value) => _email = value!.trim(),
-  //     action: TextInputAction.done,
-  //   );
-  // }
-

126-126: Remove commented-out code.

This commented-out line should be removed along with the unused DialogService import if it's no longer needed.

lib/services/ib_offline_service.dart (3)

18-19: Replace print() with proper logging.

Using print() directly is not recommended in Flutter. Consider using debugPrint() for debug-only logging, or a structured logging service for production code. This applies to both error handlers (lines 19 and 32).

🔧 Suggested fix
+import 'package:flutter/foundation.dart';
 import 'dart:io';
 import 'package:path_provider/path_provider.dart';
     catch(e){
-      print("Offline save error: $e");
+      debugPrint("Offline save error: $e");
     }

37-40: Remove commented-out code.

The commented-out isDownloaded method should be removed if it's not planned for immediate use. It can always be retrieved from version control if needed later.


6-10: The "/" replacement is appropriate for the API file paths being used.

Chapter IDs in this codebase are file paths from the CircuitVerse API (e.g., 'index.md', 'docs/binary-representation/index.md'), so the current sanitization replacing / with _ is correct. Since these paths come from a controlled, Jekyll-based API with predictable naming conventions (alphanumeric, hyphens, underscores, dots), additional sanitization for characters like :, ?, *, etc. is not necessary.

If the source of chapter IDs ever changes or becomes less predictable, consider using a more comprehensive sanitization approach (e.g., a whitelist of allowed characters or a slug-generation library).

lib/services/API/users_api.dart (1)

211-214: Remove commented-out code.

The commented-out lines should be removed before merging. The new implementation that always returns true on successful POST is cleaner and sufficient since errors are handled via exceptions.

🧹 Proposed cleanup
     try {
-      // var jsonResponse = await ApiUtils.post(uri, headers: headers, body: json);
-      // return jsonResponse['message'] is String;
       await ApiUtils.post(uri, headers: headers, body: json);
       return true;

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 52bec1ae-21d7-4c61-ad22-077ef90fcccc

📥 Commits

Reviewing files that changed from the base of the PR and between f621531 and e2b2d5d.

📒 Files selected for processing (13)
  • .gitignore
  • lib/l10n/app_ar.arb
  • lib/l10n/app_en.arb
  • lib/l10n/app_hi.arb
  • lib/services/API/users_api.dart
  • lib/services/ib_offline_service.dart
  • lib/ui/views/authentication/forgot_password_view.dart
  • lib/ui/views/ib/ib_page_view.dart
  • lib/utils/network_utils.dart
  • lib/viewmodels/authentication/forgot_password_viewmodel.dart
  • lib/viewmodels/cv_landing_viewmodel.dart
  • lib/viewmodels/ib/ib_page_viewmodel.dart
  • pubspec.yaml

Comment thread lib/ui/views/ib/ib_page_view.dart
Comment thread lib/utils/network_utils.dart
Comment thread lib/viewmodels/cv_landing_viewmodel.dart
Comment thread lib/viewmodels/ib/ib_page_viewmodel.dart
Comment thread lib/viewmodels/ib/ib_page_viewmodel.dart
Comment thread pubspec.yaml Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
lib/viewmodels/ib/ib_page_viewmodel.dart (1)

65-74: ⚠️ Potential issue | 🟠 Major

Cache the full page snapshot, not just flattened markdown.

This stores only concatenated IbMd text and then rebuilds an IbPageData without tableOfContents, chapterOfContents, or the real pageUrl. The offline screen will render markdown, but it loses TOC-driven UI and any inline chapter_contents block, and same-page link handling no longer has the original page URL context.

Also applies to: 87-92


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0c2131f-f002-4c89-963e-3fb98ef8eff7

📥 Commits

Reviewing files that changed from the base of the PR and between e2b2d5d and 09f8eb6.

📒 Files selected for processing (4)
  • lib/l10n/app_en.arb
  • lib/ui/views/ib/ib_page_view.dart
  • lib/viewmodels/ib/ib_page_viewmodel.dart
  • pubspec.yaml
🚧 Files skipped from review as they are similar to previous changes (2)
  • pubspec.yaml
  • lib/l10n/app_en.arb

Comment thread lib/ui/views/ib/ib_page_view.dart Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (6)
lib/ui/views/ib/ib_page_view.dart (4)

87-88: Remove commented-out code.

These lines are dead code. The ShowCaseWidget.of(context) logic has been correctly moved to didChangeDependencies; the commented remnants add noise.

🧹 Proposed cleanup
     _ibFloatingButtonState = IbFloatingButtonState();
-    // super.initState();
-    // _showCaseWidgetState = ShowCaseWidget.of(context);
     _landingModel = context.read<IbLandingViewModel>();

485-489: Remove the commented-out dispose block.

The active dispose() is now at lines 76-81. Leaving the old implementation commented out adds clutter and confusion.

🧹 Proposed cleanup
-  // `@override`
-  // void dispose() {
-  //   _hideButtonController.dispose();
-  //   super.dispose();
-  // }
-
   `@override`
   Widget build(BuildContext context) {

496-507: Clean up orphaned comments.

Lines 496 and 500 are remnants of the old implementation. They should be removed for clarity.

🧹 Proposed cleanup
       onModelReady: (model) {
         _model = model;
-        // model.fetchPageData(id: widget.chapter.id);
         WidgetsBinding.instance.addPostFrameCallback((_) async{
-        if (!mounted) return;
-        await model.fetchPageData(id: widget.chapter.id);
-        // Future.delayed(const Duration(milliseconds: 300), () {
-        if (!mounted) return;
-        model.showCase(
-          _showCaseWidgetState,
-          widget.showCase,
-          widget.globalKeysMap,
-        );
-      });
+          if (!mounted) return;
+          await model.fetchPageData(id: widget.chapter.id);
+          if (!mounted) return;
+          model.showCase(
+            _showCaseWidgetState,
+            widget.showCase,
+            widget.globalKeysMap,
+          );
+        });
       },

526-543: Minor cleanup and optimization.

  1. Line 526: Remove the commented-out line.
  2. Line 538: Add const to TextStyle for a minor performance optimization.
  3. Consider using theme-aware colors for better dark-mode support (optional).
🧹 Proposed cleanup
-                  // children: _buildPageContent(model.pageData),
                   children: [
-                  if(model.isOfflineAvailable)
+                  if (model.isOfflineAvailable)
                     Container(
                       margin: const EdgeInsets.only(bottom: 10),
                       padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                       decoration: BoxDecoration(
                         color: Colors.green,
                         borderRadius: BorderRadius.circular(5),
                       ),
-                      child:Text(
+                      child: Text(
                         AppLocalizations.of(context)!.ib_page_offline_available,
-                        style: TextStyle(color: Colors.white, fontSize: 12),
+                        style: const TextStyle(color: Colors.white, fontSize: 12),
                       ),
                     ),
-
                     ..._buildPageContent(model.pageData),
                   ],
lib/viewmodels/ib/ib_page_viewmodel.dart (2)

26-27: Consider using locator for service instantiation consistency.

_ibEngineService uses locator<IbEngineService>() (line 40), but _offlineService is instantiated directly. Using the locator pattern for IbOfflineService would maintain consistency and improve testability by allowing dependency injection.

♻️ Suggested refactor

First, register the service in your locator setup:

locator.registerLazySingleton<IbOfflineService>(() => IbOfflineService());

Then update the viewmodel:

-  final IbOfflineService _offlineService = IbOfflineService();
+  final IbOfflineService _offlineService = locator<IbOfflineService>();

45-54: Remove commented-out legacy code.

Version control preserves the history. Keeping the old implementation as comments adds noise and reduces readability.

♻️ Suggested fix
-  // Future? fetchPageData({String id = 'index.md'}) async {
-  //   try {
-  //     _pageData = await _ibEngineService.getPageData(id: id);
-
-  //     setStateFor(IB_FETCH_PAGE_DATA, ViewState.Success);
-  //   } on Failure catch (f) {
-  //     setStateFor(IB_FETCH_PAGE_DATA, ViewState.Error);
-  //     setErrorMessageFor(IB_FETCH_PAGE_DATA, f.message);
-  //   }
-  // }
-

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ce2fff68-3790-44c1-8d47-45e1e4245149

📥 Commits

Reviewing files that changed from the base of the PR and between 09f8eb6 and be9d6d3.

📒 Files selected for processing (3)
  • lib/ui/views/ib/ib_page_view.dart
  • lib/viewmodels/cv_landing_viewmodel.dart
  • lib/viewmodels/ib/ib_page_viewmodel.dart
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/viewmodels/cv_landing_viewmodel.dart

Comment thread lib/viewmodels/ib/ib_page_viewmodel.dart
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (3)
lib/viewmodels/ib/ib_page_viewmodel.dart (3)

90-100: ⚠️ Potential issue | 🟡 Minor

Add disposed check after loading cached content.

After await _offlineService.loadChapter(), the method continues to mutate _pageData and isOfflineAvailable without checking if the viewmodel was disposed during the await.

🛡️ Proposed fix
     final cachedMarkdown = await _offlineService.loadChapter(id);
+    if (disposed) return;

     if (cachedMarkdown != null) {

58-60: ⚠️ Potential issue | 🟡 Minor

Missing early return when disposed is true.

The disposed check here only skips setting the Busy state but allows the method to continue executing the fetch and all subsequent operations. Add an early return to prevent unnecessary work and potential post-disposal state mutations.

🛡️ Proposed fix
-  if (!disposed) {
-    setStateFor(IB_FETCH_PAGE_DATA, ViewState.Busy);
-  }
+  if (disposed) return;
+  setStateFor(IB_FETCH_PAGE_DATA, ViewState.Busy);

63-70: ⚠️ Potential issue | 🟡 Minor

Add disposed check after async operation before accessing state.

After await _ibEngineService.getPageData(), if disposed became true during the await, the code continues to access _pageData!.content and mutate state. This can cause issues if the widget tree was disposed while the request was in flight.

🛡️ Proposed fix
     _pageData = await _ibEngineService.getPageData(id: id);
+    if (disposed) return;

     String markdown = "";
🧹 Nitpick comments (2)
lib/viewmodels/ib/ib_page_viewmodel.dart (2)

26-26: Consider using the locator pattern for IbOfflineService.

The _ibEngineService on line 40 uses locator<IbEngineService>() for dependency injection, but _offlineService is directly instantiated. This inconsistency makes IbOfflineService harder to mock in unit tests.

♻️ Suggested refactor

Register IbOfflineService in the locator and inject it here:

-  final IbOfflineService _offlineService = IbOfflineService();
+  final IbOfflineService _offlineService = locator<IbOfflineService>();

Then register it in your locator setup file.


45-54: Remove commented-out dead code.

The old fetchPageData implementation is preserved in git history. Remove this commented block to keep the codebase clean.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c5e1b928-7a40-45bb-99be-4fb46b6c67cc

📥 Commits

Reviewing files that changed from the base of the PR and between be9d6d3 and 8da5e8f.

📒 Files selected for processing (1)
  • lib/viewmodels/ib/ib_page_viewmodel.dart

Comment thread lib/viewmodels/ib/ib_page_viewmodel.dart
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
lib/viewmodels/ib/ib_page_viewmodel.dart (2)

26-26: Inconsistent dependency injection pattern.

IbOfflineService is instantiated directly, whereas IbEngineService on line 40 is obtained via locator<IbEngineService>(). Using direct instantiation makes unit testing harder since you cannot inject a mock. Consider registering IbOfflineService with the service locator for consistency.

♻️ Suggested refactor
-  final IbOfflineService _offlineService = IbOfflineService();
+  final IbOfflineService _offlineService = locator<IbOfflineService>();

You would also need to register the service in your locator setup.


45-54: Remove commented-out dead code.

The old fetchPageData implementation should be deleted rather than left as comments. Version control preserves the history if you ever need to reference the original.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ebc6062-4ecf-4361-a3fe-2a9bc3335cfc

📥 Commits

Reviewing files that changed from the base of the PR and between 8da5e8f and 819dbda.

📒 Files selected for processing (1)
  • lib/viewmodels/ib/ib_page_viewmodel.dart

Comment thread lib/viewmodels/ib/ib_page_viewmodel.dart
Comment thread lib/viewmodels/ib/ib_page_viewmodel.dart
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Offline Reading Support for Interactive Books

1 participant