Skip to content

PRD: Offline Mode — MAUI Blazor Hybrid Android App #110

@fboucher

Description

@fboucher

Epic Tracking


Problem Statement As a NoteBookmark user, I rely on the app to save bookmarks and take reading notes throughout my day. However, because the current app is a Blazor Server application Gn++n++ where all rendering depends on a live SignalR connection to the server Gn++n++ I am completely unable to use it when I am offline (e.g., on an airplane, in a low-connectivity area, or simply away from Wi-Fi on my Android tablet). I cannot browse my saved posts, I cannot write notes, and I cannot review summaries. Any work I want to do with my reading list must wait until I am back online. --- ## Solution Build NoteBookmark.MauiApp, a .NET MAUI Blazor Hybrid Android application, as a second client that runs alongside the existing web app. Both clients share the same REST API (NoteBookmark.Api). The MAUI app adds a local SQLite database for offline storage and a sync engine that reconciles changes when connectivity is restored, using a last-write-wins strategy based on DateModified timestamps. This approach reuses the existing Blazor component library (extracted into a shared Razor Class Library) and avoids any breaking changes to the existing web application. --- ## User Stories 1. As a user, I want to install the NoteBookmark app on my Android tablet so that I can access my reading list natively without opening a browser. 2. As a user, I want to browse all my saved posts while offline so that I can decide what to read even without internet access. 3. As a user, I want to read the metadata (title, author, URL, date) of saved posts while offline so that I can choose which article to open. 4. As a user, I want to write notes on a saved post while offline so that I can capture my thoughts immediately as I read. 5. As a user, I want to edit an existing note while offline so that I can refine my thoughts without needing a connection. 6. As a user, I want to mark a post as read while offline so that my reading list stays up to date even when disconnected. 7. As a user, I want my offline changes to sync automatically when I come back online so that I don't have to manually trigger a sync. 8. As a user, I want the sync to use the most recent version of a record (last-write-wins) so that I don't lose work done on either device. 9. As a user, I want to see a clear visual indicator when the app is in offline mode so that I understand why some actions may be limited. 10. As a user, I want to log in to the app once and stay logged in, even across offline periods, so that I don't have to re-authenticate every time I open the app. 11. As a user, I want the app to silently refresh my authentication token when I come back online so that my session is never interrupted. 12. As a user, I want to search my saved posts while offline so that I can find specific articles in my local cache. 13. As a user, I want to filter my posts by read/unread status while offline so that I can focus on what I haven't read yet. 14. As a user, I want to view my notes for a post while offline so that I can review what I previously wrote. 15. As a user, I want to view the list of summaries while offline so that I can review previously generated summaries. 16. As a user, I want the app's settings (theme, AI configuration) to be available offline so that the UI is consistent regardless of connectivity. 17. As a user, I want the sync to run in the foreground when I resume the app after being offline so that my latest changes are pushed and pulled promptly. 18. As a user, I want the app to queue any writes made offline and replay them in order when the connection is restored so that no data is lost. 19. As a user, I want delta sync Gn++n++ only transferring records changed since my last sync Gn++n++ so that the sync is fast and uses minimal data. 20. As a user, I want the MAUI app to feel visually consistent with the web app (same Fluent UI design system) so that the experience is familiar. --- ## Implementation Decisions ### New Projects - NoteBookmark.SharedUI Gn++n++ Razor Class Library. Reusable Blazor components extracted from NoteBookmark.BlazorApp. Both the existing web app and the new MAUI app reference this library. Components include: post list, post detail, note dialog, summary list, search, and settings form. - NoteBookmark.MauiApp Gn++n++ .NET MAUI Blazor Hybrid project targeting Android. References NoteBookmark.SharedUI and NoteBookmark.Domain. Contains the MAUI shell (app lifecycle, navigation, DI bootstrap) and all offline/sync infrastructure. ### Domain / API Changes - Add DateModified (UTC timestamp) to the Post and Note domain models and their Azure Table Storage representations. Updated automatically on every create or update. - Extend the existing API endpoints to support a modifiedAfter query parameter (ISO 8601 timestamp), enabling delta fetch: GET /posts?modifiedAfter={ts} and GET /notes?modifiedAfter={ts}. - The DateModified field is also used as the tiebreaker in last-write-wins conflict resolution during sync. ### Local Storage Layer (MauiApp) - ILocalDataService Gn++n++ interface mirroring the data operations needed by the UI (get posts, get notes, save note, update post, etc.). - LocalDataService Gn++n++ sqlite-net-pcl implementation. Local schema mirrors Post and Note domain models with three additional fields: DateModified (UTC), IsPendingSync (bool), IsDeleted (soft delete flag). - Database is initialized and migrated on app startup. - Stores LastSyncTimestamp in MAUI Preferences. ### Offline-Aware Data Layer (MauiApp) - IOfflineDataService Gn++n++ single interface used by all Blazor components inside MauiApp. Routes read/write operations based on connectivity state. - OfflineDataService Gn++n++ implementation: when offline, delegates to ILocalDataService; when online, calls the remote API (PostNoteClient) and mirrors results to local SQLite. Writes made offline are flagged IsPendingSync = true. ### Sync Engine (MauiApp) - SyncService Gn++n++ triggered on App.OnResume and on the Connectivity.ConnectivityChanged event (transition to online). - Push phase: query all local records where IsPendingSync = true; PATCH/POST each to the API; on success, clear the flag. - Pull phase: call GET /posts?modifiedAfter={LastSyncTimestamp} and GET /notes?modifiedAfter={LastSyncTimestamp}; for each returned record, apply last-write-wins against the local copy using DateModified; update LastSyncTimestamp to now. - Soft-deleted local records are pushed as deletes during the push phase. ### Authentication (MauiApp) - OIDC via WebAuthenticator Gn++n++ opens Keycloak login in the system browser; handles the redirect callback. - Token cache Gn++n++ access token and refresh token stored in MAUI SecureStorage. - On app startup: load cached tokens; if the access token is expired and the device is online, attempt a silent refresh; if offline and token is not yet expired, allow access. - If the refresh fails or the token is expired while offline, the user sees a "Session expired Gn++n++ please go online to re-authenticate" message. ### UI / UX - Offline banner Gn++n++ a persistent banner is shown at the top of the app whenever Connectivity.NetworkAccess != NetworkAccess.Internet, clearly labelling the offline state. - All write actions (add note, mark as read) are permitted while offline; they are queued and synced later. - Navigation and layout reuse the shared Fluent UI components from NoteBookmark.SharedUI. ### Authorization - All screens in NoteBookmark.MauiApp require a valid (possibly cached) authenticated session. - API calls from the MAUI app use the cached Bearer token in the Authorization header, identical to the web app. --- ## Testing Decisions Good tests verify observable behavior through public contracts Gn++n++ not internal implementation details. Tests should not assert on private fields, internal method calls, or specific SQL queries. ### LocalDataService Gn++n++ Integration Tests - Use an in-memory or temp-file SQLite database. - Test: save a post/note, retrieve it, update it, soft-delete it, query pending sync records. - Prior art: the existing ApiTestFixture / AzureStorageTestFixture patterns in the test projects. ### SyncService Gn++n++ Unit Tests - Mock ILocalDataService and PostNoteClient (or IOfflineDataService). - Test: push phase sends all IsPendingSync=true records and clears the flag on success; pull phase applies last-write-wins correctly (remote newer wins, local newer wins); LastSyncTimestamp is updated after a successful sync. - Use xUnit + FluentAssertions (already in the codebase). ### API Endpoints Gn++n++ Integration Tests - Use WebApplicationFactory (already used in NoteBookmark.Api.Tests). - Test: GET /posts?modifiedAfter={ts} returns only records modified after the given timestamp; GET /notes?modifiedAfter={ts} same; PATCH /posts/{id} and PATCH /notes/{id} correctly update DateModified. - Tests should cover both empty-result and multi-result cases for delta queries. --- ## Out of Scope - iOS support Gn++n++ Android is the primary target; iOS can be added in a future issue. - Background sync Gn++n++ sync runs in the foreground only (app resume / connectivity change); no background service or background fetch. - File and blob sync Gn++n++ markdown export files and blob-stored content are not synced offline; only structured Post and Note records are. - Multi-device conflict resolution Gn++n++ beyond last-write-wins. Complex merge strategies are deferred. - Play Store publishing Gn++n++ APK build configuration is in scope; store submission is a separate concern. - Push notifications for sync status. --- ## Further Notes - The existing NoteBookmark.BlazorApp is not modified in terms of behavior Gn++n++ only the Blazor component library extraction (NoteBookmark.SharedUI) affects it structurally. - The v-next branch should be used as the base for all feature branches related to this PRD, and PRs should merge back into v-next. - All GitHub issues spawned from this PRD should carry the app label. - The DateModified field addition to the API is a non-breaking change Gn++n++ existing clients that don't send modifiedAfter simply receive all records as before.

Metadata

Metadata

Assignees

No one assigned

    Labels

    appepicLarge feature tracking issuetype:featureNew capability

    Projects

    Status
    Backlog
    Status
    No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions