# WeBuddhist App A modern Flutter app for learning, sharing, and live collaboration. ## πŸš€ Project Goal WeBuddhist App is designed to help users learn, live, and share knowledge interactively. It features a beautiful UI, supports both light and dark themes, and is built with best Flutter practices. ## πŸ›  Features - Light and Dark theme support with toggle - Modern Flutter architecture - Easy to customize and extend ## πŸ“¦ Getting Started ### 1. Clone the Repository ```sh git clone https://github.com/your-username/flutter_pecha.git cd flutter_pecha ``` ### 2. Install Dependencies ```sh flutter pub get ``` ### 3. Environment Setup Create environment files from the template: ```sh cp .env.example .env.dev cp .env.example .env.staging cp .env.example .env.prod ``` Edit each file with the appropriate values for that environment. Each flavor should use its own PostHog project token: | Variable | Description | |----------|-------------| | `POSTHOG_API_KEY` | PostHog project API key for this flavor | | `POSTHOG_HOST` | PostHog ingest host (default `https://us.i.posthog.com`) | | `POSTHOG_ENABLED` | Set to `false` to disable analytics locally | Screen views (`$screen`) come from the route observers on both the root and the tab-shell navigator, so every named route is tracked. Product events are declared once in `lib/core/analytics/analytics_events.dart` and follow the team tracking plan (forum post "Current state of analytics in the WeBuddhist app", section 5): `object_verb` names, snake_case properties, never text a user typed. Each feature fires them through a small analytics class (`PlanAnalytics`, `ReaderAnalytics`, `MalaAnalytics`, ...) in its `presentation/utils` folder, so screens never spell out event names or property keys themselves. Every share sheet reports one `content_shared` event with a `surface` and the user's content `language`, only after the sheet was actually used. Microsoft Clarity adds session recordings and per-screen heatmaps on top of PostHog. One Clarity project can serve every flavor: each session is tagged with `environment` and `app_flavor`, so dev traffic can be filtered out on the dashboard. Screens are named after the go_router route name (`reader`, `home-settings`, …), imperatively pushed screens after their `RouteSettings` name, and the four bottom tabs as `tab-home` / `tab-practice` / `tab-connect` / `tab-me`. | Variable | Description | |----------|-------------| | `CLARITY_PROJECT_ID` | Clarity project ID (Clarity dashboard β†’ Settings β†’ Overview). Empty disables Clarity | | `CLARITY_ENABLED` | Set to `false` to disable Clarity locally | ### 4. Run the App **Android** ```sh flutter run --flavor dev -t lib/main_dev.dart flutter run --flavor staging -t lib/main_staging.dart flutter run --flavor prod -t lib/main_prod.dart ``` Build APK: ```sh flutter build apk --flavor dev -t lib/main_dev.dart flutter build apk --flavor staging -t lib/main_staging.dart flutter build apk --flavor prod -t lib/main_prod.dart ``` Build App Bundle: ```sh flutter build appbundle --flavor prod -t lib/main_prod.dart ``` **iOS** ```sh flutter run --flavor dev -t lib/main_dev.dart flutter run --flavor staging -t lib/main_staging.dart flutter run --flavor prod -t lib/main_prod.dart ``` **macOs** ```sh flutter run -d macos --flavor dev --target lib/main_dev.dart flutter run -d macos --flavor staging --target lib/main_staging.dart flutter run -d macos --flavor prod --target lib/main_prod.dart ``` Build IPA: ```sh flutter build ios --flavor prod -t lib/main_prod.dart ``` > **Note:** > - For iOS, ensure you have Xcode installed and have granted the necessary permissions (see [Flutter macOS setup](https://docs.flutter.dev/get-started/install/macos)). > - For Android, ensure Android Studio and an emulator/device are set up. ### 5. Adding Localizations The app uses Flutter's built-in internationalization (l10n) with ARB files for translations. **Localization files location:** `lib/core/l10n/` **To add a new translation string:** 1. Add the key to all ARB files (`app_en.arb`, `app_bo.arb`, `app_zh.arb`): ```json // app_en.arb "my_new_key": "My English text", // app_bo.arb "my_new_key": "My Tibetan text", // app_zh.arb "my_new_key": "My Chinese text", ``` 2. Generate localization files: ```sh flutter gen-l10n dart run tool/generate_tolgee_bridge.dart ``` The second command regenerates the Tolgee bridge so the new key can also be updated over the air. CI fails if it is skipped β€” see [Over-the-air translations](#-over-the-air-translations-tolgee). 3. Use in your widget: ```dart import 'package:flutter_pecha/core/extensions/context_ext.dart'; // Access translation via context.l10n Text(context.l10n.my_new_key) ``` **For translations with parameters:** ```json // app_en.arb "greeting": "Hello {name}", "@greeting": { "placeholders": { "name": {"type": "String"} } } ``` Usage: ```dart Text(context.l10n.greeting('John')) ``` ## 🌍 Over-the-air translations (Tolgee) Translation text can be corrected in [Tolgee](https://tolgee.io) and picked up by installed apps without shipping a new release. Nothing changes at call sites: `context.l10n.my_key` keeps working exactly as before. ### How it works `tool/generate_tolgee_bridge.dart` reads the gen-l10n output and emits `TolgeeAppLocalizations`, a subclass of `AppLocalizations` that overrides every key. Each override asks Tolgee first and falls back to the bundled ARB value. It is registered in `main.dart` in place of `AppLocalizations.delegate`. The ARB files stay the source of truth in git and are the offline fallback; Tolgee is an override layer on top of them. ``` context.l10n.my_key -> TolgeeAppLocalizations.my_key (generated override) -> Tolgee CDN value (if loaded and for the right language) -> bundled ARB value (otherwise) ``` Because the bundled ARB is always the fallback, the app behaves exactly as it did before Tolgee whenever the CDN is unreachable, still fetching, or missing a key. The payload is fetched and parsed by the app rather than by the Tolgee SDK, which cannot serve a multi-part language tag such as `bo-IN`. See [docs/tolgee.md](docs/tolgee.md#why-the-sdk-is-not-used). ### Configuration None. The Content Delivery URL is public and the same for every flavor, so it lives in code as `TolgeeCdn.baseUrl` ([`tolgee_cdn.dart`](lib/core/l10n/tolgee/tolgee_cdn.dart)) and Tolgee is on in every build. There are no `.env` lines or GitHub secrets for it. To fix a bad translation, correct it in Tolgee and publish β€” see [docs/tolgee.md](docs/tolgee.md#a-bad-translation-shipped). > **Never put a Tolgee key in `.env`.** The files are bundled as assets and can > be extracted from a release build. The app needs no key at all; the sync key > below stays in your shell or GitHub secrets. ### Tolgee project requirements - App UI locales use bare codes (`en`, `bo`, `zh`, `hi`, `mn`, `ne`). Tolgee Content Delivery language tags (and CDN filenames) must be `en`, `bo-IN`, `zh-Hant-TW`, `hi`, `mn`, `ne` β€” the app maps `bo`/`zh` to those CDN tags via `TolgeeLocaleMap`. Publishing `bo.json` or `zh.json` will 404. - Enable ICU placeholder support so plural strings are served as ICU source. - Content Delivery must export **flat** JSON (nesting and arrays disabled), one file per language at `/.json`. The SDK's CDN parser expects `{"key": "value"}` and cannot read nested objects. ### When updates apply Translations are fetched on app start, on language change, and when the app returns to the foreground (at most every 5 minutes). An edit published in Tolgee reaches users within minutes of their next launch or return to the app; there is no live push. A cold start with no network shows the bundled ARB text until a later return to the app fetches again. ### ARB sync (manual and CI) Bundled ARB files stay the offline fallback; the Tolgee CDN is the OTA override. Sync uses a write key that must never live in `.env.*`: | Key | Where | Role | | --- | --- | --- | | `TOLGEE_SYNC_API_KEY` | shell env or GitHub Actions secret only | Local / CI ARB ↔ Tolgee sync (write) | You can sync manually with `dart run tool/tolgee_sync.dart ...`, or let the **Tolgee Sync** workflow do it on a schedule / via **Run workflow**. Keep both. CI opens a reviewable PR β€” it does not merge to `develop` by itself. Full command order, write-key creation (Tolgee β†’ Project API Keys β†’ + API Key), rotate-after-exposure guidance, and CI auto-update risks are in [Tolgee integration and sync](docs/tolgee.md). ## βš™οΈ Project Structure This project follows **Clean Architecture** principles with clear separation of concerns across three main layers. ### Architecture Overview ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ PRESENTATION LAYER β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ UI Components (Screens, Widgets) β”‚ β”‚ β”‚ β”‚ State Management (Riverpod Notifiers/Providers) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ ↓ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ depends on β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DOMAIN LAYER β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Entities (Business Objects) β”‚ β”‚ β”‚ β”‚ Use Cases (Business Logic) β”‚ β”‚ β”‚ β”‚ Repository Interfaces (Contracts) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ ↓ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ depends on β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DATA LAYER β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Repository Implementations β”‚ β”‚ β”‚ β”‚ Data Sources (API, Local Storage) β”‚ β”‚ β”‚ β”‚ Models (DTOs for serialization) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ↓ depends on β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ EXTERNAL SERVICES β”‚ β”‚ (Auth0, Firebase, HTTP, Storage, etc.) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### Folder Structure ``` lib/ β”œβ”€β”€ core/ # Shared infrastructure & utilities β”‚ β”œβ”€β”€ config/ # App configuration β”‚ β”œβ”€β”€ di/ # Dependency injection β”‚ β”œβ”€β”€ error/ # Error handling (Failures) β”‚ β”œβ”€β”€ network/ # Network utilities β”‚ β”œβ”€β”€ services/ # External service integrations β”‚ β”œβ”€β”€ storage/ # Local storage utilities β”‚ └── theme/ # App theming β”‚ β”œβ”€β”€ features/ # Feature-based modules β”‚ └── auth/ # ← Authentication feature example β”‚ β”œβ”€β”€ domain/ # Business logic (no dependencies) β”‚ β”œβ”€β”€ data/ # Data implementation β”‚ └── presentation/ # UI & state management β”‚ └── shared/ # Cross-cutting concerns β”œβ”€β”€ data/ # Shared data layer utilities β”œβ”€β”€ domain/ # Shared domain logic β”œβ”€β”€ presentation/ # Shared UI components └── widgets/ # Reusable widgets ``` ### Auth Flow Example Here's how authentication follows clean architecture through all layers: #### 1. Domain Layer (`features/auth/domain/`) *Pure business logic with no framework dependencies* ``` domain/ β”œβ”€β”€ entities/ β”‚ └── user.dart # User business entity β”œβ”€β”€ repositories/ β”‚ └── auth_repository.dart # Repository interface (contract) └── usecases/ β”œβ”€β”€ login_usecase.dart # Login business logic β”œβ”€β”€ get_current_user_usecase.dart └── logout_usecase.dart ``` **Key points:** - `User` entity: Pure Dart class with business properties - `AuthRepository`: Abstract interface defining data operations - `LoginUseCase`: Orchestrates login logic, returns `Either` #### 2. Data Layer (`features/auth/data/`) *Implements domain contracts, handles external dependencies* ``` data/ β”œβ”€β”€ models/ β”‚ └── user_model.dart # DTO for JSON serialization β”œβ”€β”€ datasources/ β”‚ └── auth_remote_datasource.dart # API/Service calls └── repositories/ └── auth_repository_impl.dart # Implements AuthRepository ``` **Key points:** - `UserModel`: Handles JSON ↔ Dart conversion - `AuthRemoteDataSource`: Makes actual API calls - `AuthRepositoryImpl`: Implements domain interface, uses datasource #### 3. Presentation Layer (`features/auth/presentation/`) *UI and state management* ``` presentation/ β”œβ”€β”€ providers/ β”‚ β”œβ”€β”€ auth_notifier.dart # State management β”‚ β”œβ”€β”€ auth_providers.dart # DI setup β”‚ └── use_case_providers.dart # Use case providers β”œβ”€β”€ screens/ β”‚ └── login_page.dart # Login UI └── widgets/ └── login_form.dart # Reusable form widget ``` **Key points:** - `AuthNotifier`: Manages auth state, calls use cases - `authProviders`: Wire up dependencies using Riverpod - `LoginPage`: UI that consumes state via providers ### Key Principles | Layer | Responsibility | Dependencies | |-------|---------------|--------------| | **Domain** | Business rules & logic | None (pure Dart) | | **Data** | Data sources & persistence | Domain, external services | | **Presentation** | UI & state management | Domain (via use cases) | ### Benefits - **Testable**: Each layer can be unit tested independently - **Maintainable**: Changes in one layer don't break others - **Scalable**: Easy to add new features following same pattern - **Flexible**: Swap implementations (e.g., change API) without affecting business logic ## Dio Implementation Dio is a powerful HTTP client for Dart. Think of it as a supercharged `http` package with built-in support for interceptors, retries, timeout handling, and request transformation. ### Why Dio Over `http` Package? | Feature | `http` package | Dio | |---------|----------------|-----| | Interceptors | No | Yes (we use this heavily) | | Global configuration | Limited | Full | | Automatic retries | Manual | Built-in | ### Our DioClient Structure **Location**: `lib/core/network/dio_client.dart` ```dart class DioClient { DioClient({ required AuthInterceptor authInterceptor, required RetryInterceptor retryInterceptor, // ... other interceptors }) : _dio = Dio(options) { // Interceptor order is critical _dio.interceptors.addAll([ authInterceptor, // 1. Add auth headers first cacheInterceptor, // 2. Check cache retryInterceptor, // 3. Handle 401 & network errors errorInterceptor, // 4. Convert errors loggingInterceptor, // 5. Log final result ]); } } ``` **Why this order?** Each interceptor processes the request in sequence. Auth must run first to add tokens before the request goes out. ### Interceptor Chain: How Requests Flow ``` Request: Auth β†’ Cache β†’ Retry β†’ Error β†’ Log β†’ Server Response: Log β†’ Error β†’ Retry β†’ Cache β†’ Auth β†’ UI ``` ### Interceptor 1: AuthInterceptor **What it does**: Checks if the API endpoint requires authentication, adds the auth token. ```dart // lib/core/network/interceptors/auth_interceptor.dart @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) async { // Only add auth for protected routes if (ProtectedRoutes.isProtected(options.path)) { final token = await _tokenProvider.getToken(); if (token != null) { options.headers['Authorization'] = 'Bearer $token'; } } handler.next(options); // Pass to next interceptor } ``` **Key point**: Uses `TokenProvider` abstraction so we can swap token sources without changing this code. ### Interceptor 2: RetryInterceptor **What it does**: Handles 401 (token expired) errors by refreshing the token and retrying the request. ```dart // lib/core/network/interceptors/retry_interceptor.dart @override void onError(DioException err, ErrorInterceptorHandler handler) async { // Handle 401 - Token expired if (err.response?.statusCode == 401) { if (_isRefreshing) { // Already refreshing, queue this request _refreshQueue.add(_RetryRequest(err, handler)); return; } _isRefreshing = true; final newToken = await _authService.refreshIdToken(); // Retry all queued requests with new token for (final request in _refreshQueue) { request.error.requestOptions.headers['Authorization'] = 'Bearer $newToken'; // Retry the request... } } // Retry network errors with exponential backoff if (_shouldRetry(err)) { await Future.delayed(Duration(milliseconds: 1000 * (1 << retryCount))); // Retry... } } ``` ## OAuth Implementation ### What is OAuth 2.0? OAuth 2.0 is an authorization framework that lets users grant limited access to their accounts without sharing passwords. **Real-world analogy**: Like giving a valet key to your car - it can only drive the car, not open the trunk. ### Why OAuth 2.0? 1. **Security**: User never shares password with your app 2. **Control**: User can revoke access anytime 3. **Standardization**: Industry-wide protocol 4. **Social Login**: Leverage existing accounts ### OAuth 2.0 Roles ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ User β”‚ β”‚ Auth0 Server β”‚ β”‚ (Resource β”‚ β”‚ β”‚ β”‚ Owner) β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ 1. Tap "Login with Google" β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ β”‚ β”‚ β”‚ 2. Show Google login page β”‚ │◄──────────────────────────────── β”‚ β”‚ β”‚ 3. User authenticates β”‚ β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Ίβ”‚ β”‚ β”‚ β”‚ 4. Return tokens β”‚ │◄──────────────────────────────── β”‚ β”‚ β”‚ (Access Token, ID Token, β”‚ β”‚ Refresh Token) β”‚ ``` ### Our Auth0 Implementation **Location**: `lib/features/auth/auth_service.dart` ```dart class AuthService { final Auth0 _auth0 = Auth0('YOUR_DOMAIN', 'YOUR_CLIENT_ID'); Future loginWithGoogle() async { final credentials = await _auth0.webAuthentication(scheme: 'org.pecha.app') .login( useHTTPS: true, parameters: {"connection": "google-oauth2"}, scopes: {"openid", "profile", "email", "offline_access"}, ); // Auth0 SDK handles PKCE automatically // Credentials contain: accessToken, idToken, refreshToken, expiresIn await _auth0.credentialsManager.storeCredentials(credentials); return credentials; } } ``` **What scopes mean**: - `openid`: Enables OIDC protocol - `profile`: Access to user profile data - `email`: Access to user email - `offline_access`: Enables refresh tokens ### Login Flow: Step by Step **Step 1: UI - Login Button** ```dart // lib/features/auth/presentation/widgets/auth_buttons.dart ElevatedButton( onPressed: () { ref.read(authProvider.notifier).login(connection: 'google-oauth2'); }, child: Text('Login with Google'), ) ``` **Step 2: AuthNotifier - State Management** ```dart // lib/features/auth/presentation/providers/auth_notifier.dart Future login({String? connection}) async { state = state.copyWith(isLoading: true); final result = await _loginUseCase(LoginParams(connection: connection)); result.fold( (failure) => state = state.copyWith(errorMessage: failure.message), (credentials) => _handleSuccessfulLogin(credentials), ); } ``` **Step 3: UseCase - Orchestration** ```dart // lib/features/auth/domain/usecases/login_usecase.dart Future> call(LoginParams params) async { switch (params.connection) { case 'google-oauth2': return await _repository.loginWithGoogle(); case 'apple': return await _repository.loginWithApple(); } } ``` **Step 4: Repository - Data Layer** ```dart // lib/features/auth/data/repositories/auth_repository_impl.dart Future> loginWithGoogle() async { try { final credentials = await _authService.loginWithGoogle(); return Right(_toAuthCredentials(credentials)); } catch (e) { return Left(AuthenticationFailure('Login failed')); } } ``` **Step 5: AuthService - Auth0 Integration** ```dart // lib/features/auth/auth_service.dart Future loginWithGoogle() async { return _loginWithConnection('google-oauth2'); } Future _loginWithConnection(String connection) async { final credentials = await _auth0.webAuthentication(scheme: 'org.pecha.app') .login( useHTTPS: true, parameters: {"connection": connection}, scopes: {"openid", "profile", "email", "offline_access"}, ); await _auth0.credentialsManager.storeCredentials(credentials); return credentials; } ``` ### Auth State & Navigation **Location**: `lib/core/config/router/go_router.dart` ```dart final goRouterProvider = Provider((ref) { return GoRouter( refreshListenable: GoRouterRefreshStream(ref.watch(authProvider.notifier).stream), redirect: (context, state) async { final authState = ref.watch(authProvider); // Unauthenticated trying to access protected route if (!authState.isLoggedIn && RouteConfig.isProtectedRoute(currentPath)) { return RouteConfig.login; // Redirect to login } // Authenticated user on login page if (authState.isLoggedIn && currentPath == RouteConfig.login) { return RouteConfig.home; // Redirect to home } return null; // No redirect }, ); }); ``` **How it works**: - Router watches `authProvider` for state changes - When auth state changes, router re-evaluates redirect logic - Automatically redirects based on auth status ### Logout Flow ```dart // lib/features/auth/presentation/providers/auth_notifier.dart Future logout() async { // 1. Clear credentials from storage await _localLogoutUseCase(const NoParams()); // 2. Clear user data await ref.read(userProvider.notifier).clearUser(); // 3. Update state state = state.copyWith(isLoggedIn: false); // 4. Router automatically redirects to login } ``` ### App Launch: Auth State Restoration ```dart // lib/features/auth/presentation/providers/auth_notifier.dart AuthNotifier(...) : super(const AuthState(isLoading: true)) { _restoreLoginState(); // Runs immediately on creation } Future _restoreLoginState() async { // 1. Check for valid credentials final hasCredentials = await _hasValidCredentialsUseCase(); if (hasCredentials) { // 2. Restore user data state = state.copyWith(isLoggedIn: true, isLoading: false); ref.read(userProvider.notifier).initializeUser(); } else { // 3. Check for guest mode final isGuest = await _isGuestModeUseCase(); state = state.copyWith(isLoggedIn: isGuest, isGuest: isGuest, isLoading: false); } } ``` ### Complete Flow Diagram #### End-to-End: From Login to API Call ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ AUTHENTICATION & API CALL FLOW β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ APP LAUNCH β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ AuthNotifier created β”‚ β”‚ (isLoading: true) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Check stored creds β”‚ β”‚ (CredentialsManager) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ Has Creds No Creds β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ isLoggedIn=true β”‚ β”‚ Check guest mode β”‚ β”‚ isLoading=false β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β” β”‚ Was Guest? Not Guest β”‚ β”‚ β”‚ β–Ό β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Show Home β”‚ β”‚Show Home β”‚ β”‚Show Loginβ”‚ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ USER TAPS LOGIN BUTTON β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ AuthNotifier.login() called β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ LoginUseCase called β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ AuthRepository.loginWithGoogle() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ AuthService.loginWithGoogle() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Auth0 Web Auth opens β”‚ β”‚ (PKCE flow handled by SDK) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ User authenticates with Google β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Credentials returned β”‚ β”‚ (access, id, refresh tokens) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Stored in CredentialsManager β”‚ β”‚ (Keychain/Keystore) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ AuthNotifier state updated β”‚ β”‚ (isLoggedIn: true) β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ GoRouter redirects to Home β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ User fetches their plans β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ UserPlansNotifier.fetchPlans() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ GetPlansUseCase called β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Repository.getUserPlans() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DataSource calls Dio.get() β”‚ β”‚ URL: /users/me/plans β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ REQUEST INTERCEPTOR CHAIN β”‚ β”‚ β”‚ β”‚ 1. AuthInterceptor β”‚ β”‚ - Path is protected? YES β”‚ β”‚ - Get token from Provider β”‚ β”‚ - Provider calls AuthService β”‚ β”‚ - AuthService checks expiry β”‚ β”‚ - If expired, refreshes β”‚ β”‚ - Returns valid token β”‚ β”‚ - Adds Authorization header β”‚ β”‚ β”‚ β”‚ 2. CacheInterceptor β”‚ β”‚ - Not in cache, proceed β”‚ β”‚ β”‚ β”‚ 3. RetryInterceptor β”‚ β”‚ - No error, proceed β”‚ β”‚ β”‚ β”‚ 4. Send to server β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ SERVER RESPONSE: 200 OK β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ RESPONSE INTERCEPTOR CHAIN β”‚ β”‚ β”‚ β”‚ 1. RetryInterceptor β”‚ β”‚ - No 401, proceed β”‚ β”‚ β”‚ β”‚ 2. CacheInterceptor β”‚ β”‚ - Store in cache β”‚ β”‚ β”‚ β”‚ 3. ErrorInterceptor β”‚ β”‚ - No error, proceed β”‚ β”‚ β”‚ β”‚ 4. LoggingInterceptor β”‚ β”‚ - Log success β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ DataSource parses JSON β”‚ β”‚ Returns List β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Repository maps to entities β”‚ β”‚ Returns Either β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ UseCase returns Either β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Notifier folds Either β”‚ β”‚ Updates state with data β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ UI rebuilds with plans data β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ### 401 Error Flow ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ 401 TOKEN EXPIRED FLOW β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ API Request with expired token β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Server returns 401 Unauthorized β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ RetryInterceptor.onError() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Check: Has valid credentials? β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ YES NO β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Check if β”‚ β”‚ Pass error β”‚ β”‚ already β”‚ β”‚ through β”‚ β”‚ refreshing? β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β” β”‚ β”‚ Refreshing Not refreshing β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Queue β”‚ β”‚ Set refreshing = β”‚ β”‚ request β”‚ β”‚ true β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Call AuthService β”‚ β”‚ .refreshIdToken() β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Auth0 API renews β”‚ β”‚ using refresh token β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ Success Failure β”‚ β”‚ β–Ό β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Store new β”‚ β”‚ onAuthExpired β”‚ β”‚ credentials β”‚ β”‚ callback β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β†’ Logout β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Retry queued β”‚ β”‚ requests β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Retry originalβ”‚ β”‚ request β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Set refreshingβ”‚ β”‚ = false β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` ## 🀝 Contributing Pull requests are welcome! For major changes, please open an issue first to discuss what you would like to change. ## πŸ“„ License [MIT](LICENSE) **Table of Contents Screen** - Browse through organized text content lists with ease - View all available versions of each text in one convenient location **Reader Screen** - Immersive reading experience with optimized formatting