| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648 |
- // ignore_for_file: constant_identifier_names, depend_on_referenced_packages, prefer_final_fields
- import 'dart:async';
- import 'dart:convert';
- import 'dart:core';
- import 'dart:io';
- import 'dart:typed_data';
- import 'package:collection/collection.dart';
- import 'package:http/http.dart' as http;
- import 'package:equatable/equatable.dart';
- import 'package:http_parser/http_parser.dart';
- import 'package:mime_type/mime_type.dart';
- import 'package:flutter/foundation.dart';
- import 'package:http/browser_client.dart'
- if (dart.library.io) 'browser_client_stub.dart';
- import '/flutter_flow/uploaded_file.dart';
- import '/backend/api_requests/api_streaming.dart';
- import 'get_streamed_response.dart';
- enum ApiCallType {
- GET,
- POST,
- DELETE,
- PUT,
- PATCH,
- }
- enum BodyType {
- NONE,
- JSON,
- TEXT,
- X_WWW_FORM_URL_ENCODED,
- MULTIPART,
- }
- class ApiCallOptions extends Equatable {
- const ApiCallOptions({
- this.callName = '',
- required this.callType,
- required this.apiUrl,
- required this.headers,
- required this.params,
- this.bodyType,
- this.body,
- this.returnBody = true,
- this.encodeBodyUtf8 = false,
- this.decodeUtf8 = false,
- this.alwaysAllowBody = false,
- this.cache = false,
- this.isStreamingApi = false,
- });
- final String callName;
- final ApiCallType callType;
- final String apiUrl;
- final Map<String, dynamic> headers;
- final Map<String, dynamic> params;
- final BodyType? bodyType;
- final String? body;
- final bool returnBody;
- final bool encodeBodyUtf8;
- final bool decodeUtf8;
- final bool alwaysAllowBody;
- final bool cache;
- final bool isStreamingApi;
- /// Creates a new [ApiCallOptions] with optionally updated parameters.
- ///
- /// This helper function allows creating a copy of the current options while
- /// selectively modifying specific fields. Any parameter that is not provided
- /// will retain its original value from the current instance.
- ApiCallOptions copyWith({
- String? callName,
- ApiCallType? callType,
- String? apiUrl,
- Map<String, dynamic>? headers,
- Map<String, dynamic>? params,
- BodyType? bodyType,
- String? body,
- bool? returnBody,
- bool? encodeBodyUtf8,
- bool? decodeUtf8,
- bool? alwaysAllowBody,
- bool? cache,
- bool? isStreamingApi,
- }) {
- return ApiCallOptions(
- callName: callName ?? this.callName,
- callType: callType ?? this.callType,
- apiUrl: apiUrl ?? this.apiUrl,
- headers: headers ?? _cloneMap(this.headers),
- params: params ?? _cloneMap(this.params),
- bodyType: bodyType ?? this.bodyType,
- body: body ?? this.body,
- returnBody: returnBody ?? this.returnBody,
- encodeBodyUtf8: encodeBodyUtf8 ?? this.encodeBodyUtf8,
- decodeUtf8: decodeUtf8 ?? this.decodeUtf8,
- alwaysAllowBody: alwaysAllowBody ?? this.alwaysAllowBody,
- cache: cache ?? this.cache,
- isStreamingApi: isStreamingApi ?? this.isStreamingApi,
- );
- }
- ApiCallOptions clone() => ApiCallOptions(
- callName: callName,
- callType: callType,
- apiUrl: apiUrl,
- headers: _cloneMap(headers),
- params: _cloneMap(params),
- bodyType: bodyType,
- body: body,
- returnBody: returnBody,
- encodeBodyUtf8: encodeBodyUtf8,
- decodeUtf8: decodeUtf8,
- alwaysAllowBody: alwaysAllowBody,
- cache: cache,
- isStreamingApi: isStreamingApi,
- );
- @override
- List<Object?> get props => [
- callName,
- callType.name,
- apiUrl,
- headers,
- params,
- bodyType,
- body,
- returnBody,
- encodeBodyUtf8,
- decodeUtf8,
- alwaysAllowBody,
- cache,
- isStreamingApi,
- ];
- static Map<String, dynamic> _cloneMap(Map<String, dynamic> map) {
- try {
- return json.decode(json.encode(map)) as Map<String, dynamic>;
- } catch (_) {
- return Map.from(map);
- }
- }
- }
- class ApiCallResponse {
- const ApiCallResponse(
- this.jsonBody,
- this.headers,
- this.statusCode, {
- this.response,
- this.streamedResponse,
- this.exception,
- this.requestOptions,
- });
- final dynamic jsonBody;
- final Map<String, String> headers;
- final int statusCode;
- final http.Response? response;
- final http.StreamedResponse? streamedResponse;
- final Object? exception;
- /// The original request options used to make the API call.
- /// Available in interceptor's onResponse callback to access request details
- /// like URL, HTTP method, headers, params, and request body.
- final ApiCallOptions? requestOptions;
- // Whether we received a 2xx status (which generally marks success).
- bool get succeeded => statusCode >= 200 && statusCode < 300;
- String getHeader(String headerName) => headers[headerName] ?? '';
- // Return the raw body from the response, or if this came from a cloud call
- // and the body is not a string, then the json encoded body.
- String get bodyText =>
- response?.body ??
- (jsonBody is String ? jsonBody as String : jsonEncode(jsonBody));
- String get exceptionMessage => exception.toString();
- /// Creates a new [ApiCallResponse] with optionally updated parameters.
- ///
- /// This helper function allows creating a copy of the current response while
- /// selectively modifying specific fields. Any parameter that is not provided
- /// will retain its original value from the current instance.
- ApiCallResponse copyWith({
- dynamic jsonBody,
- Map<String, String>? headers,
- int? statusCode,
- http.Response? response,
- http.StreamedResponse? streamedResponse,
- Object? exception,
- ApiCallOptions? requestOptions,
- }) {
- return ApiCallResponse(
- jsonBody ?? this.jsonBody,
- headers ?? this.headers,
- statusCode ?? this.statusCode,
- response: response ?? this.response,
- streamedResponse: streamedResponse ?? this.streamedResponse,
- exception: exception ?? this.exception,
- requestOptions: requestOptions ?? this.requestOptions,
- );
- }
- static ApiCallResponse fromHttpResponse(
- http.Response response,
- bool returnBody,
- bool decodeUtf8,
- ) {
- dynamic jsonBody;
- try {
- final responseBody = decodeUtf8 && returnBody
- ? const Utf8Decoder().convert(response.bodyBytes)
- : response.body;
- jsonBody = returnBody ? json.decode(responseBody) : null;
- } catch (_) {}
- return ApiCallResponse(
- jsonBody,
- response.headers,
- response.statusCode,
- response: response,
- );
- }
- static ApiCallResponse fromCloudCallResponse(Map<String, dynamic> response) =>
- ApiCallResponse(
- response['body'],
- ApiManager.toStringMap(response['headers'] ?? {}),
- response['statusCode'] ?? 400,
- );
- }
- class ApiManager {
- ApiManager._();
- // Cache that will ensure identical calls are not repeatedly made.
- static Map<ApiCallOptions, ApiCallResponse> _apiCache = {};
- static ApiManager? _instance;
- static ApiManager get instance => _instance ??= ApiManager._();
- /// Get HTTP client with optional credentials support for web
- ///
- /// Parameters:
- /// - withCredentials: Whether to include credentials (cookies) with requests
- /// Only applies to web platform (BrowserClient)
- /// Default: false
- ///
- /// Returns a platform-specific HTTP client:
- /// - Web: BrowserClient with credentials setting applied
- /// - Mobile/Desktop: Standard http.Client
- static http.Client getClient({bool withCredentials = false}) {
- // For web platform, return BrowserClient with appropriate settings
- if (kIsWeb) {
- return BrowserClient()..withCredentials = withCredentials;
- }
- // For mobile/desktop, return standard http.Client
- // (credentials are handled differently on these platforms)
- return http.Client();
- }
- // If your API calls need authentication, populate this field once
- // the user has authenticated. Alter this as needed.
- static String? _accessToken;
- // Map of active streaming response subscriptions
- // Key is a unique identifier for the subscription
- // Value is the stream subscription
- final Map<String, StreamSubscription<ResponseStreamMessage>>
- _activeStreamingResponseSubscriptions = {};
- // Add a new active streaming response subscription
- void addActiveStreamingResponseSubscription(
- String subscriptionKey,
- StreamSubscription<ResponseStreamMessage>? subscription,
- ) {
- // Check if the subscription key is empty or if the subscription is null
- if (subscriptionKey.isEmpty || subscription == null) {
- return;
- }
- // Add the subscription to the map
- _activeStreamingResponseSubscriptions[subscriptionKey] = subscription;
- }
- // Cancel an active streaming response subscription
- Future<void> cancelActiveStreamingResponseSubscription(
- String subscriptionKey,
- ) async {
- // Check if the subscription key is in the map
- if (_activeStreamingResponseSubscriptions.containsKey(subscriptionKey)) {
- // Cancel the subscription
- await _activeStreamingResponseSubscriptions[subscriptionKey]!.cancel();
- }
- // Remove the subscription from the map
- _activeStreamingResponseSubscriptions.remove(subscriptionKey);
- }
- // You may want to call this if, for example, you make a change to the
- // database and no longer want the cached result of a call that may
- // have changed.
- static void clearCache(String callName) => _apiCache.keys
- .toSet()
- .forEach((k) => k.callName == callName ? _apiCache.remove(k) : null);
- static Map<String, String> toStringMap(Map map) =>
- map.map((key, value) => MapEntry(key.toString(), value.toString()));
- static String asQueryParams(Map<String, dynamic> map) => map.entries
- .map((e) =>
- "${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}")
- .join('&');
- static Future<ApiCallResponse> urlRequest(
- ApiCallType callType,
- String apiUrl,
- Map<String, dynamic> headers,
- Map<String, dynamic> params,
- bool returnBody,
- bool decodeUtf8,
- bool isStreamingApi, {
- http.Client? client,
- }) async {
- if (params.isNotEmpty) {
- final specifier =
- Uri.parse(apiUrl).queryParameters.isNotEmpty ? '&' : '?';
- apiUrl = '$apiUrl$specifier${asQueryParams(params)}';
- }
- if (isStreamingApi) {
- client ??= http.Client();
- final request =
- http.Request(callType.toString().split('.').last, Uri.parse(apiUrl))
- ..headers.addAll(toStringMap(headers));
- final streamedResponse = await getStreamedResponse(request);
- return ApiCallResponse(
- null,
- streamedResponse.headers,
- streamedResponse.statusCode,
- streamedResponse: streamedResponse,
- );
- }
- final makeRequest = callType == ApiCallType.GET
- ? (client != null ? client.get : http.get)
- : (client != null ? client.delete : http.delete);
- final response =
- await makeRequest(Uri.parse(apiUrl), headers: toStringMap(headers));
- return ApiCallResponse.fromHttpResponse(response, returnBody, decodeUtf8);
- }
- static Future<ApiCallResponse> requestWithBody(
- ApiCallType type,
- String apiUrl,
- Map<String, dynamic> headers,
- Map<String, dynamic> params,
- String? body,
- BodyType? bodyType,
- bool returnBody,
- bool encodeBodyUtf8,
- bool decodeUtf8,
- bool alwaysAllowBody,
- bool isStreamingApi, {
- http.Client? client,
- }) async {
- assert(
- {ApiCallType.POST, ApiCallType.PUT, ApiCallType.PATCH}.contains(type) ||
- (alwaysAllowBody && type == ApiCallType.DELETE),
- 'Invalid ApiCallType $type for request with body',
- );
- final postBody =
- createBody(headers, params, body, bodyType, encodeBodyUtf8);
- if (isStreamingApi) {
- client ??= http.Client();
- final request =
- http.Request(type.toString().split('.').last, Uri.parse(apiUrl))
- ..headers.addAll(toStringMap(headers));
- request.body = postBody;
- final streamedResponse = await getStreamedResponse(request);
- return ApiCallResponse(
- null,
- streamedResponse.headers,
- streamedResponse.statusCode,
- streamedResponse: streamedResponse,
- );
- }
- if (bodyType == BodyType.MULTIPART) {
- return multipartRequest(type, apiUrl, headers, params, returnBody,
- decodeUtf8, alwaysAllowBody, client);
- }
- final requestFn = {
- ApiCallType.POST: client != null ? client.post : http.post,
- ApiCallType.PUT: client != null ? client.put : http.put,
- ApiCallType.PATCH: client != null ? client.patch : http.patch,
- ApiCallType.DELETE: client != null ? client.delete : http.delete,
- }[type]!;
- final response = await requestFn(Uri.parse(apiUrl),
- headers: toStringMap(headers), body: postBody);
- return ApiCallResponse.fromHttpResponse(response, returnBody, decodeUtf8);
- }
- static Future<ApiCallResponse> multipartRequest(
- ApiCallType? type,
- String apiUrl,
- Map<String, dynamic> headers,
- Map<String, dynamic> params,
- bool returnBody,
- bool decodeUtf8,
- bool alwaysAllowBody,
- http.Client? client,
- ) async {
- assert(
- {ApiCallType.POST, ApiCallType.PUT, ApiCallType.PATCH}.contains(type) ||
- (alwaysAllowBody && type == ApiCallType.DELETE),
- 'Invalid ApiCallType $type for request with body',
- );
- bool isFile(dynamic e) =>
- e is FFUploadedFile ||
- e is List<FFUploadedFile> ||
- (e is List && e.firstOrNull is FFUploadedFile);
- final nonFileParams = toStringMap(
- Map.fromEntries(params.entries.where((e) => !isFile(e.value))));
- List<http.MultipartFile> files = [];
- params.entries.where((e) => isFile(e.value)).forEach((e) {
- final param = e.value;
- final uploadedFiles = param is List
- ? param as List<FFUploadedFile>
- : [param as FFUploadedFile];
- for (var uploadedFile in uploadedFiles) {
- files.add(
- http.MultipartFile.fromBytes(
- e.key,
- uploadedFile.bytes ?? Uint8List.fromList([]),
- filename: uploadedFile.name,
- contentType: _getMediaType(uploadedFile.name),
- ),
- );
- }
- });
- final request = http.MultipartRequest(
- type.toString().split('.').last, Uri.parse(apiUrl))
- ..headers.addAll(toStringMap(headers))
- ..files.addAll(files);
- nonFileParams.forEach((key, value) => request.fields[key] = value);
- final response = await http.Response.fromStream(
- await (client != null ? client.send(request) : request.send()));
- return ApiCallResponse.fromHttpResponse(response, returnBody, decodeUtf8);
- }
- static MediaType? _getMediaType(String? filename) {
- final contentType = mime(filename);
- if (contentType == null) {
- return null;
- }
- final parts = contentType.split('/');
- if (parts.length != 2) {
- return null;
- }
- return MediaType(parts.first, parts.last);
- }
- static dynamic createBody(
- Map<String, dynamic> headers,
- Map<String, dynamic>? params,
- String? body,
- BodyType? bodyType,
- bool encodeBodyUtf8,
- ) {
- String? contentType;
- dynamic postBody;
- switch (bodyType) {
- case BodyType.JSON:
- contentType = 'application/json';
- postBody = body ?? json.encode(params ?? {});
- break;
- case BodyType.TEXT:
- contentType = 'text/plain';
- postBody = body ?? json.encode(params ?? {});
- break;
- case BodyType.X_WWW_FORM_URL_ENCODED:
- contentType = 'application/x-www-form-urlencoded';
- postBody = toStringMap(params ?? {});
- break;
- case BodyType.MULTIPART:
- contentType = 'multipart/form-data';
- postBody = params;
- break;
- case BodyType.NONE:
- case null:
- break;
- }
- // Set "Content-Type" header if it was previously unset.
- if (contentType != null &&
- !headers.keys.any((h) => h.toLowerCase() == 'content-type')) {
- headers['Content-Type'] = contentType;
- }
- return encodeBodyUtf8 && postBody is String
- ? utf8.encode(postBody)
- : postBody;
- }
- Future<ApiCallResponse> call(
- ApiCallOptions options, {
- http.Client? client,
- }) =>
- makeApiCall(
- callName: options.callName,
- apiUrl: options.apiUrl,
- callType: options.callType,
- headers: options.headers,
- params: options.params,
- body: options.body,
- bodyType: options.bodyType,
- returnBody: options.returnBody,
- encodeBodyUtf8: options.encodeBodyUtf8,
- decodeUtf8: options.decodeUtf8,
- alwaysAllowBody: options.alwaysAllowBody,
- cache: options.cache,
- isStreamingApi: options.isStreamingApi,
- options: options,
- client: client,
- );
- Future<ApiCallResponse> makeApiCall({
- required String callName,
- required String apiUrl,
- required ApiCallType callType,
- Map<String, dynamic> headers = const {},
- Map<String, dynamic> params = const {},
- String? body,
- BodyType? bodyType,
- bool returnBody = true,
- bool encodeBodyUtf8 = false,
- bool decodeUtf8 = false,
- bool alwaysAllowBody = false,
- bool cache = false,
- bool isStreamingApi = false,
- ApiCallOptions? options,
- http.Client? client,
- }) async {
- final callOptions = options ??
- ApiCallOptions(
- callName: callName,
- callType: callType,
- apiUrl: apiUrl,
- headers: headers,
- params: params,
- bodyType: bodyType,
- body: body,
- returnBody: returnBody,
- encodeBodyUtf8: encodeBodyUtf8,
- decodeUtf8: decodeUtf8,
- alwaysAllowBody: alwaysAllowBody,
- cache: cache,
- isStreamingApi: isStreamingApi,
- );
- // Modify for your specific needs if this differs from your API.
- if (_accessToken != null) {
- headers[HttpHeaders.authorizationHeader] = 'Bearer $_accessToken';
- }
- if (!apiUrl.startsWith('http')) {
- apiUrl = 'https://$apiUrl';
- }
- // If we've already made this exact call before and caching is on,
- // return the cached result.
- if (cache && _apiCache.containsKey(callOptions)) {
- return _apiCache[callOptions]!;
- }
- ApiCallResponse result;
- try {
- switch (callType) {
- case ApiCallType.GET:
- result = await urlRequest(
- callType,
- apiUrl,
- headers,
- params,
- returnBody,
- decodeUtf8,
- isStreamingApi,
- client: client,
- );
- break;
- case ApiCallType.DELETE:
- result = alwaysAllowBody
- ? await requestWithBody(
- callType,
- apiUrl,
- headers,
- params,
- body,
- bodyType,
- returnBody,
- encodeBodyUtf8,
- decodeUtf8,
- alwaysAllowBody,
- isStreamingApi,
- client: client,
- )
- : await urlRequest(
- callType,
- apiUrl,
- headers,
- params,
- returnBody,
- decodeUtf8,
- isStreamingApi,
- client: client,
- );
- break;
- case ApiCallType.POST:
- case ApiCallType.PUT:
- case ApiCallType.PATCH:
- result = await requestWithBody(
- callType,
- apiUrl,
- headers,
- params,
- body,
- bodyType,
- returnBody,
- encodeBodyUtf8,
- decodeUtf8,
- alwaysAllowBody,
- isStreamingApi,
- client: client,
- );
- break;
- }
- // If caching is on, cache the result (if present).
- if (cache) {
- _apiCache[callOptions] = result;
- }
- } catch (e) {
- result = ApiCallResponse(null, {}, -1, exception: e);
- }
- return result;
- }
- }
|