import 'dart:convert'; import 'dart:core'; import 'dart:developer'; import 'dart:io'; import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; import 'package:http/http.dart' as http; import 'package:http_parser/http_parser.dart'; import 'package:mime_type/mime_type.dart'; import '/flutter_flow/uploaded_file.dart'; import 'get_streamed_response.dart'; enum ApiCallType { GET, POST, DELETE, PUT, PATCH } enum BodyType { NONE, JSON, TEXT, X_WWW_FORM_URL_ENCODED, MULTIPART, BLOB } 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 headers; final Map 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; 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 get props => [ callName, callType.name, apiUrl, headers, params, bodyType, body, returnBody, encodeBodyUtf8, decodeUtf8, alwaysAllowBody, cache, isStreamingApi, ]; static Map _cloneMap(Map map) { try { return json.decode(json.encode(map)) as Map; } catch (_) { return Map.from(map); } } } class ApiCallResponse { const ApiCallResponse( this.jsonBody, this.headers, this.statusCode, { this.response, this.streamedResponse, this.exception, }); final dynamic jsonBody; final Map headers; final int statusCode; final http.Response? response; final http.StreamedResponse? streamedResponse; final Object? exception; bool get succeeded => statusCode >= 200 && statusCode < 300; String getHeader(String headerName) => headers[headerName] ?? ''; String get bodyText => response?.body ?? (jsonBody is String ? jsonBody as String : jsonEncode(jsonBody)); String get exceptionMessage => exception.toString(); static ApiCallResponse fromHttpResponse( http.Response response, bool returnBody, bool decodeUtf8, BodyType? bodyType, ) { dynamic jsonBody; try { if (bodyType == BodyType.BLOB) { jsonBody = response.bodyBytes; } else { 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 response) => ApiCallResponse( response['body'], ApiManager.toStringMap(response['headers'] ?? {}), response['statusCode'] ?? 400, ); } class ApiManager { ApiManager._(); static final Map _apiCache = {}; static ApiManager? _instance; static ApiManager get instance => _instance ??= ApiManager._(); static String? _accessToken; static void clearCache(String callName) => _apiCache.keys .toSet() .forEach((k) => k.callName == callName ? _apiCache.remove(k) : null); static Map toStringMap(Map map) => map.map((key, value) => MapEntry(key.toString(), value.toString())); static String asQueryParams(Map map) => map.entries .map((e) => "${Uri.encodeComponent(e.key)}=${Uri.encodeComponent(e.value.toString())}") .join('&'); static Future urlRequest( ApiCallType callType, String apiUrl, Map headers, Map params, bool returnBody, bool decodeUtf8, bool isStreamingApi, { http.Client? client, BodyType? bodyType, }) 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, bodyType); } static Future requestWithBody( ApiCallType type, String apiUrl, Map headers, Map 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, bodyType); } 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, bodyType); } static Future multipartRequest( ApiCallType? type, String apiUrl, Map headers, Map params, bool returnBody, bool decodeUtf8, bool alwaysAllowBody, BodyType? bodyType, ) 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 || (e is List && e.firstOrNull is FFUploadedFile); final nonFileParams = toStringMap( Map.fromEntries(params.entries.where((e) => !isFile(e.value)))); List files = []; params.entries.where((e) => isFile(e.value)).forEach((e) { final param = e.value; final uploadedFiles = param is List ? param as List : [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 request.send()); return ApiCallResponse.fromHttpResponse( response, returnBody, decodeUtf8, bodyType); } 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 headers, Map? 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.BLOB: contentType = 'application/octet-stream'; postBody = body; break; case BodyType.NONE: case null: break; } 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 call(ApiCallOptions options) => 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, ); Future makeApiCall({ required String callName, required String apiUrl, required ApiCallType callType, Map headers = const {}, Map 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, ); if (_accessToken != null) { headers[HttpHeaders.authorizationHeader] = 'Bearer $_accessToken'; } if (!apiUrl.startsWith('http')) { apiUrl = 'https://$apiUrl'; } 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 (cache) { _apiCache[callOptions] = result; } } catch (e) { result = ApiCallResponse(null, {}, -1, exception: e); } print('API Call: $callName'); print('URL: $apiUrl'); print('Headers: $headers'); print('Params: $params'); print('Response: ${result.jsonBody}'); return result; } }