# api_provider

> Flutter package: a thin layer over Dio for apps that call a REST API with a bearer token (JWT, OAuth access token, API key). It attaches the token, refreshes it on 401 (once for concurrent failures) and replays requests, stores it securely, restores it on launch, and turns DioException into user-readable messages. Version 2.x. Import only `package:api_provider/api_provider.dart` (common Dio types are re-exported).

Use it when an app has a login and authenticated endpoints. Do not use it for apps that only call public endpoints (plain `dio`/`http` is enough).

## Setup

```yaml
dependencies:
  api_provider: ^2.0.0
```

Create ONE ApiProvider per backend and share it (top-level, or via your DI: get_it, Riverpod, Provider). If the refresh closure refers to `api`, declare the type explicitly (`final ApiProvider api = ...`), otherwise Dart reports `top_level_cycle`.

```dart
import 'package:api_provider/api_provider.dart';

final ApiProvider api = ApiProvider(
  baseUrl: 'https://api.example.com/',            // trailing slash; request paths have none
  refreshTokenProvider: RefreshTokenProvider.from(() async {
    final res = await api.post<Map<String, dynamic>>(
      'auth/refresh',
      data: {'refresh': await loadRefreshToken()},  // the app stores its refresh token itself
      options: skipAuth(),                         // REQUIRED: the refresh call is public
    );
    return res.data?['accessToken'] as String?;    // null => session over
  }),
  onUnauthorized: () => router.go('/login'),       // refresh failed or 401 without a refresher
);
```

## Core API

- Login (public endpoint): `api.post<Map<String, dynamic>>('auth/login', data: {...}, options: skipAuth())`
- Save token: `await api.persistToken(token: t, isLocalSaved: true)` (`isLocalSaved: false` = memory only)
- Restore on start: automatic before the first authenticated request; `await api.hasAuthToken()` returns the token or null if you need it earlier (e.g. to choose the first screen).
- Sign out: `await api.deleteAuthToken()`
- State: `api.isAuthenticated`, `api.authToken`, `api.authTokenListenable` (ValueListenable<String?>; use as go_router `refreshListenable`)
- Requests: `get/post/put/patch/delete/head<T>(path, {data, queryParameters, options, cancelToken, ...})` return `Future<Response<T>>`; `download(url, savePath)`.
- Escape hatch: `api.dio`.
- Per-feature classes: `class UsersProvider extends BaseProvider { UsersProvider(ApiProvider api) : super(httpClient: api.baseApiClient, name: 'Users'); }`

## Errors

Failures throw `DioException`. Extension getters: `userMessage` (show in UI), `serverMessage`, `statusCode`, `isUnauthorized`, `isForbidden`, `isNotFound`, `isConflict`, `isRateLimited`, `isBadRequest`, `isClientError`, `isServerError`, `isTimeout`, `isNetworkError`, `isCancelled`, `isMissingToken`.

```dart
try {
  await api.get<dynamic>('orders');
} on DioException catch (e) {
  showSnackBar(e.userMessage);
}
```

Translate texts: `ApiErrorMessages.current = const ApiErrorMessages(timeout: '...', ...)` or `e.userMessageWith(messages)`. Hide backend texts: `ApiErrorMessages(preferServerMessage: false)`.

## Options (ApiProvider constructor)

`headers`, `connectTimeout` (default 15 s), `sendTimeout` (30 s), `receiveTimeout` (30 s), `enableLogging` (default: debug builds; headers never logged), `interceptors` (run after auth), `refreshOnStatusCodes` (default {401}), `authHeader` (default 'Authorization'), `authScheme` (default 'Bearer'; '' = raw token, e.g. `authHeader: 'X-Api-Key', authScheme: ''`), `tokenStorage` (default SecureTokenStorage), `dio` (its own timeouts are kept).

## Testing

```dart
final api = ApiProvider(baseUrl: 'https://test/', tokenStorage: MemoryTokenStorage(), enableLogging: false);
api.dio.httpClientAdapter = FakeAdapter(); // implements HttpClientAdapter, returns ResponseBody.fromString(...)
```

With the default SecureTokenStorage in widget tests, call `FlutterSecureStorage.setMockInitialValues({})` in setUp.

## Common mistakes

- Missing `skipAuth()` on login/register/refresh endpoints: they fail with `isMissingToken` (no token yet) or the refresh fails with its 401.
- Creating a new ApiProvider per request: tokens and refresh coordination are per instance.
- Calling the API after `api.close()`.
- Expecting the package to store the refresh token: it stores the access token only.

## Docs

- README: https://github.com/iamjunaidkhokhar/api_provider/blob/main/README.md
- Example app + widget test: https://github.com/iamjunaidkhokhar/api_provider/tree/main/example
- Changelog / migration from 1.x: https://github.com/iamjunaidkhokhar/api_provider/blob/main/CHANGELOG.md
