-
Notifications
You must be signed in to change notification settings - Fork 3
PRD: Offline Mode — MAUI Blazor Hybrid Android App #110
Description
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 — where all rendering depends on a live SignalR connection to the server — 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
- 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.
- 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.
- 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.
- 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.
- As a user, I want to edit an existing note while offline so that I can refine my thoughts without needing a connection.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- As a user, I want to search my saved posts while offline so that I can find specific articles in my local cache.
- 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.
- As a user, I want to view my notes for a post while offline so that I can review what I previously wrote.
- As a user, I want to view the list of summaries while offline so that I can review previously generated summaries.
- 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.
- 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.
- 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.
- As a user, I want delta sync — only transferring records changed since my last sync — so that the sync is fast and uses minimal data.
- 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— Razor Class Library. Reusable Blazor components extracted fromNoteBookmark.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— .NET MAUI Blazor Hybrid project targeting Android. ReferencesNoteBookmark.SharedUIandNoteBookmark.Domain. Contains the MAUI shell (app lifecycle, navigation, DI bootstrap) and all offline/sync infrastructure.
Domain / API Changes
- Add
DateModified(UTC timestamp) to thePostandNotedomain models and their Azure Table Storage representations. Updated automatically on every create or update. - Extend the existing API endpoints to support a
modifiedAfterquery parameter (ISO 8601 timestamp), enabling delta fetch:GET /posts?modifiedAfter={ts}andGET /notes?modifiedAfter={ts}. - The
DateModifiedfield is also used as the tiebreaker in last-write-wins conflict resolution during sync.
Local Storage Layer (MauiApp)
ILocalDataService— interface mirroring the data operations needed by the UI (get posts, get notes, save note, update post, etc.).LocalDataService— sqlite-net-pcl implementation. Local schema mirrorsPostandNotedomain models with three additional fields:DateModified(UTC),IsPendingSync(bool),IsDeleted(soft delete flag).- Database is initialized and migrated on app startup.
- Stores
LastSyncTimestampin MAUIPreferences.
Offline-Aware Data Layer (MauiApp)
IOfflineDataService— single interface used by all Blazor components inside MauiApp. Routes read/write operations based on connectivity state.OfflineDataService— implementation: when offline, delegates toILocalDataService; when online, calls the remote API (PostNoteClient) and mirrors results to local SQLite. Writes made offline are flaggedIsPendingSync = true.
Sync Engine (MauiApp)
SyncService— triggered onApp.OnResumeand on theConnectivity.ConnectivityChangedevent (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}andGET /notes?modifiedAfter={LastSyncTimestamp}; for each returned record, apply last-write-wins against the local copy usingDateModified; updateLastSyncTimestampto now. - Soft-deleted local records are pushed as deletes during the push phase.
Authentication (MauiApp)
- OIDC via
WebAuthenticator— opens Keycloak login in the system browser; handles the redirect callback. - Token cache — 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 — please go online to re-authenticate" message.
UI / UX
- Offline banner — 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.MauiApprequire 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 — not internal implementation details. Tests should not assert on private fields, internal method calls, or specific SQL queries.
LocalDataService — 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/AzureStorageTestFixturepatterns in the test projects.
SyncService — Unit Tests
- Mock
ILocalDataServiceandPostNoteClient(orIOfflineDataService). - Test: push phase sends all
IsPendingSync=truerecords and clears the flag on success; pull phase applies last-write-wins correctly (remote newer wins, local newer wins);LastSyncTimestampis updated after a successful sync. - Use xUnit + FluentAssertions (already in the codebase).
API Endpoints — Integration Tests
- Use
WebApplicationFactory(already used inNoteBookmark.Api.Tests). - Test:
GET /posts?modifiedAfter={ts}returns only records modified after the given timestamp;GET /notes?modifiedAfter={ts}same;PATCH /posts/{id}andPATCH /notes/{id}correctly updateDateModified. - Tests should cover both empty-result and multi-result cases for delta queries.
Out of Scope
- iOS support — Android is the primary target; iOS can be added in a future issue.
- Background sync — sync runs in the foreground only (app resume / connectivity change); no background service or background fetch.
- File and blob sync — markdown export files and blob-stored content are not synced offline; only structured Post and Note records are.
- Multi-device conflict resolution — beyond last-write-wins. Complex merge strategies are deferred.
- Play Store publishing — APK build configuration is in scope; store submission is a separate concern.
- Push notifications for sync status.
Further Notes
- The existing
NoteBookmark.BlazorAppis not modified in terms of behavior — only the Blazor component library extraction (NoteBookmark.SharedUI) affects it structurally. - The
v-nextbranch should be used as the base for all feature branches related to this PRD, and PRs should merge back intov-next. - All GitHub issues spawned from this PRD should carry the
applabel. - The
DateModifiedfield addition to the API is a non-breaking change — existing clients that don't sendmodifiedAftersimply receive all records as before.
Metadata
Metadata
Assignees
Labels
Projects
Status
Status