api_manager.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. // ignore_for_file: constant_identifier_names, depend_on_referenced_packages, prefer_final_fields
  2. import 'dart:async';
  3. import 'dart:convert';
  4. import 'dart:core';
  5. import 'dart:io';
  6. import 'dart:typed_data';
  7. import 'package:collection/collection.dart';
  8. import 'package:http/http.dart' as http;
  9. import 'package:equatable/equatable.dart';
  10. import 'package:http_parser/http_parser.dart';
  11. import 'package:mime_type/mime_type.dart';
  12. import 'package:flutter/foundation.dart';
  13. import 'package:http/browser_client.dart'
  14. if (dart.library.io) 'browser_client_stub.dart';
  15. import '/flutter_flow/uploaded_file.dart';
  16. import 'get_streamed_response.dart';
  17. enum ApiCallType {
  18. GET,
  19. POST,
  20. DELETE,
  21. PUT,
  22. PATCH,
  23. }
  24. enum BodyType {
  25. NONE,
  26. JSON,
  27. TEXT,
  28. X_WWW_FORM_URL_ENCODED,
  29. MULTIPART,
  30. }
  31. class ApiCallOptions extends Equatable {
  32. const ApiCallOptions({
  33. this.callName = '',
  34. required this.callType,
  35. required this.apiUrl,
  36. required this.headers,
  37. required this.params,
  38. this.bodyType,
  39. this.body,
  40. this.returnBody = true,
  41. this.encodeBodyUtf8 = false,
  42. this.decodeUtf8 = false,
  43. this.alwaysAllowBody = false,
  44. this.cache = false,
  45. this.isStreamingApi = false,
  46. });
  47. final String callName;
  48. final ApiCallType callType;
  49. final String apiUrl;
  50. final Map<String, dynamic> headers;
  51. final Map<String, dynamic> params;
  52. final BodyType? bodyType;
  53. final String? body;
  54. final bool returnBody;
  55. final bool encodeBodyUtf8;
  56. final bool decodeUtf8;
  57. final bool alwaysAllowBody;
  58. final bool cache;
  59. final bool isStreamingApi;
  60. /// Creates a new [ApiCallOptions] with optionally updated parameters.
  61. ///
  62. /// This helper function allows creating a copy of the current options while
  63. /// selectively modifying specific fields. Any parameter that is not provided
  64. /// will retain its original value from the current instance.
  65. ApiCallOptions copyWith({
  66. String? callName,
  67. ApiCallType? callType,
  68. String? apiUrl,
  69. Map<String, dynamic>? headers,
  70. Map<String, dynamic>? params,
  71. BodyType? bodyType,
  72. String? body,
  73. bool? returnBody,
  74. bool? encodeBodyUtf8,
  75. bool? decodeUtf8,
  76. bool? alwaysAllowBody,
  77. bool? cache,
  78. bool? isStreamingApi,
  79. }) {
  80. return ApiCallOptions(
  81. callName: callName ?? this.callName,
  82. callType: callType ?? this.callType,
  83. apiUrl: apiUrl ?? this.apiUrl,
  84. headers: headers ?? _cloneMap(this.headers),
  85. params: params ?? _cloneMap(this.params),
  86. bodyType: bodyType ?? this.bodyType,
  87. body: body ?? this.body,
  88. returnBody: returnBody ?? this.returnBody,
  89. encodeBodyUtf8: encodeBodyUtf8 ?? this.encodeBodyUtf8,
  90. decodeUtf8: decodeUtf8 ?? this.decodeUtf8,
  91. alwaysAllowBody: alwaysAllowBody ?? this.alwaysAllowBody,
  92. cache: cache ?? this.cache,
  93. isStreamingApi: isStreamingApi ?? this.isStreamingApi,
  94. );
  95. }
  96. ApiCallOptions clone() => ApiCallOptions(
  97. callName: callName,
  98. callType: callType,
  99. apiUrl: apiUrl,
  100. headers: _cloneMap(headers),
  101. params: _cloneMap(params),
  102. bodyType: bodyType,
  103. body: body,
  104. returnBody: returnBody,
  105. encodeBodyUtf8: encodeBodyUtf8,
  106. decodeUtf8: decodeUtf8,
  107. alwaysAllowBody: alwaysAllowBody,
  108. cache: cache,
  109. isStreamingApi: isStreamingApi,
  110. );
  111. @override
  112. List<Object?> get props => [
  113. callName,
  114. callType.name,
  115. apiUrl,
  116. headers,
  117. params,
  118. bodyType,
  119. body,
  120. returnBody,
  121. encodeBodyUtf8,
  122. decodeUtf8,
  123. alwaysAllowBody,
  124. cache,
  125. isStreamingApi,
  126. ];
  127. static Map<String, dynamic> _cloneMap(Map<String, dynamic> map) {
  128. try {
  129. return json.decode(json.encode(map)) as Map<String, dynamic>;
  130. } catch (_) {
  131. return Map.from(map);
  132. }
  133. }
  134. }
  135. class ApiCallResponse {
  136. const ApiCallResponse(
  137. this.jsonBody,
  138. this.headers,
  139. this.statusCode, {
  140. this.response,
  141. this.streamedResponse,
  142. this.exception,
  143. this.requestOptions,
  144. });
  145. final dynamic jsonBody;
  146. final Map<String, String> headers;
  147. final int statusCode;
  148. final http.Response? response;
  149. final http.StreamedResponse? streamedResponse;
  150. final Object? exception;
  151. /// The original request options used to make the API call.
  152. /// Available in interceptor's onResponse callback to access request details
  153. /// like URL, HTTP method, headers, params, and request body.
  154. final ApiCallOptions? requestOptions;
  155. // Whether we received a 2xx status (which generally marks success).
  156. bool get succeeded => statusCode >= 200 && statusCode < 300;
  157. String getHeader(String headerName) => headers[headerName] ?? '';
  158. // Return the raw body from the response, or if this came from a cloud call
  159. // and the body is not a string, then the json encoded body.
  160. String get bodyText =>
  161. response?.body ??
  162. (jsonBody is String ? jsonBody as String : jsonEncode(jsonBody));
  163. String get exceptionMessage => exception.toString();
  164. /// Creates a new [ApiCallResponse] with optionally updated parameters.
  165. ///
  166. /// This helper function allows creating a copy of the current response while
  167. /// selectively modifying specific fields. Any parameter that is not provided
  168. /// will retain its original value from the current instance.
  169. ApiCallResponse copyWith({
  170. dynamic jsonBody,
  171. Map<String, String>? headers,
  172. int? statusCode,
  173. http.Response? response,
  174. http.StreamedResponse? streamedResponse,
  175. Object? exception,
  176. ApiCallOptions? requestOptions,
  177. }) {
  178. return ApiCallResponse(
  179. jsonBody ?? this.jsonBody,
  180. headers ?? this.headers,
  181. statusCode ?? this.statusCode,
  182. response: response ?? this.response,
  183. streamedResponse: streamedResponse ?? this.streamedResponse,
  184. exception: exception ?? this.exception,
  185. requestOptions: requestOptions ?? this.requestOptions,
  186. );
  187. }
  188. static ApiCallResponse fromHttpResponse(
  189. http.Response response,
  190. bool returnBody,
  191. bool decodeUtf8,
  192. ) {
  193. dynamic jsonBody;
  194. try {
  195. final responseBody = decodeUtf8 && returnBody
  196. ? const Utf8Decoder().convert(response.bodyBytes)
  197. : response.body;
  198. jsonBody = returnBody ? json.decode(responseBody) : null;
  199. } catch (_) {}
  200. return ApiCallResponse(
  201. jsonBody,
  202. response.headers,
  203. response.statusCode,
  204. response: response,
  205. );
  206. }
  207. static ApiCallResponse fromCloudCallResponse(Map<String, dynamic> response) =>
  208. ApiCallResponse(
  209. response['body'],
  210. ApiManager.toStringMap(response['headers'] ?? {}),
  211. response['statusCode'] ?? 400,
  212. );
  213. }
  214. class ApiManager {
  215. ApiManager._();
  216. // Cache that will ensure identical calls are not repeatedly made.
  217. static Map<ApiCallOptions, ApiCallResponse> _apiCache = {};
  218. static ApiManager? _instance;
  219. static ApiManager get instance => _instance ??= ApiManager._();
  220. /// Get HTTP client with optional credentials support for web
  221. ///
  222. /// Parameters:
  223. /// - withCredentials: Whether to include credentials (cookies) with requests
  224. /// Only applies to web platform (BrowserClient)
  225. /// Default: false
  226. ///
  227. /// Returns a platform-specific HTTP client:
  228. /// - Web: BrowserClient with credentials setting applied
  229. /// - Mobile/Desktop: Standard http.Client
  230. static http.Client getClient({bool withCredentials = false}) {
  231. // For web platform, return BrowserClient with appropriate settings
  232. if (kIsWeb) {
  233. return BrowserClient()..withCredentials = withCredentials;
  234. }
  235. // For mobile/desktop, return standard http.Client
  236. // (credentials are handled differently on these platforms)
  237. return http.Client();
  238. }
  239. // If your API calls need authentication, populate this field once
  240. // the user has authenticated. Alter this as needed.
  241. static String? _accessToken;
  242. // You may want to call this if, for example, you make a change to the
  243. // database and no longer want the cached result of a call that may
  244. // have changed.
  245. static void clearCache(String callName) => _apiCache.keys
  246. .toSet()
  247. .forEach((k) => k.callName == callName ? _apiCache.remove(k) : null);
  248. static Map<String, String> toStringMap(Map map) =>
  249. map.map((key, value) => MapEntry(key.toString(), value.toString()));
  250. static String asQueryParams(Map<String, dynamic> map) => map.entries
  251. .map((e) =>
  252. "${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}")
  253. .join('&');
  254. static Future<ApiCallResponse> urlRequest(
  255. ApiCallType callType,
  256. String apiUrl,
  257. Map<String, dynamic> headers,
  258. Map<String, dynamic> params,
  259. bool returnBody,
  260. bool decodeUtf8,
  261. bool isStreamingApi, {
  262. http.Client? client,
  263. }) async {
  264. if (params.isNotEmpty) {
  265. final specifier =
  266. Uri.parse(apiUrl).queryParameters.isNotEmpty ? '&' : '?';
  267. apiUrl = '$apiUrl$specifier${asQueryParams(params)}';
  268. }
  269. if (isStreamingApi) {
  270. client ??= http.Client();
  271. final request =
  272. http.Request(callType.toString().split('.').last, Uri.parse(apiUrl))
  273. ..headers.addAll(toStringMap(headers));
  274. final streamedResponse = await getStreamedResponse(request);
  275. return ApiCallResponse(
  276. null,
  277. streamedResponse.headers,
  278. streamedResponse.statusCode,
  279. streamedResponse: streamedResponse,
  280. );
  281. }
  282. final makeRequest = callType == ApiCallType.GET
  283. ? (client != null ? client.get : http.get)
  284. : (client != null ? client.delete : http.delete);
  285. final response =
  286. await makeRequest(Uri.parse(apiUrl), headers: toStringMap(headers));
  287. return ApiCallResponse.fromHttpResponse(response, returnBody, decodeUtf8);
  288. }
  289. static Future<ApiCallResponse> requestWithBody(
  290. ApiCallType type,
  291. String apiUrl,
  292. Map<String, dynamic> headers,
  293. Map<String, dynamic> params,
  294. String? body,
  295. BodyType? bodyType,
  296. bool returnBody,
  297. bool encodeBodyUtf8,
  298. bool decodeUtf8,
  299. bool alwaysAllowBody,
  300. bool isStreamingApi, {
  301. http.Client? client,
  302. }) async {
  303. assert(
  304. {ApiCallType.POST, ApiCallType.PUT, ApiCallType.PATCH}.contains(type) ||
  305. (alwaysAllowBody && type == ApiCallType.DELETE),
  306. 'Invalid ApiCallType $type for request with body',
  307. );
  308. final postBody =
  309. createBody(headers, params, body, bodyType, encodeBodyUtf8);
  310. if (isStreamingApi) {
  311. client ??= http.Client();
  312. final request =
  313. http.Request(type.toString().split('.').last, Uri.parse(apiUrl))
  314. ..headers.addAll(toStringMap(headers));
  315. request.body = postBody;
  316. final streamedResponse = await getStreamedResponse(request);
  317. return ApiCallResponse(
  318. null,
  319. streamedResponse.headers,
  320. streamedResponse.statusCode,
  321. streamedResponse: streamedResponse,
  322. );
  323. }
  324. if (bodyType == BodyType.MULTIPART) {
  325. return multipartRequest(type, apiUrl, headers, params, returnBody,
  326. decodeUtf8, alwaysAllowBody, client);
  327. }
  328. final requestFn = {
  329. ApiCallType.POST: client != null ? client.post : http.post,
  330. ApiCallType.PUT: client != null ? client.put : http.put,
  331. ApiCallType.PATCH: client != null ? client.patch : http.patch,
  332. ApiCallType.DELETE: client != null ? client.delete : http.delete,
  333. }[type]!;
  334. final response = await requestFn(Uri.parse(apiUrl),
  335. headers: toStringMap(headers), body: postBody);
  336. return ApiCallResponse.fromHttpResponse(response, returnBody, decodeUtf8);
  337. }
  338. static Future<ApiCallResponse> multipartRequest(
  339. ApiCallType? type,
  340. String apiUrl,
  341. Map<String, dynamic> headers,
  342. Map<String, dynamic> params,
  343. bool returnBody,
  344. bool decodeUtf8,
  345. bool alwaysAllowBody,
  346. http.Client? client,
  347. ) async {
  348. assert(
  349. {ApiCallType.POST, ApiCallType.PUT, ApiCallType.PATCH}.contains(type) ||
  350. (alwaysAllowBody && type == ApiCallType.DELETE),
  351. 'Invalid ApiCallType $type for request with body',
  352. );
  353. bool isFile(dynamic e) =>
  354. e is FFUploadedFile ||
  355. e is List<FFUploadedFile> ||
  356. (e is List && e.firstOrNull is FFUploadedFile);
  357. final nonFileParams = toStringMap(
  358. Map.fromEntries(params.entries.where((e) => !isFile(e.value))));
  359. List<http.MultipartFile> files = [];
  360. params.entries.where((e) => isFile(e.value)).forEach((e) {
  361. final param = e.value;
  362. final uploadedFiles = param is List
  363. ? param as List<FFUploadedFile>
  364. : [param as FFUploadedFile];
  365. for (var uploadedFile in uploadedFiles) {
  366. files.add(
  367. http.MultipartFile.fromBytes(
  368. e.key,
  369. uploadedFile.bytes ?? Uint8List.fromList([]),
  370. filename: uploadedFile.name,
  371. contentType: _getMediaType(uploadedFile.name),
  372. ),
  373. );
  374. }
  375. });
  376. final request = http.MultipartRequest(
  377. type.toString().split('.').last, Uri.parse(apiUrl))
  378. ..headers.addAll(toStringMap(headers))
  379. ..files.addAll(files);
  380. nonFileParams.forEach((key, value) => request.fields[key] = value);
  381. final response = await http.Response.fromStream(
  382. await (client != null ? client.send(request) : request.send()));
  383. return ApiCallResponse.fromHttpResponse(response, returnBody, decodeUtf8);
  384. }
  385. static MediaType? _getMediaType(String? filename) {
  386. final contentType = mime(filename);
  387. if (contentType == null) {
  388. return null;
  389. }
  390. final parts = contentType.split('/');
  391. if (parts.length != 2) {
  392. return null;
  393. }
  394. return MediaType(parts.first, parts.last);
  395. }
  396. static dynamic createBody(
  397. Map<String, dynamic> headers,
  398. Map<String, dynamic>? params,
  399. String? body,
  400. BodyType? bodyType,
  401. bool encodeBodyUtf8,
  402. ) {
  403. String? contentType;
  404. dynamic postBody;
  405. switch (bodyType) {
  406. case BodyType.JSON:
  407. contentType = 'application/json';
  408. postBody = body ?? json.encode(params ?? {});
  409. break;
  410. case BodyType.TEXT:
  411. contentType = 'text/plain';
  412. postBody = body ?? json.encode(params ?? {});
  413. break;
  414. case BodyType.X_WWW_FORM_URL_ENCODED:
  415. contentType = 'application/x-www-form-urlencoded';
  416. postBody = toStringMap(params ?? {});
  417. break;
  418. case BodyType.MULTIPART:
  419. contentType = 'multipart/form-data';
  420. postBody = params;
  421. break;
  422. case BodyType.NONE:
  423. case null:
  424. break;
  425. }
  426. // Set "Content-Type" header if it was previously unset.
  427. if (contentType != null &&
  428. !headers.keys.any((h) => h.toLowerCase() == 'content-type')) {
  429. headers['Content-Type'] = contentType;
  430. }
  431. return encodeBodyUtf8 && postBody is String
  432. ? utf8.encode(postBody)
  433. : postBody;
  434. }
  435. Future<ApiCallResponse> call(
  436. ApiCallOptions options, {
  437. http.Client? client,
  438. }) =>
  439. makeApiCall(
  440. callName: options.callName,
  441. apiUrl: options.apiUrl,
  442. callType: options.callType,
  443. headers: options.headers,
  444. params: options.params,
  445. body: options.body,
  446. bodyType: options.bodyType,
  447. returnBody: options.returnBody,
  448. encodeBodyUtf8: options.encodeBodyUtf8,
  449. decodeUtf8: options.decodeUtf8,
  450. alwaysAllowBody: options.alwaysAllowBody,
  451. cache: options.cache,
  452. isStreamingApi: options.isStreamingApi,
  453. options: options,
  454. client: client,
  455. );
  456. Future<ApiCallResponse> makeApiCall({
  457. required String callName,
  458. required String apiUrl,
  459. required ApiCallType callType,
  460. Map<String, dynamic> headers = const {},
  461. Map<String, dynamic> params = const {},
  462. String? body,
  463. BodyType? bodyType,
  464. bool returnBody = true,
  465. bool encodeBodyUtf8 = false,
  466. bool decodeUtf8 = false,
  467. bool alwaysAllowBody = false,
  468. bool cache = false,
  469. bool isStreamingApi = false,
  470. ApiCallOptions? options,
  471. http.Client? client,
  472. }) async {
  473. final callOptions = options ??
  474. ApiCallOptions(
  475. callName: callName,
  476. callType: callType,
  477. apiUrl: apiUrl,
  478. headers: headers,
  479. params: params,
  480. bodyType: bodyType,
  481. body: body,
  482. returnBody: returnBody,
  483. encodeBodyUtf8: encodeBodyUtf8,
  484. decodeUtf8: decodeUtf8,
  485. alwaysAllowBody: alwaysAllowBody,
  486. cache: cache,
  487. isStreamingApi: isStreamingApi,
  488. );
  489. // Modify for your specific needs if this differs from your API.
  490. if (_accessToken != null) {
  491. headers[HttpHeaders.authorizationHeader] = 'Bearer $_accessToken';
  492. }
  493. if (!apiUrl.startsWith('http')) {
  494. apiUrl = 'https://$apiUrl';
  495. }
  496. // If we've already made this exact call before and caching is on,
  497. // return the cached result.
  498. if (cache && _apiCache.containsKey(callOptions)) {
  499. return _apiCache[callOptions]!;
  500. }
  501. ApiCallResponse result;
  502. try {
  503. switch (callType) {
  504. case ApiCallType.GET:
  505. result = await urlRequest(
  506. callType,
  507. apiUrl,
  508. headers,
  509. params,
  510. returnBody,
  511. decodeUtf8,
  512. isStreamingApi,
  513. client: client,
  514. );
  515. break;
  516. case ApiCallType.DELETE:
  517. result = alwaysAllowBody
  518. ? await requestWithBody(
  519. callType,
  520. apiUrl,
  521. headers,
  522. params,
  523. body,
  524. bodyType,
  525. returnBody,
  526. encodeBodyUtf8,
  527. decodeUtf8,
  528. alwaysAllowBody,
  529. isStreamingApi,
  530. client: client,
  531. )
  532. : await urlRequest(
  533. callType,
  534. apiUrl,
  535. headers,
  536. params,
  537. returnBody,
  538. decodeUtf8,
  539. isStreamingApi,
  540. client: client,
  541. );
  542. break;
  543. case ApiCallType.POST:
  544. case ApiCallType.PUT:
  545. case ApiCallType.PATCH:
  546. result = await requestWithBody(
  547. callType,
  548. apiUrl,
  549. headers,
  550. params,
  551. body,
  552. bodyType,
  553. returnBody,
  554. encodeBodyUtf8,
  555. decodeUtf8,
  556. alwaysAllowBody,
  557. isStreamingApi,
  558. client: client,
  559. );
  560. break;
  561. }
  562. // If caching is on, cache the result (if present).
  563. if (cache) {
  564. _apiCache[callOptions] = result;
  565. }
  566. } catch (e) {
  567. result = ApiCallResponse(null, {}, -1, exception: e);
  568. }
  569. return result;
  570. }
  571. }