Skip to content

feat: user authentication, reading plans, settings & streak sync (#6)#10

Merged
MelakuDemeke merged 34 commits into
mainfrom
pr-6-user-auth
May 26, 2026
Merged

feat: user authentication, reading plans, settings & streak sync (#6)#10
MelakuDemeke merged 34 commits into
mainfrom
pr-6-user-auth

Conversation

@MelakuDemeke

Copy link
Copy Markdown
Member

Summary

  • Full email/password + Google OAuth flow connected to EOTCbibleBE
  • All auth screens: login, register, OTP, forgot/reset password, profile
  • Backend sync for bookmarks, highlights, notes, and reading progress after login
  • Reading plans wired to GET /reading-plans with book-style cards coloured by testament (OT burgundy, NT navy, deuterocanonical olive)
  • View All screen and tap-to-read with local position resume
  • Settings sync: pull remote theme/fontSize on login (local wins on conflict), push on every change via PUT /auth/profile
  • Streak sync: max(local, remote) for both current and longest streak after login

What is deferred

  • Facebook OAuth — button shows "Coming soon"; backend endpoint exists, native Flutter integration requires Facebook Developer Console setup (platform registration + key hashes)

Test plan

  • Register a new account → OTP email arrives → verify → lands on home authenticated
  • Login with email/password → profile visible in Me tab
  • Login with Google → avatar loads from Google photo URL
  • 5 failed logins → locked account error message shown
  • Forgot password → reset email → reset flow completes
  • Log out → app returns to unauthenticated state, reading plans show auth prompt
  • Reading plans load from backend after login; tapping a card opens the reader at the correct chapter
  • Tapping a plan card a second time resumes from the same position
  • Change font size → log out → log in on fresh install → font size restored from backend
  • Streak on backend higher than local → home screen shows the higher count after login
  • Bookmarks, highlights, notes created offline sync to backend after login

Changes

See PR_CHANGES.md in the repo root for a full breakdown.

🤖 Generated with Claude Code

MelakuDemeke and others added 30 commits May 19, 2026 14:48
…deps

Adds http, flutter_secure_storage, and google_sign_in packages.
Creates core/api/api_client.dart (typed HTTP wrapper with Bearer token support),
core/auth/auth_storage.dart (secure JWT persistence), and
core/auth/user_profile.dart (UserProfile model).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AuthRepository wraps all /auth/* endpoints including Google Sign-In
(idToken flow). AuthNotifier auto-inits on first read: reads stored
token, validates via GET /profile, and resolves to authenticated or
unauthenticated. Logout is best-effort on the server and always clears
the token locally.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Amharic-first UI matching app design language. Includes email/password
form with validation, show/hide password toggle, locked-account error
handling, Google sign-in button, forgot password and register links.
Register and ForgotPassword screens are stubs pending Steps 4 and 5.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rse quote

Adds app logo header with language switcher, remember-me checkbox,
Google + Apple side-by-side social buttons, and bible verse footer quote.
Apple sign-in shows coming-soon snackbar pending backend support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Register: name/email/phone/password form with 2-step progress bar,
live password strength indicator (4 bars), and terms checkbox.
OTP: 6 individual digit boxes with auto-advance/back focus, 60s
countdown timer, resend, and masked contact display. Both match
app design language — parchment bg, burgundy primary, Geez labels.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Forgot password: email/phone tab switcher, key icon header, sends
POST /forgot-password, navigates to reset screen. Phone tab shows
coming-soon snackbar. Reset password: token field (from email),
new/confirm password fields, live requirements checklist with animated
check circles (length/uppercase/number/special char), green success
snackbar on completion, redirects to login.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ProfileScreen with avatar, stats (streak/bookmarks/progress),
  achievements section, and logout/delete-account menu
- Update MeScreen to ConsumerStatefulWidget; show real name+email when
  authenticated and navigate to ProfileScreen on tap
- Show guest card with login prompt when unauthenticated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SyncRepository: HTTP calls to POST/PUT/DELETE /bookmarks, /notes,
  /highlights, and POST /progress/log-reading
- SyncService: processes pending (pendingCreate/Update/Delete) items
  against the API, updates local sync_status and remote_id on success;
  errors are silently skipped and retried on next sync
- AppDatabase: add soft-delete variants (mark pendingDelete when
  remote_id exists) and getPending* / updateAnnotationSync helpers
- Auth: trigger syncAll() fire-and-forget after every login path
  (login, verifyOtp, signInWithGoogle, token restore on cold start)
- Annotations: use soft-delete for removes; trigger sync after every
  bookmark/note/highlight mutation
- Reader: fire-and-forget logReadingProgress after qualifying dwell

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Google login: show profile picture (NetworkImage) with initials
  fallback on error
- Email/password login: show first letter of user name in primary-
  color circle
- Guest: keep existing placeholder 'ን' circle
- Greeting updates to 'ሰላም, [firstName]' / 'Hello, [firstName]' when
  authenticated

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shows 'Facebook Sign In — በቅርብ ይመጣል' snackbar on tap.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t sync

Highlights were silently rejected by the backend because 'teal' and 'red'
are not valid color names. Backend accepts: yellow, green, pink, blue,
red, purple.
- 0xFF80CBC4 (teal)     → 'green'
- 0xFFEF9A9A (pink-red) → 'pink'

Add 'book' field alongside 'bookId' on both bookmark and highlight
create/update payloads, matching the web FE format. The backend uses
'book' for verse reference lookup; without it bookmarks showed
'verse not found' on the web.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix wrong webClientId in AuthRepository (was pointing to a different
  Google Cloud project entirely, preventing idToken from being issued)
- Add pullAll() to SyncService and wire it before syncAll() on auth
- Add upsertServer* methods to AppDatabase for conflict-safe pulls
- Add fetch methods to SyncRepository to pull bookmarks/highlights/notes
- Trigger pullAll in SavedScreen on open when authenticated
- Align highlight palette colors with backend accepted color names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add 60 new keys across four new sections in app_strings.dart, am_strings.dart,
and en_strings.dart: profile screen, saved/collection screen, forgot password,
and reset password. Update all nine affected screens to use L10n.of(context)
instead of inline Amharic/English literals.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When no user is signed in the header now shows a pill-shaped "Sign In"
button that navigates to the login screen, removing the stray 'ን' initial.
_Avatar is now non-nullable so it only renders for authenticated users.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tons

- Add flutter_svg dependency and declare assets/signin/ in pubspec
- Replace hand-drawn Google logo painter with the official web_light_sq_na /
  web_dark_sq_na SVG assets, switching on theme brightness
- Set Facebook button background to the official Facebook blue (#1877F2)
  instead of the generic dark color

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign out of any cached Google session before calling signIn() so the
account chooser sheet is always presented instead of silently reusing
the last account.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ge, logout

Data layer:
- UserProfile gains `provider` field ('email'|'google') + isGoogleUser helper
- ApiClient gains multipart uploadFile() method
- AuthRepository gains updateProfile(), changePassword(), uploadAvatar()
- AuthNotifier exposes the three new actions and dart:io import

UI:
- ProfileScreen rewritten as ConsumerStatefulWidget with inline editing:
  - Avatar section shows camera badge and 'Change Photo' link for email users;
    Google users see avatar read-only
  - Account Info section with First Name, Last Name, Email fields;
    email field is locked with 🔒 badge for Google accounts + info notice
  - Security section (email users only) — Change Password row opens a
    bottom sheet with current/new/confirm password fields
  - Preferences section with language chips and night-mode toggle
  - Save Changes button appears only when there are unsaved edits
  - Logout button rendered as a subtle pill at the bottom of the screen;
    the ⋯ menu now only contains Delete Account
- MeScreen (Settings tab) gains a Logout button below Reminders,
  visible only when the user is authenticated
- 18 new l10n keys in app_strings / am_strings / en_strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Accept main's saved screen files (PR #8 reviewed version with ids,
  delete support, and i18n)
- Keep auth branch's sync service call in SavedScreen._doLoad()
- Remove auth branch's duplicate saved-screen string keys from
  app_strings / am_strings (superseded by PR #8 keys)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- DB v4 migration: mark synced items with title-case bookId as
  pendingUpdate so they are re-pushed in the correct kebab-case format
- Rewrite upsertServer* to do three-tier upsert: match by remoteId,
  merge by verse ref, or insert as new — prevents silent drops when a
  local pendingCreate item occupies the same verse as a server item
- Add _toApiBookId/_normalizeBookId helpers so push uses kebab-case
  ("genesis") and pull normalises server keys back to local bookNameEn
- Fix createNote/updateNote to send flat fields (bookId, chapter, ...)
  that the server actually requires — nested verseRef format caused 400
- Add 404 fallback in syncBookmarks/Notes/Highlights: stale remoteId
  on PUT triggers a fresh POST instead of silently failing
- Handle null remoteId on pendingUpdate by falling back to create
- Surface HTTP status codes and exception types in sync error logs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ReadingPlan/DailyReading models and ReadingPlanRepository (GET /reading-plans, PATCH day complete)
- Add readingPlansProvider (AsyncNotifierProvider) that fetches when authenticated, returns [] otherwise
- Add patch() method to ApiClient
- Replace hardcoded plan stubs with live data from backend; show auth prompt with login/dismiss when unauthenticated
- Add readingPlansSyncPrompt and continueWithoutAccount l10n strings (AM + EN)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract BookCover to lib/core/widgets/book_cover.dart with coverColor/size params; remove duplicate from continue_reading_section
- Add testamentColor() helper — OT burgundy, NT navy, deuterocanonical olive
- Add startBook to ReadingPlan model for testament detection
- Redesign carousel cards: upright book cover + name + progress, colour from testament
- Add ReadingPlansScreen (full list, same book cover + horizontal card layout)
- Wire View All button to ReadingPlansScreen

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MelakuDemeke and others added 3 commits May 26, 2026 15:49
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Parse DailyReadingItem (book, startChapter, endChapter) from each day's readings
- Add plan_position table to AppDatabase (v5 migration) with get/save methods
- openReadingPlan() resolves saved position → first incomplete day → first day,
  maps kebab book name to BookIndexEntry, saves position, navigates to ReaderScreen
- Wire tap on carousel card and View All list card to openReadingPlan()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings sync:
- Lift ValueNotifier<AppSettings> out of Settings widget into a Riverpod
  settingsNotifierProvider so AuthNotifier can read/write it without BuildContext
- Parse RemoteSettings (theme, fontSize) from UserProfile.fromJson
- On login, apply remote values only for fields still at their default (local wins)
- Start a ValueNotifier listener after login to push settings to PUT /auth/profile
  on every change; stop listener on logout/delete

Streak sync:
- Parse RemoteStreak (current, longest) from UserProfile.fromJson
- After login, merge with local streak — keep whichever value is higher per field
- Invalidate readingStreakStateProvider so the UI reflects the merged count immediately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 26, 2026 13:48
@MelakuDemeke MelakuDemeke merged commit c9f96e9 into main May 26, 2026
1 check failed
@MelakuDemeke MelakuDemeke removed the request for review from Copilot May 26, 2026 14:11
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.

1 participant