|
| 1 | +# Riderescue API SDK |
| 2 | + |
| 3 | +Flutter/Dart SDK for the Riderescue API. |
| 4 | + |
| 5 | +## Installation |
| 6 | + |
| 7 | +Add to your `pubspec.yaml`: |
| 8 | + |
| 9 | +```yaml |
| 10 | +dependencies: |
| 11 | + riderescue_api: |
| 12 | + git: |
| 13 | + url: https://github.com/riderescue/riderescue_api_sdk.git |
| 14 | + path: packages/dart |
| 15 | + ref: main |
| 16 | +``` |
| 17 | +
|
| 18 | +## Usage |
| 19 | +
|
| 20 | +### Initialization |
| 21 | +
|
| 22 | +```dart |
| 23 | +import 'package:riderescue_api/riderescue_api.dart'; |
| 24 | + |
| 25 | +await Server.init( |
| 26 | + baseUrl: 'https://api.riderescue.com', |
| 27 | + authStrategy: BearerStrategy(tokenKey: 'access_token'), |
| 28 | + defaultCacheTtl: const Duration(minutes: 5), |
| 29 | +); |
| 30 | +``` |
| 31 | + |
| 32 | +### Making API Calls |
| 33 | + |
| 34 | +```dart |
| 35 | +final api = AuthApi(Server.dio); |
| 36 | +
|
| 37 | +final result = await api.login( |
| 38 | + LoginBody( |
| 39 | + email: email, |
| 40 | + password: password, |
| 41 | + ), |
| 42 | +); |
| 43 | +``` |
| 44 | + |
| 45 | +### Error Handling |
| 46 | + |
| 47 | +```dart |
| 48 | +final apiError = ApiErrorResponse.fromJson(error.response?.data); |
| 49 | +
|
| 50 | +final emailError = apiError.firstFieldError(LoginErrorFields.email); |
| 51 | +final passwordError = apiError.firstFieldError(LoginErrorFields.password); |
| 52 | +``` |
| 53 | + |
| 54 | +## Features |
| 55 | + |
| 56 | +- ✅ Dio-based HTTP client |
| 57 | +- ✅ Cookie management |
| 58 | +- ✅ Hive caching |
| 59 | +- ✅ Riverpod state management |
| 60 | +- ✅ Upload progress tracking |
| 61 | +- ✅ Multiple auth strategies (Bearer, API Key, None) |
| 62 | +- ✅ Auto-generated endpoints and DTOs |
| 63 | + |
| 64 | +## Structure |
| 65 | + |
| 66 | +- `lib/src/client/` - Manually owned runtime API client code |
| 67 | +- `lib/src/generated/` - Auto-generated endpoints, DTOs, and clients |
| 68 | + |
| 69 | +## Development |
| 70 | + |
| 71 | +To regenerate the generated code: |
| 72 | + |
| 73 | +```bash |
| 74 | +cd sdk/generators |
| 75 | +pnpm generate:dart |
| 76 | +``` |
| 77 | +# Dependencies to add to pubspec.yaml |
| 78 | + |
| 79 | +```yaml |
| 80 | +dependencies: |
| 81 | + dio: ^5.7.0 |
| 82 | + dio_cookie_manager: ^3.1.1 |
| 83 | + cookie_jar: ^4.0.8 |
| 84 | + hive_flutter: ^1.1.0 |
| 85 | + flutter_riverpod: ^2.5.1 |
| 86 | + path_provider: ^2.1.4 |
| 87 | + |
| 88 | +dev_dependencies: |
| 89 | + build_runner: ^2.4.13 |
| 90 | + hive_generator: ^2.0.1 |
| 91 | +``` |
| 92 | +
|
| 93 | +## main.dart |
| 94 | +
|
| 95 | +```dart |
| 96 | +void main() async { |
| 97 | + WidgetsFlutterBinding.ensureInitialized(); |
| 98 | + |
| 99 | + await Server.init( |
| 100 | + baseUrl: 'https://api.riderescue.com', |
| 101 | + authStrategy: BearerStrategy(tokenKey: 'access_token'), |
| 102 | + defaultCacheTtl: Duration(minutes: 5), |
| 103 | + ); |
| 104 | + |
| 105 | + runApp(const ProviderScope(child: MyApp())); |
| 106 | +} |
| 107 | +``` |
| 108 | + |
| 109 | +## Screen usage examples |
| 110 | + |
| 111 | +```dart |
| 112 | +// GET with cache |
| 113 | +final result = await ref.read(apiProvider.notifier).send( |
| 114 | + GetRequest( |
| 115 | + endpoint: V1.brands, |
| 116 | + version: ApiVersion.v1, |
| 117 | + fromJson: (j) => BrandsResponse.fromJson(j as Map), |
| 118 | + cache: true, |
| 119 | + cacheTtl: Duration(minutes: 10), |
| 120 | + ), |
| 121 | +); |
| 122 | +result.when( |
| 123 | + success: (data, message, _) => showSnackbar(message), |
| 124 | + error: (error, message, _) => showSnackbar(message), |
| 125 | +); |
| 126 | +
|
| 127 | +// POST |
| 128 | +final result = await ref.read(apiProvider.notifier).send( |
| 129 | + PostRequest( |
| 130 | + endpoint: V1.login, |
| 131 | + version: ApiVersion.v1, |
| 132 | + body: {'email': email, 'password': password}, |
| 133 | + fromJson: AuthSessionResponse.fromJson, |
| 134 | + noAuth: true, |
| 135 | + ), |
| 136 | +); |
| 137 | +
|
| 138 | +// Force refresh — bypasses cache |
| 139 | +await ref.read(apiProvider.notifier).send( |
| 140 | + GetRequest( |
| 141 | + endpoint: V1.brands, |
| 142 | + version: ApiVersion.v1, |
| 143 | + fromJson: (j) => BrandsResponse.fromJson(j as Map), |
| 144 | + forceRefresh: true, |
| 145 | + ), |
| 146 | +); |
| 147 | +
|
| 148 | +// Invalidate cache for a whole endpoint family |
| 149 | +await ApiCache.invalidateWhere('/v1/brands'); |
| 150 | +
|
| 151 | +// Upload with progress bar |
| 152 | +ref.read(uploadProvider.notifier).upload( |
| 153 | + UploadRequest( |
| 154 | + endpoint: V1.upload, |
| 155 | + version: ApiVersion.v1, |
| 156 | + fromJson: (j) => UploadResponse.fromJson(j as Map), |
| 157 | + files: [UploadFile.fromPath(field: 'photo', path: filePath)], |
| 158 | + fields: {'vehicleId': id}, |
| 159 | + ), |
| 160 | +); |
| 161 | +
|
| 162 | +// In widget tree — watch progress |
| 163 | +final progress = ref.watch(uploadProgressStreamProvider); |
| 164 | +progress.when( |
| 165 | + data: (p) => LinearProgressIndicator(value: p.percent), |
| 166 | + loading: () => const SizedBox.shrink(), |
| 167 | + error: (_, __) => const SizedBox.shrink(), |
| 168 | +); |
| 169 | +
|
| 170 | +// Listen to auth events anywhere |
| 171 | +AuthEvents.onUnauthorized.listen((e) { |
| 172 | + // refresh token or logout |
| 173 | +}); |
| 174 | +
|
| 175 | +// Cookie helpers |
| 176 | +final cookies = CookieManager(); |
| 177 | +final hasToken = await cookies.has('refresh_token'); |
| 178 | +await cookies.clearAll(); // on logout |
| 179 | +CookieEvents.onCookieSet('refresh_token').listen((_) { |
| 180 | + // cookie was just set by backend |
| 181 | +}); |
| 182 | +
|
| 183 | +// Save token after login (app creator does this) |
| 184 | +await BearerStrategy.saveToken(token, key: 'access_token'); |
| 185 | +
|
| 186 | +// Full logout |
| 187 | +await Server.logout(tokenKey: 'access_token'); |
| 188 | +``` |
| 189 | + |
| 190 | +--- |
| 191 | + |
| 192 | +**Complete `lib/server/` plan** |
| 193 | + |
| 194 | +**Init — once at app start** |
| 195 | + |
| 196 | +```dart |
| 197 | +Server.init( |
| 198 | + baseUrl: 'https://api.riderescue.com', |
| 199 | + defaultVersion: ApiVersion.v1, |
| 200 | + authStrategy: BearerStrategy(tokenKey: 'access_token'), |
| 201 | + cacheTtl: Duration(minutes: 5), |
| 202 | + apiKey: null, // future |
| 203 | +); |
| 204 | +``` |
| 205 | + |
| 206 | +**Folder structure — final** |
| 207 | + |
| 208 | +``` |
| 209 | +lib/server/ |
| 210 | + api/ |
| 211 | + api_client.dart — Dio instance, version routing, interceptors |
| 212 | + api_request.dart — all request types including upload |
| 213 | + api_result.dart — ApiResult<T> with data + message + success |
| 214 | + api_provider.dart — single Riverpod notifier screens talk to |
| 215 | + api_cache.dart — L1 memory + L2 Hive, TTL, manual invalidation |
| 216 | + api_endpoints.dart — endpoint constants + deprecation markers |
| 217 | + api_versions.dart — version enum + deprecation mechanism |
| 218 | + auth/ |
| 219 | + auth_strategy.dart — abstract + Bearer + Cookie + ApiKey(future) |
| 220 | + auth_config.dart — token key constant, api key placeholder |
| 221 | + cookies/ |
| 222 | + cookie_manager.dart — CookieJar wrapper, get/set/clear/clearByName |
| 223 | + cookie_events.dart — streams: onCookiesChanged, onCookiesCleared, onCookieSet |
| 224 | + upload/ |
| 225 | + upload_provider.dart — upload notifier with progress stream |
| 226 | + upload_progress.dart — UploadProgress model (sent, total, percent, isDone) |
| 227 | + models/ — already exists, auto-generated |
| 228 | + server.dart — barrel export + Server.init() |
| 229 | +``` |
| 230 | + |
| 231 | +**Request types summary** |
| 232 | + |
| 233 | +``` |
| 234 | +ApiRequest.get() — endpoint, version, query, cache config, fromJson |
| 235 | +ApiRequest.post() — endpoint, version, body, fromJson |
| 236 | +ApiRequest.put() — endpoint, version, body, fromJson |
| 237 | +ApiRequest.patch() — endpoint, version, body, fromJson |
| 238 | +ApiRequest.delete() — endpoint, version, body, fromJson |
| 239 | +ApiRequest.upload() — endpoint, version, fields, files, fromJson |
| 240 | +``` |
| 241 | + |
| 242 | +**Cache** |
| 243 | + |
| 244 | +``` |
| 245 | +L1 memory — always, instant |
| 246 | +L2 Hive — persistent, own named box: 'server_cache' |
| 247 | +TTL — app-level default constant, per-request override |
| 248 | +Invalidate — by exact key or pattern |
| 249 | +``` |
| 250 | + |
| 251 | +**Auth** |
| 252 | + |
| 253 | +``` |
| 254 | +BearerStrategy — memory first → Hive fallback, key is app constant |
| 255 | +CookieStrategy — Dio CookieJar fully automatic |
| 256 | +ApiKeyStrategy — future, compile-time constant, all requests, opt-out per request |
| 257 | +Screens — can only opt out via noAuth: true |
| 258 | +Token refresh — app responsibility, layer emits onUnauthorized stream |
| 259 | +``` |
| 260 | + |
| 261 | +**Cookie streams** |
| 262 | + |
| 263 | +``` |
| 264 | +onCookiesChanged — any add/update |
| 265 | +onCookiesCleared — full clear |
| 266 | +onCookieSet(name) — specific cookie set |
| 267 | +``` |
| 268 | + |
| 269 | +**Auth event streams** |
| 270 | + |
| 271 | +``` |
| 272 | +onUnauthorized — 401 |
| 273 | +onForbidden — 403 |
| 274 | +``` |
| 275 | + |
| 276 | +**Upload** |
| 277 | + |
| 278 | +``` |
| 279 | +Progress stream — UploadProgress(sent, total, percent, isDone) |
| 280 | +Cancel token — screen can cancel mid-upload |
| 281 | +Result — same ApiResult<T> as all other requests |
| 282 | +Content-Type — auto-set by request type |
| 283 | +Supports — file path, bytes, stream, mixed FormData |
| 284 | +``` |
| 285 | + |
| 286 | +**ApiResult<T>** |
| 287 | + |
| 288 | +```dart |
| 289 | +ApiResult<T> |
| 290 | + ├── success: bool |
| 291 | + ├── message: String // raw server message for snackbars |
| 292 | + ├── data: T? // parsed model |
| 293 | + ├── statusCode: int |
| 294 | + └── error: String? // logged as warning, shown if needed |
| 295 | +``` |
| 296 | + |
| 297 | +**Screen usage will look like:** |
| 298 | + |
| 299 | +```dart |
| 300 | +// GET with cache |
| 301 | +ref.read(apiProvider.notifier).send( |
| 302 | + GetRequest( |
| 303 | + endpoint: V1.brands, |
| 304 | + version: ApiVersion.v1, |
| 305 | + fromJson: BrandsResponse.fromJson, |
| 306 | + cache: true, |
| 307 | + cacheTtl: Duration(minutes: 10), |
| 308 | + ), |
| 309 | +); |
| 310 | +
|
| 311 | +// POST |
| 312 | +ref.read(apiProvider.notifier).send( |
| 313 | + PostRequest( |
| 314 | + endpoint: V1.login, |
| 315 | + version: ApiVersion.v1, |
| 316 | + body: {'email': email, 'password': password}, |
| 317 | + fromJson: AuthSessionResponse.fromJson, |
| 318 | + ), |
| 319 | +); |
| 320 | +
|
| 321 | +// Upload with progress |
| 322 | +ref.read(uploadProvider.notifier).upload( |
| 323 | + UploadRequest( |
| 324 | + endpoint: V1.uploadPhoto, |
| 325 | + version: ApiVersion.v1, |
| 326 | + files: [UploadFile(field: 'photo', path: filePath)], |
| 327 | + fields: {'vehicleId': id}, |
| 328 | + fromJson: UploadResponse.fromJson, |
| 329 | + ), |
| 330 | +); |
| 331 | +
|
| 332 | +// Watch progress |
| 333 | +final progress = ref.watch(uploadProgressProvider); |
| 334 | +LinearProgressIndicator(value: progress.percent); |
| 335 | +``` |
| 336 | + |
| 337 | +--- |
| 338 | + |
| 339 | +**Are you happy with this full plan?** Once you confirm I'll write each file one at a time in order, complete and ready to copy in. |
| 340 | +# flutter_api_bridge |
0 commit comments