Line data Source code
1 : import 'dart:async';
2 : import 'dart:convert';
3 : import 'package:amadeus_proto/api/utils/api_cache.dart';
4 : import 'package:amadeus_proto/api/utils/api_error.dart';
5 : import 'package:amadeus_proto/api/utils/api_reactions.dart';
6 : import 'package:amadeus_proto/api/utils/api_response.dart';
7 : import 'package:either_dart/either.dart';
8 : import 'package:flutter/material.dart';
9 : import 'package:http/http.dart' as http;
10 : import 'package:http/http.dart';
11 : import 'package:provider/provider.dart';
12 :
13 : /// Class simplifying Web API calls
14 : class ApiBase extends ChangeNotifier {
15 : String? _accessToken;
16 : String? _refreshToken;
17 : final Client _client;
18 : String? _baseUrl;
19 : Map<String, String>? _defaultHeaders;
20 : BuildContext? _currentBuildContext;
21 : late final ApiReactions _apiReactions;
22 :
23 : ApiCache cache = ApiCache();
24 :
25 0 : BuildContext? get context => _currentBuildContext;
26 :
27 : /// Constructor for [ApiBase]
28 : /// If a [Client] isn't provided, the default [Client] is an [http.Client].
29 : /// If a [ApiReactions] isn't provided, one will be created.
30 6 : ApiBase({Client? client, ApiReactions? apiReactions})
31 1 : : _client = client ?? http.Client(),
32 5 : _apiReactions = apiReactions ?? ApiReactions();
33 :
34 : /// Set the access token
35 : ///
36 : /// If the token is invalid, one of those [Exception] will be throw:
37 : /// - `Invalid JWT format: Expected 3 parts, got [number of parts]`
38 : /// - `Unsupported signature algorithm: [algorithm]`
39 : /// - `Token expired at [DateTime]`
40 : /// - `Token issued in the future at [DateTime]`
41 2 : set accessToken(String? value) {
42 : if (value != null) {
43 2 : _verifyJWTFormat(value);
44 : }
45 2 : _accessToken = value;
46 2 : notifyListeners();
47 : }
48 :
49 2 : String? get accessToken => _accessToken;
50 :
51 : /// Set the refresh token
52 : ///
53 : /// If the token is invalid, one of those [Exception] will be throw:
54 : /// - `Invalid JWT format: Expected 3 parts, got [number of parts]`
55 : /// - `Unsupported signature algorithm: [algorithm]`
56 : /// - `Token expired at [DateTime]`
57 : /// - `Token issued in the future at [DateTime]`
58 2 : set refreshToken(String? value) {
59 : if (value != null) {
60 2 : _verifyJWTFormat(value);
61 : }
62 2 : _refreshToken = value;
63 : }
64 :
65 6 : String? get refreshToken => _refreshToken;
66 :
67 : /// Set base URL for Web API calls.
68 6 : set baseUrl(String url) {
69 6 : if (url.isEmpty) {
70 1 : throw Exception("Url can't be empty");
71 : }
72 6 : _baseUrl = url;
73 : }
74 :
75 2 : get defaultHeaders => _defaultHeaders;
76 :
77 : /// Reset the default headers
78 1 : void resetHeaders() {
79 1 : _defaultHeaders = null;
80 : }
81 :
82 : /// Add default headers for future HTTP requests
83 : ///
84 : /// [headers] is a Map containing all headers to add. If a keys is already present in
85 : /// the default headers, its value will be overriden.
86 4 : void addHeaders(Map<String, String> headers) {
87 4 : if (_defaultHeaders == null) {
88 4 : _defaultHeaders = headers;
89 : return;
90 : }
91 2 : _defaultHeaders!.addAll(headers);
92 : }
93 :
94 : /// Delete default headers for future HTTP requests
95 : ///
96 : /// [keys] is a list containing all header keys to remove. If a key isn't present in
97 : /// the default headers, it will be ignored.
98 1 : void deleteHeaders(List<String> keys) {
99 2 : for (final key in keys) {
100 2 : _defaultHeaders?.remove(key);
101 : }
102 : }
103 :
104 : /// Verify the format of the JWT [token]
105 : ///
106 : /// If the token is valid, nothing will happen, otherwise an [Exception] will be thrown.
107 : ///
108 : /// Exceptions thrown are:
109 : /// - `Invalid JWT format: Expected 3 parts, got [number of parts]`
110 : /// - `Unsupported signature algorithm: [algorithm]`
111 : /// - `Token expired at [DateTime]`
112 : /// - `Token issued in the future at [DateTime]`
113 2 : void _verifyJWTFormat(String token) {
114 2 : List<String> parts = token.split('.');
115 :
116 4 : if (parts.length != 3) {
117 1 : throw Exception(
118 2 : 'Invalid JWT format: Expected 3 parts, got ${parts.length}');
119 : }
120 :
121 6 : for (int i = 0; i < parts.length; i++) {
122 8 : if (parts[i].length % 4 != 0) {
123 14 : parts[i] += "=" * (4 - (parts[i].length % 4));
124 : }
125 : }
126 :
127 : Map<String, dynamic> header =
128 8 : jsonDecode(utf8.decode(base64Url.decode(parts[0])));
129 :
130 : Map<String, dynamic> payload =
131 8 : jsonDecode(utf8.decode(base64Url.decode(parts[1])));
132 :
133 2 : String alg = header['alg'] as String;
134 4 : if (!['HS256', 'RS256'].contains(alg)) {
135 2 : throw Exception('Unsupported signature algorithm: $alg');
136 : }
137 :
138 2 : DateTime? exp = payload.containsKey('exp')
139 3 : ? DateTime.fromMillisecondsSinceEpoch((payload['exp'] as int) * 1000)
140 : : null;
141 2 : if (exp != null && exp.isBefore(DateTime.now())) {
142 2 : throw Exception('Token expired at $exp');
143 : }
144 : }
145 :
146 : /// A wrapper around [context.watch], it should be used instead of it.
147 : ///
148 : /// Performs a `context.watch<ApiBase>()` to get [ApiBase] from the [context],
149 : /// set the current context of the [ApiBase] to [context] then return the [ApiBase]
150 1 : static ApiBase watch(BuildContext context) =>
151 2 : context.watch<ApiBase>().._currentBuildContext = context;
152 :
153 : /// A wrapper around [context.read], it should be used instead of it.
154 : ///
155 : /// Performs a `context.read<ApiBase>()` to get [ApiBase] from the [context],
156 : /// set the current context of the [ApiBase] to [context] then return the [ApiBase]
157 6 : static ApiBase read(BuildContext context) =>
158 12 : context.read<ApiBase>().._currentBuildContext = context;
159 :
160 : /// Transform the [promise] response into a [Future<Either<ApiError, Map<String, dynamic>>>]
161 : ///
162 : /// If the status of the [Response] is 200, the response will be returned as a right [Either] containing
163 : /// the body of the response decoded from json string with [jsonDecode].
164 : ///
165 : /// If the status of the [Response] isn't 200, the response will be return as left [Either] containing
166 : /// a [ApiError] containing the status coode of the response and the body as its message.
167 6 : Future<Either<ApiError, dynamic>> _handleApiResult(
168 : Future<Response> promise, [String? path]) async {
169 : try {
170 : final response = await promise;
171 6 : final apiResponse = ApiResponse.fromJson(
172 24 : response.body.isEmpty ? {} : jsonDecode(response.body));
173 6 : if (!apiResponse.success) {
174 12 : _apiReactions.executeReaction(
175 12 : _currentBuildContext!, apiResponse.statusCode, path);
176 : // if (response.statusCode != HttpStatus.ok) {
177 12 : return Left(ApiError(
178 6 : httpErrorCode: response.statusCode,
179 6 : message: apiResponse.message,
180 6 : apiStatusCode: apiResponse.statusCode));
181 : // ApiError(httpErrorCode: response.statusCode, message: response.body, apiStatusCode: "xxxxxx"));
182 : }
183 : // return Right(response.body.isEmpty ? {} : jsonDecode(response.body));
184 13 : return Right(apiResponse.data ?? {});
185 : } catch (e) {
186 : rethrow;
187 : }
188 : }
189 :
190 : /// Perform a GET HTTP request
191 : ///
192 : /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
193 : /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
194 : ///
195 : /// Example of a [conversionFunc] for a example class:
196 : /// ```dart
197 : /// class Person {
198 : /// Person({required this.firstName, required this.lastName});
199 : ///
200 : /// String firstName;
201 : /// String lastName;
202 : /// }
203 : ///
204 : /// Person fromJsonToPerson(Map<String, dynamic> json) {
205 : /// return Person(firstName: json["firstName"], lastName: json["lastName"]);
206 : /// }
207 : /// ```
208 : ///
209 : /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
210 : /// in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
211 : /// has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
212 : /// set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
213 : /// - [queryParameters] is a map containing the query parameters of the request
214 : /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
215 : /// is set to 10 seconds.
216 : ///
217 : /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
218 : /// transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
219 : /// containing the status code and the body of the response as its message.
220 6 : Future<Either<ApiError, T>> get<T>(
221 : String path, T Function(dynamic json) conversionFunc,
222 : {Map<String, String>? headers,
223 : Map<String, String>? queryParameters,
224 : Duration? timeoutDuration}) async {
225 12 : final promise = _client.get(
226 18 : Uri.parse((_baseUrl ?? "") +
227 6 : path +
228 2 : (queryParameters?.entries
229 10 : .map((entry) => "${entry.key}=${entry.value}")
230 4 : .fold("?", (previous, current) {
231 2 : if (previous == "?") {
232 2 : return "?$current";
233 : }
234 2 : return "$previous&$current";
235 : }) ??
236 : "")),
237 6 : headers: <String, String>{
238 6 : ...?_defaultHeaders,
239 9 : if (_accessToken != null) "Authorization": "Bearer $_accessToken",
240 1 : ...?headers,
241 : },
242 6 : ).timeout(timeoutDuration ?? const Duration(seconds: 10));
243 12 : return _handleApiResult(promise, path).mapRight(conversionFunc);
244 : }
245 :
246 : /// Perform a PUT HTTP request
247 : ///
248 : /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
249 : /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
250 : ///
251 : /// Example of a [conversionFunc] for a example class:
252 : /// ```dart
253 : /// class Person {
254 : /// Person({required this.firstName, required this.lastName});
255 : ///
256 : /// String firstName;
257 : /// String lastName;
258 : /// }
259 : ///
260 : /// Person fromJsonToPerson(Map<String, dynamic> json) {
261 : /// return Person(firstName: json["firstName"], lastName: json["lastName"]);
262 : /// }
263 : /// ```
264 : ///
265 : /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
266 : /// in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
267 : /// has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
268 : /// set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
269 : /// - [requestBody] is a [Map<String, dynamic>] containing the request body to send
270 : /// - [queryParameters] is a map containing the query parameters of the request
271 : /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
272 : /// is set to 10 seconds.
273 : ///
274 : /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
275 : /// transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
276 : /// containing the status code and the body of the response as its message.
277 1 : Future<Either<ApiError, T>> put<T>(
278 : String path, T Function(dynamic json) conversionFunc,
279 : {Map<String, String>? headers,
280 : Map<String, dynamic>? requestBody,
281 : Map<String, String>? queryParameters,
282 : Duration? timeoutDuration}) async {
283 0 : String? body = requestBody == null ? null : json.encode(requestBody);
284 1 : final promise = _client
285 1 : .put(
286 3 : Uri.parse((_baseUrl ?? "") +
287 1 : path +
288 0 : (queryParameters?.entries
289 0 : .map((entry) => "${entry.key}=${entry.value}")
290 0 : .fold("?", (previous, current) {
291 0 : if (previous == "?") {
292 0 : return "?$current";
293 : }
294 0 : return "$previous&$current";
295 : }) ??
296 : "")),
297 1 : headers: <String, String>{
298 1 : ...?_defaultHeaders,
299 1 : if (_accessToken != null) "Authorization": "Bearer $_accessToken",
300 1 : ...?headers,
301 : },
302 : body: body,
303 : )
304 1 : .timeout(timeoutDuration ?? const Duration(seconds: 10));
305 2 : return _handleApiResult(promise, path).mapRight(conversionFunc);
306 : }
307 :
308 : /// Perform a POST HTTP request
309 : ///
310 : /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
311 : /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
312 : ///
313 : /// Example of a [conversionFunc] for a example class:
314 : /// ```dart
315 : /// class Person {
316 : /// Person({required this.firstName, required this.lastName});
317 : ///
318 : /// String firstName;
319 : /// String lastName;
320 : /// }
321 : ///
322 : /// Person fromJsonToPerson(Map<String, dynamic> json) {
323 : /// return Person(firstName: json["firstName"], lastName: json["lastName"]);
324 : /// }
325 : /// ```
326 : ///
327 : /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
328 : /// in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
329 : /// has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
330 : /// set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
331 : /// - [requestBody] is a [Map<String, dynamic>] containing the request body to send
332 : /// - [queryParameters] is a map containing the query parameters of the request
333 : /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
334 : /// is set to 10 seconds.
335 : ///
336 : /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
337 : /// transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
338 : /// containing the status code and the body of the response as its message.
339 2 : Future<Either<ApiError, T>> post<T>(
340 : String path, T Function(dynamic json) conversionFunc,
341 : {Map<String, String>? headers,
342 : Map<String, dynamic>? requestBody,
343 : Map<String, String>? queryParameters,
344 : Duration? timeoutDuration}) async {
345 1 : String? body = requestBody == null ? null : json.encode(requestBody);
346 2 : final promise = _client
347 2 : .post(
348 6 : Uri.parse((_baseUrl ?? "") +
349 2 : path +
350 1 : (queryParameters?.entries
351 5 : .map((entry) => "${entry.key}=${entry.value}")
352 2 : .fold("?", (previous, current) {
353 1 : if (previous == "?") {
354 1 : return "?$current";
355 : }
356 0 : return "$previous&$current";
357 : }) ??
358 : "")),
359 2 : headers: <String, String>{
360 2 : ...?_defaultHeaders,
361 2 : if (_accessToken != null) "Authorization": "Bearer $_accessToken",
362 1 : ...?headers
363 : },
364 : body: body,
365 : )
366 2 : .timeout(timeoutDuration ?? const Duration(seconds: 10));
367 4 : return _handleApiResult(promise, path).mapRight(conversionFunc);
368 : }
369 :
370 : /// Perform a PATCH HTTP request
371 : ///
372 : /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
373 : /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
374 : ///
375 : /// Example of a [conversionFunc] for a example class:
376 : /// ```dart
377 : /// class Person {
378 : /// Person({required this.firstName, required this.lastName});
379 : ///
380 : /// String firstName;
381 : /// String lastName;
382 : /// }
383 : ///
384 : /// Person fromJsonToPerson(Map<String, dynamic> json) {
385 : /// return Person(firstName: json["firstName"], lastName: json["lastName"]);
386 : /// }
387 : /// ```
388 : ///
389 : /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
390 : /// in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
391 : /// has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
392 : /// set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
393 : /// - [requestBody] is a [Map<String, dynamic>] containing the request body to send
394 : /// - [queryParameters] is a map containing the query parameters of the request
395 : /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
396 : /// is set to 10 seconds.
397 : ///
398 : /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
399 : /// transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
400 : /// containing the status code and the body of the response as its message.
401 1 : Future<Either<ApiError, T>> patch<T>(
402 : String path, T Function(dynamic json) conversionFunc,
403 : {Map<String, String>? headers,
404 : Map<String, dynamic>? requestBody,
405 : Map<String, String>? queryParameters,
406 : Duration? timeoutDuration}) async {
407 1 : String? body = requestBody == null ? null : json.encode(requestBody);
408 1 : final promise = _client
409 1 : .patch(
410 3 : Uri.parse((_baseUrl ?? "") +
411 1 : path +
412 0 : (queryParameters?.entries
413 0 : .map((entry) => "${entry.key}=${entry.value}")
414 0 : .fold("?", (previous, current) {
415 0 : if (previous == "?") {
416 0 : return "?$current";
417 : }
418 0 : return "$previous&$current";
419 : }) ??
420 : "")),
421 1 : headers: <String, String>{
422 1 : ...?_defaultHeaders,
423 1 : if (_accessToken != null) "Authorization": "Bearer $_accessToken",
424 0 : ...?headers
425 : },
426 : body: body,
427 : )
428 1 : .timeout(timeoutDuration ?? const Duration(seconds: 10));
429 2 : return _handleApiResult(promise, path).mapRight(conversionFunc);
430 : }
431 :
432 : /// Perform a DELETE HTTP request
433 : ///
434 : /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
435 : /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
436 : ///
437 : /// Example of a [conversionFunc] for a example class:
438 : /// ```dart
439 : /// class Person {
440 : /// Person({required this.firstName, required this.lastName});
441 : ///
442 : /// String firstName;
443 : /// String lastName;
444 : /// }
445 : ///
446 : /// Person fromJsonToPerson(Map<String, dynamic> json) {
447 : /// return Person(firstName: json["firstName"], lastName: json["lastName"]);
448 : /// }
449 : /// ```
450 : ///
451 : /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
452 : /// in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
453 : /// has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
454 : /// set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
455 : /// - [requestBody] is a [Map<String, dynamic>] containing the request body to send
456 : /// - [queryParameters] is a map containing the query parameters of the request
457 : /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
458 : /// is set to 10 seconds.
459 : ///
460 : /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
461 : /// transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
462 : /// containing the status code and the body of the response as its message.
463 1 : Future<Either<ApiError, T>> delete<T>(
464 : String path, T Function(dynamic json) conversionFunc,
465 : {Map<String, String>? headers,
466 : Map<String, dynamic>? requestBody,
467 : Map<String, String>? queryParameters,
468 : Duration? timeoutDuration}) async {
469 0 : String? body = requestBody == null ? null : json.encode(requestBody);
470 1 : final promise = _client
471 1 : .delete(
472 3 : Uri.parse((_baseUrl ?? "") +
473 1 : path +
474 0 : (queryParameters?.entries
475 0 : .map((entry) => "${entry.key}=${entry.value}")
476 0 : .fold("?", (previous, current) {
477 0 : if (previous == "?") {
478 0 : return "?$current";
479 : }
480 0 : return "$previous&$current";
481 : }) ??
482 : "")),
483 1 : headers: <String, String>{
484 1 : ...?_defaultHeaders,
485 1 : if (_accessToken != null) "Authorization": "Bearer $_accessToken",
486 1 : ...?headers
487 : },
488 : body: body,
489 : )
490 1 : .timeout(timeoutDuration ?? const Duration(seconds: 10));
491 2 : return _handleApiResult(promise, path).mapRight(conversionFunc);
492 : }
493 : }
|