# Flutter App Architecture — Part 4

Mobile client for the Core-PHP backend (`../api`). This document is the
contract between app and backend; the `lib/` scaffold implements it.

## Stack

| Concern | Choice | Why |
|---|---|---|
| State / DI | **Riverpod** (`flutter_riverpod`) | compile-safe DI, testable, no `BuildContext` coupling. *The one swappable call — Bloc/Provider are viable; the layering below is unaffected.* |
| HTTP | **Dio** | interceptors (auth, envelope, logging), cancellation, retry |
| Routing | **go_router** | declarative + a single `redirect` auth guard |
| Secure storage | **flutter_secure_storage** | Keychain/Keystore for tokens only |
| Auth (social) | **firebase_auth** | Google/Apple → **Firebase ID token** → `POST /auth/social-login` |
| Push | **firebase_messaging** | FCM token → `POST /notifications/register-device` |
| Checkout | **url_launcher / webview** | open `cart.checkout_url`; **no card data in app** |

## Layering

```
lib/
  core/
    env.dart                  build-time config (--dart-define)
    api/
      api_response.dart       the { success,message,data,errors,meta } envelope
      api_exception.dart      typed failure (status, message, field errors)
      api_client.dart         Dio factory + envelope + error interceptors
    auth/
      token_store.dart        secure access/refresh persistence
      auth_interceptor.dart   Bearer attach + single-flight refresh ROTATION
      session.dart            Riverpod auth state (unauth/authed)
    push/push_service.dart    FCM token registration
    router.dart               go_router + auth redirect
  data/
    models/                   user, page<T>, product, cart …
    repositories/             auth, catalog, cart … (return unwrapped data)
  features/
    auth/        otp_controller + login/otp screens (the reference pattern)
    catalog/     home, product_list (paged), product_detail, search
    cart/        cart_screen (Shopify checkout handoff)
    wishlist/ orders/ profile/ notifications/
    shell/       home_shell (bottom-tab IndexedStack)
  app.dart  main.dart

All feature screens follow the auth pattern: a Riverpod provider (Future/
StateNotifier) → repository → `AsyncView<T>` (uniform loading/error+retry;
maps ApiException incl. Shopify 502/503). Paged lists infinite-scroll on the
`end_cursor`. Cart id persists in secure storage (guest-friendly).
```

Rule of dependency: `features → data → core`. Never the reverse. UI never
touches Dio or the envelope directly — only repositories do.

## Backend contract (must match exactly)

**Envelope** — every response is
`{ success, message, data, errors, meta }`. `ApiClient` unwraps `data` on
success and throws `ApiException(message, statusCode, fieldErrors)` otherwise.
`errors` is a `{field: [msgs]}` map (422) surfaced to forms.

**Auth** — `Authorization: Bearer <access>`. Access token ~15 min; refresh
token is **opaque and rotates on every `/auth/refresh`** and the backend
does reuse-detection (a replayed refresh token revokes the whole family). So
the client MUST:
- send the refresh token at most once per rotation,
- **single-flight** refresh: concurrent 401s await one refresh, then retry,
- persist the *new* refresh token atomically, and on refresh failure clear
  the session and route to login.

**Pagination** — cursor based. List `meta = { limit, has_next, end_cursor }`;
pass `?cursor=<end_cursor>&limit=`. `Page<T>` model wraps this.

**Catalog/cart/orders** are Shopify-backed and may return `503`
("store not connected") or `502` — show a soft "temporarily unavailable",
not a crash. Wishlist list degrades with `meta.catalog_unavailable=true`.

**Checkout** — never collected in-app. `POST /cart/create` → keep `cart.id`;
mutate via add/update/remove; at checkout open `cart.checkout_url` (Shopify
hosted) externally and poll `/orders` after return.

**Social login** — `firebase_auth` sign-in → `user.getIdToken()` → send as
`id_token` with `provider: google|apple`. Backend returns `503` only if
Firebase isn't configured, `401` if the token is rejected.

**Push** — on launch + token refresh, send `device_id` (stable app id),
`platform`, `fcm_token` to `/notifications/register-device` (auth-optional;
re-send after login to bind the device to the user).

## Endpoint map (selected)

| Flow | Calls |
|---|---|
| OTP login | `POST /auth/send-otp` → `POST /auth/verify-otp` → store tokens |
| Session keepalive | `POST /auth/refresh` (interceptor, on 401) |
| Logout | `POST /auth/logout` (`{refresh_token}` or `{all:true}`) |
| Home | `GET /home`, `GET /banners` |
| Browse | `GET /products?collection=&sort=&cursor=`, `GET /product/{id}` |
| Search/filter | `GET /search?q=`, `POST /filter` |
| Cart | `POST /cart/create|add|update|remove`, `GET /cart?cart_id=` |
| Wishlist | `GET/POST /wishlist*` |
| Orders | `GET /orders`, `GET /order/{id}`, `GET /order/track/{id}` |
| Profile | `GET /profile`, `POST /profile/update`, address CRUD |
| Notifications | `GET /notifications`, `POST /notifications/read` |

## Configuration

No secrets in the bundle. Inject at build:

```
flutter run --dart-define=API_BASE_URL=https://app.indiansilkhouseagencies.com \
            --dart-define=API_PREFIX=/api/v1
```

`Env` reads `String.fromEnvironment`. Firebase config via the standard
`flutterfire configure` (`firebase_options.dart`, git-ignored).

## Error & UX rules

- 422 → map `errors` onto form fields.
- 401 after refresh fails → clear session, go to `/login`.
- 403 → "blocked/forbidden" message; 404 → empty state.
- 502/503 on Shopify surfaces → retry affordance, keep app usable.
- All network on a `Result`/`AsyncValue`; no raw exceptions to widgets.

## Testing

- Unit: repositories with a mocked Dio adapter (golden envelope fixtures).
- The refresh-rotation interceptor has dedicated tests: concurrent-401
  single-flight, rotation persistence, reuse → forced logout.
- Widget: OTP flow against a fake `AuthRepository`.

## Caveat

No Flutter SDK in the authoring environment — this scaffold is structured and
syntax-reviewed but **not `flutter analyze`/compile-verified** (the analogue
of the backend's `php -l`-only gap). First `flutter pub get` + `analyze` is
the first real validation; pin versions in `pubspec.yaml` then.
