airqo-platform / airqo-platform/AirQo-frontend

[Mobile] Fix: users logged out unexpectedly after login (session persistence)

Open
#3,594 0 comments 0 reactions 1 assignee Claimed by @Hassan-KreateStudio View on GitHub
bug good first issue mobile-app
Dominant language
TypeScript
Stars
23
Forks
48
Avg merge
15h 24m
Merged PRs (30d)
57

Description

## Summary
Users report being logged out shortly after logging in, or after closing and reopening the app. Session tokens are saved via `flutter_secure_storage`, but something in the auth lifecycle is clearing them too aggressively.

## User story
As a logged-in AirQo user,
I want to stay signed in across app restarts,
So I don't have to log in again every time I open the app.

## Background (read first)
Auth flow files:
- `src/mobile/lib/src/app/auth/bloc/auth_bloc.dart` — `AppStarted`, `SessionExpired`, `_clearAuthData`
- `src/mobile/lib/src/app/auth/services/auth_token_storage.dart` — saves token to secure storage
- `src/mobile/lib/src/app/auth/services/auth_helper.dart` — `refreshTokenIfNeeded()`
- `src/mobile/lib/src/app/shared/repository/secure_storage_repository.dart` — Keychain / EncryptedSharedPreferences
- `src/mobile/lib/main.dart` — `_checkTokenExpiryOnResume()` on app resume
- `src/mobile/lib/src/app/shared/repository/base_repository.dart` — 401 → `notifySessionExpired()`
- `src/mobile/lib/src/app/dashboard/repository/user_preferences_repository.dart` — **any 401 on preferences → `notifySessionExpired()`** (suspect)

Current behavior:
1. Login saves token via `AuthTokenStorage.saveAuthToken()` ✅
2. On cold start: `AppStarted` reads token → tries refresh → if refresh fails, **clears all auth data** and emits `GuestUser`
3. On resume: same refresh check can force logout
4. Any API 401 can cascade to global session expiry

## Scope

### In scope
1. **Reproduce and document** the bug with a written test matrix (see below)
2. **Fix root cause(s)** so a valid login persists across:
- App background / foreground
- App kill + cold start
- 30+ minutes idle (token refresh path)
3. **Add automated tests** (unit or bloc tests) for:
- Token present on startup → `AuthLoaded` (refresh succeeds)
- Token present, refresh fails due to network → user stays logged in if token not expired
- Token actually expired + refresh fails → `SessionExpiredState` (expected)
4. **Harden 401 handling** so unrelated 401s don't log users out globally (especially `user_preferences_repository.dart`)

### Out of scope
- Guest-first onboarding UX redesign
- Moving login prompts into individual features (PM/design task)
- Backend token TTL / refresh endpoint changes (file separate issue if needed)
- Social login / OAuth flow changes

## Reproduction test matrix (complete before coding)
| Step | Expected | Actual |
|------|----------|--------|
| Email login → kill app → reopen | Still logged in (`AuthLoaded`) | |
| Login → background 5 min → resume | Still logged in | |
| Login → open Favorites tab | Still logged in, places load | |
| Login → airplane mode → reopen app | Graceful degradation, no false logout if token valid | |
| Login → wait until JWT expires → reopen | Refresh OR prompt re-login (not silent guest) | |

## Acceptance criteria
- [ ] All matrix rows pass on **physical device** (Android + iOS if possible)
- [ ] After successful login, `SecureStorageKeys.authToken` is readable on next app launch
- [ ] Failed **network** during refresh does **not** clear a still-valid token
- [ ] `user_preferences_repository` 401 does not call `notifySessionExpired()` unless response body indicates JWT/session expiry (align with `BaseRepository._isSessionRelated401` pattern)
- [ ] At least 2 automated tests added under `src/mobile/test/`
- [ ] PR includes short "root cause" section in description

## Investigation starting points (likely culprits)
1. `auth_bloc.dart` lines 57–63: refresh failure on startup always clears auth
2. `main.dart` `_checkTokenExpiryOnResume`: refresh `null` triggers `notifySessionExpired()`
3. `user_preferences_repository.dart` `_handleUnauthorized()` — unconditional session expiry on 401
4. Race: login succeeds but a parallel API call returns 401 before token is saved

## Implementation hints
- Consider: on startup, if refresh fails but `JwtDecoder.isExpired(token) == false`, emit `AuthLoaded` anyway (network blip)
- Reuse `BaseRepository._isSessionRelated401` logic in preferences repo instead of blanket logout
- Add logging (existing `loggy`) at each logout path with reason code for future debugging
- Do **not** change secure storage keys without migration plan

## Test plan
- [ ] Manual matrix above on Android emulator + one real device
- [ ] `flutter test` for new auth tests
- [ ] Verify guest mode still works (no token → `GuestUser` on launch)
- [ ] Verify explicit logout still clears storage

## Dependencies / blockers
- [ ] Test account credentials (ask staff — do not commit)
- [ ] Confirm expected refresh token TTL with backend team if refresh always fails after N days

## Estimated effort
4–6 days

## Codebase path
`src/mobile/`

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.