import 'package:floaty/whitelabels.dart'; import 'package:floaty/features/api/models/definitions.dart'; import 'package:floaty/features/logs/repositories/log_service.dart'; import 'package:floaty/features/authentication/services/oauth2_service.dart'; import 'package:hive_ce/hive.dart'; import 'package:logging/logging.dart'; import 'dart:convert'; import 'package:flutter/material.dart'; import 'dart:async'; import 'package:get_it/get_it.dart'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:path_provider/path_provider.dart'; import 'package:http/http.dart' as http; final FPApiRequests fpApiRequests = GetIt.I(); /// Custom HTTP client wrapper that adds cookie support class _CookieHttpClient extends http.BaseClient { final http.Client _inner = http.Client(); final PersistCookieJar cookieJar; _CookieHttpClient(this.cookieJar); @override Future send(http.BaseRequest request) async { // Add cookies from jar to request final uri = request.url; final cookies = await cookieJar.loadForRequest(uri); if (cookies.isNotEmpty) { final cookieHeader = cookies.map((c) => '${c.name}=${c.value}').join('; '); request.headers['cookie'] = cookieHeader; } // Send request final response = await _inner.send(request); // Save cookies from response final setCookieHeaders = response.headers['set-cookie']; if (setCookieHeaders != null) { final responseCookies = setCookieHeaders .split(',') .map((str) => Cookie.fromSetCookieValue(str)) .toList(); await cookieJar.saveFromResponse(uri, responseCookies); } return response; } @override void close() { _inner.close(); } } /// Custom HTTP client wrapper that adds OAuth2 Bearer token authentication class _OAuth2HttpClient extends http.BaseClient { final _CookieHttpClient _inner; final OAuth2Service _oauth2Service; final String _whitelabel; _OAuth2HttpClient(this._inner, this._oauth2Service, this._whitelabel); @override Future send(http.BaseRequest request) async { // Get fresh access token (handles refresh if needed) String? accessToken = await _oauth2Service.getAccessToken(whitelabel: _whitelabel); // Check if token is expired and refresh if needed final isExpired = await _oauth2Service.isTokenExpired(whitelabel: _whitelabel); if (isExpired && accessToken != null) { final refreshToken = await _oauth2Service.getRefreshToken(whitelabel: _whitelabel); if (refreshToken != null) { final result = await _oauth2Service.refreshToken(refreshToken); if (result.isSuccess) { await _oauth2Service.storeTokens(result, whitelabel: _whitelabel); accessToken = result.accessToken; } } } // Add Authorization header if we have a token if (accessToken != null && accessToken.isNotEmpty) { request.headers['authorization'] = 'Bearer $accessToken'; } // Delegate to inner client (which handles cookies) final response = await _inner.send(request); return response; } @override void close() { // Don't close the inner client as it's shared across all OAuth2 clients } } class FPApiRequests { final Logger _log = Logger('FPApiRequests'); String userAgent = 'FloatyClient/error'; late final PersistCookieJar cookieJar; late final OAuth2Service _oauth2Service; final Map _oauth2Clients = {}; late final http.Client _fallbackClient; late final _CookieHttpClient _cookieClient; PackageInfo? packageInfo; // Feature flag for cookie-based testing bool get useCookieAuth => const bool.fromEnvironment('USE_COOKIE_AUTH', defaultValue: false); /// Get the appropriate HTTP client for a specific whitelabel Future getHttpClient(String whitelabel) async { final authMethod = await _oauth2Service.getAuthMethod(whitelabel: whitelabel); final useCookie = (authMethod == 'cookie') || (authMethod == null && useCookieAuth); if (!useCookie) { // Try to get or create OAuth2 client for this whitelabel if (!_oauth2Clients.containsKey(whitelabel)) { final client = await createOAuth2Client(whitelabel: whitelabel); if (client != null) { _oauth2Clients[whitelabel] = client; } } if (_oauth2Clients.containsKey(whitelabel)) { return _oauth2Clients[whitelabel]!; } } // Fallback to cookie client return _fallbackClient; } /// Get the appropriate HTTP client (OAuth2 or fallback with cookies) /// This is kept for backwards compatibility but deprecated @Deprecated('Use getHttpClient(whitelabel) instead') http.Client get httpClient => _fallbackClient; FPApiRequests() { _init(); } Future _init() async { packageInfo = await PackageInfo.fromPlatform(); const flavor = String.fromEnvironment('FLUTTER_FLAVOR', defaultValue: 'release'); userAgent = 'FloatyClient/${packageInfo?.version}+${packageInfo?.buildNumber}-$flavor'; final dir = await getApplicationSupportDirectory(); _oauth2Service = OAuth2Service.instance; cookieJar = PersistCookieJar( storage: FileStorage('${dir.path}/.cookies/'), ); // Create cookie HTTP client _cookieClient = _CookieHttpClient(cookieJar); // Create fallback HTTP client with cookie support _fallbackClient = _cookieClient; // Initialize OAuth2 clients for all logged-in whitelabels if (!useCookieAuth) { _initOAuth2Clients(); } } /// Initialize OAuth2 Clients for all logged-in whitelabels Future _initOAuth2Clients() async { final loggedInLabels = await whitelabels.getLoggedInLabels(); for (final label in loggedInLabels) { final whitelabelName = label.split('-')[0]; final authMethod = await _oauth2Service.getAuthMethod(whitelabel: whitelabelName); if (authMethod == 'oauth2') { final client = await createOAuth2Client(whitelabel: whitelabelName); if (client != null) { _oauth2Clients[whitelabelName] = client; } } } } /// Create or recreate OAuth2 client from stored credentials for a specific whitelabel Future createOAuth2Client({String? whitelabel}) async { try { final whitelabelName = whitelabel ?? 'default'; // if (kDebugMode) { // print( // '[FPApiRequests] createOAuth2Client() for whitelabel: $whitelabelName'); // } final accessToken = await _oauth2Service.getAccessToken(whitelabel: whitelabelName); final refreshToken = await _oauth2Service.getRefreshToken(whitelabel: whitelabelName); if (accessToken == null || refreshToken == null) { return null; } // Create a custom HTTP client that handles OAuth2 authentication and cookies final client = _OAuth2HttpClient(_cookieClient, _oauth2Service, whitelabelName); return client; } catch (e) { return null; } } /// Remove OAuth2 client from cache (e.g., on logout) void clearOAuth2Client(String whitelabel) { if (_oauth2Clients.containsKey(whitelabel)) { _oauth2Clients[whitelabel]?.close(); _oauth2Clients.remove(whitelabel); } } Future postData( String apiUrl, String whitelabel, [ Map? body, Map? queryParams, ]) async { try { final whiteLabel = whitelabels.getWhitelabel(whitelabel); final client = await getHttpClient(whiteLabel.friendlyName); var url = Uri.parse('${whiteLabel.apiUrl}/$apiUrl'); if (queryParams != null && queryParams.isNotEmpty) { url = url.replace(queryParameters: queryParams); } final response = await client.post( url, headers: {'Content-Type': 'application/json', 'User-Agent': userAgent}, body: body != null ? jsonEncode(body) : null, ); return response.body; } catch (e) { return 'Error: $e'; } } Future fetchData( String apiUrl, String whitelabel, [ Map? queryParams, ]) async { try { final whiteLabel = whitelabels.getWhitelabel(whitelabel); final client = await getHttpClient(whiteLabel.friendlyName); var url = Uri.parse('${whiteLabel.apiUrl}/$apiUrl'); if (queryParams != null && queryParams.isNotEmpty) { url = url.replace(queryParameters: queryParams); } final response = await client.get( url, headers: {'User-Agent': userAgent}, ); return response.body; } catch (e) { return {'statusCode': 500, 'body': e.toString()}; } } Stream getUser(String whitelabel) async* { try { final user = await fetchData('v3/user/self', whitelabel); if (user != null && user is String && user.isNotEmpty) { yield UserSelfV3Response.fromJson(jsonDecode(user)); } } catch (e) { yield UserSelfV3Response(); } } Future getUserNOS(String whitelabel) async { try { final user = await fetchData('v3/user/self', whitelabel); return UserSelfV3Response.fromJson(jsonDecode(user)); } catch (e) { return UserSelfV3Response(); } } Future> getNamedUser(String whitelabel, String username) async { final user = await fetchData('v3/user/named?username[0]=$username', whitelabel); if (user != null && user.isNotEmpty) { return jsonDecode(user); } return []; } Future getActivity(String whitelabel, String userId) async { final activity = await fetchData('v3/user/activity?id=$userId', whitelabel); if (activity != null && activity.isNotEmpty) { return activity; } return []; } Future registerNotifications(String whitelabel, String token) async { final response = await postData('v3/push/web/register?token=$token', whitelabel); LogService.logInfo('✅ Notifications registered! Response: $response'); return response; } Stream> getSubscribedCreators(String whitelabel) async* { try { final subres = await fetchData('v3/user/subscriptions?active=true', whitelabel); if (subres != null && subres is String && subres.isNotEmpty) { List subscriptions = jsonDecode(subres); List creatorIds = subscriptions .where((subscription) => subscription is Map && subscription['creator'] is String) .map((subscription) => subscription['creator'] as String) .toSet() // Remove duplicates .toList(); List creators = []; for (String id in creatorIds) { try { final creatorInfo = await fetchData('v3/creator/info?id=$id', whitelabel); if (creatorInfo != null && creatorInfo is String && creatorInfo.isNotEmpty) { Map creatorJson = jsonDecode(creatorInfo); creators.add(CreatorModelV3.fromJson(creatorJson)); } } catch (e) { continue; } } yield creators; } } catch (e) { yield []; } } Stream> getSubscribedCreatorsIds(String whitelabel) async* { try { final subres = await fetchData('v3/user/subscriptions?active=true', whitelabel); if (subres != null && subres.isNotEmpty) { List subscriptions = jsonDecode(subres); List creatorIds = subscriptions .where((subscription) => subscription is Map && subscription['creator'] is String) .map((subscription) => subscription['creator'] as String) .toList(); yield creatorIds; } } catch (e) { yield []; } } Future getMultiCreatorVideoFeed( String whitelabel, List creatorIds, int limit, {List? lastElements}) async { try { if (creatorIds.isEmpty) { return ContentCreatorListV3Response(blogPosts: [], lastElements: []); } final Map queryParams = { for (int i = 0; i < creatorIds.length; i++) 'ids[$i]': creatorIds[i], 'limit': limit.toString(), }; if (lastElements != null && lastElements.isNotEmpty) { for (int i = 0; i < lastElements.length; i++) { queryParams['fetchAfter[$i][creatorId]'] = lastElements[i].creatorId; queryParams['fetchAfter[$i][blogPostId]'] = lastElements[i].blogPostId; queryParams['fetchAfter[$i][moreFetchable]'] = lastElements[i].moreFetchable.toString(); } } final response = await fetchData('v3/content/creator/list', whitelabel, queryParams); if (response != null && response.isNotEmpty) { return ContentCreatorListV3Response.fromJson(jsonDecode(response)); } return ContentCreatorListV3Response(blogPosts: [], lastElements: []); } catch (error) { return ContentCreatorListV3Response(blogPosts: [], lastElements: []); } } Future> getChannelVideoFeed( String whitelabel, String creator, int limit, int fetchAfter, { String? channel, String? searchQuery, Set? contentTypes, RangeValues? durationRange, DateTime? fromDate, DateTime? toDate, bool? isAscending, }) async { try { bool? hasVideo = contentTypes?.contains('Video'); bool? hasAudio = contentTypes?.contains('Audio'); bool? hasPicture = contentTypes?.contains('Picture'); bool? hasText = contentTypes?.contains('Text'); final Map queryParams = { 'id': creator, 'limit': limit.toString(), 'fetchAfter': fetchAfter.toString(), if (channel != null) 'channel': channel, if (fromDate != null) 'fromDate': fromDate.toUtc().toIso8601String(), if (toDate != null) 'toDate': toDate.toUtc().toIso8601String(), if (contentTypes != null) ...{ 'hasVideo': hasVideo.toString(), 'hasAudio': hasAudio.toString(), 'hasPicture': hasPicture.toString(), 'hasText': hasText.toString(), }, if (durationRange != null) ...{ if ((durationRange.start * 60).round().toString() != '0') 'fromDuration': (durationRange.start * 60).round().toString(), if ((durationRange.end * 60).round().toString() != '10800') 'toDuration': (durationRange.end * 60).round().toString(), }, if (isAscending != null) 'sort': isAscending ? 'ASC' : 'DESC', if (searchQuery != null && searchQuery.isNotEmpty) 'search': searchQuery, }; final response = await fetchData('v3/content/creator', whitelabel, queryParams); if (response != null && response.isNotEmpty) { List decodedResponse = jsonDecode(response); return decodedResponse .map((item) => BlogPostModelV3.fromJson(item)) .toList(); } return []; } catch (error) { return []; } } Future> getVideoProgress( String whitelabel, List blogPostIds, ) async { final Map requestBody = { "ids": blogPostIds, "contentType": "blogPost" }; try { final response = await postData('v3/content/get/progress', whitelabel, requestBody); if (response.isNotEmpty) { final List jsonResponse = jsonDecode(response); return jsonResponse .map((data) => GetProgressResponse.fromJson(data)) .toList(); } return []; } catch (e) { return []; } } Stream> getCreators(String whitelabel, {String? query}) async* { try { final apiUrl = query != null ? 'v3/creator/list?search=$query' : 'v3/creator/list'; final subres = await fetchData(apiUrl, whitelabel); if (subres != null && subres.isNotEmpty) { List jsonList = jsonDecode(subres); yield jsonList.map((json) => CreatorModelV3.fromJson(json)).toList(); } } catch (e) { yield []; } } Stream> getCreatorDiscovery(String whitelabel, {String? query}) async* { try { final apiUrl = query != null ? 'v3/creator/discover?searchField=$query&featuredBlogPosts=1&creatorStats=true' : 'v3/creator/discover?featuredBlogPosts=1&creatorStats=true'; final subres = await fetchData(apiUrl, whitelabel); if (subres != null && subres.isNotEmpty) { List jsonList = jsonDecode(subres)['creators']; yield jsonList .map((json) => CreatorDiscoveryResponse.fromJson(json)) .toList(); } } catch (e) { yield []; } } Future> getHistory(String whitelabel, {int? offset}) async { try { int offsetInt = offset ?? 0; final response = await fetchData('v3/content/history?offset=$offsetInt', whitelabel); if (response != null && response.isNotEmpty) { List jsonList = jsonDecode(response); return jsonList.map((json) => HistoryModelV3.fromJson(json)).toList(); } return []; } catch (e) { return []; } } Stream getCreator(String whitelabel, {String? urlname, int? id}) async* { try { final apiUrl = urlname != null ? 'v3/creator/named?creatorURL=$urlname' : 'v3/creator/info?id=$id'; final creatorInfo = await fetchData(apiUrl, whitelabel); if (creatorInfo != null && creatorInfo.isNotEmpty) { List creatorList = jsonDecode(creatorInfo); if (creatorList.isNotEmpty) { Map creatorJson = creatorList.first; final freshCreator = CreatorModelV3.fromJson(creatorJson); yield freshCreator; } } } catch (e) { yield CreatorModelV3(); } } Future getStats(String whitelabel, String creatorId) async { try { final stats = await fetchData('v2/plan/info?creatorId=$creatorId', whitelabel); if (stats != null && stats.isNotEmpty) { dynamic statsJson = jsonDecode(stats); return StatsModel( totalIncome: statsJson['totalIncome'], totalSubcriberCount: statsJson['totalSubscriberCount'], ); } return StatsModel(totalIncome: 0, totalSubcriberCount: 0); } catch (e) { return StatsModel(totalIncome: 0, totalSubcriberCount: 0); } } Future> getStatsV3( String whitelabel, String creatorId) async { try { final stats = await fetchData('v3/creator/stats?id=$creatorId', whitelabel); if (stats != null && stats.isNotEmpty) { dynamic statsJson = jsonDecode(stats); return statsJson; } return {"error": true}; } catch (e) { return {"error": true}; } } Future> getUserInfo(String whitelabel) async { try { final userinfo = await fetchData('v3/user/self', whitelabel); if (userinfo != null && userinfo.isNotEmpty) { dynamic userinfoJson = jsonDecode(userinfo); return userinfoJson; } return {"error": true}; } catch (e) { return {"error": true}; } } Future> getInvoices(String whitelabel) async { try { final invoices = await fetchData('v3/payment/invoice/list', whitelabel); if (invoices != null && invoices.isNotEmpty) { dynamic invoicesJson = jsonDecode(invoices); return invoicesJson; } return {"error": true}; } catch (e) { return {"error": true}; } } Stream getBlogPost( String whitelabel, String blogPostId) async* { try { final apiUrl = 'v3/content/post?id=$blogPostId'; final response = await fetchData(apiUrl, whitelabel); if (response != null && response.isNotEmpty) { try { final jsonData = jsonDecode(response); final parsed = ContentPostV3Response.fromJson(jsonData); yield parsed; } catch (e) { yield ContentPostV3Response(); } } else { yield ContentPostV3Response(); } } catch (e) { yield ContentPostV3Response(); } } Future likeBlogPost(String whitelabel, String blogPostId) async { final response = await postData('v3/content/like', whitelabel, {'contentType': 'blogPost', 'id': blogPostId}); final decodedres = jsonDecode(response); if (decodedres.contains('like')) { return 'success'; } else if (response.toString() == '[]') { return 'removed'; } else { return 'fail'; } } Future dislikeBlogPost(String whitelabel, String blogPostId) async { final response = await postData('v3/content/dislike', whitelabel, {'contentType': 'blogPost', 'id': blogPostId}); final decodedres = jsonDecode(response); if (decodedres.contains('dislike')) { return 'success'; } else if (response.toString() == '[]') { return 'removed'; } else { return 'fail'; } } Future likeComment( String whitelabel, String commentId, String blogPostId) async { final response = await postData('v3/comment/like', whitelabel, {'comment': commentId, 'blogPost': blogPostId}); if (response.isEmpty || response.toString() == '[]' || response == 'null') { return 'removed'; } final decodedres = jsonDecode(response); if (decodedres != null && decodedres.toString().contains('like')) { return 'success'; } return 'fail'; } Future dislikeComment( String whitelabel, String commentId, String blogPostId) async { final response = await postData('v3/comment/dislike', whitelabel, {'comment': commentId, 'blogPost': blogPostId}); if (response.isEmpty || response.toString() == '[]' || response == 'null') { return 'removed'; } final decodedres = jsonDecode(response); if (decodedres != null && decodedres.toString().contains('dislike')) { return 'success'; } return 'fail'; } Future> getRecommended( String whitelabel, String blogPostId) async { try { final response = await fetchData('v3/content/related?id=$blogPostId', whitelabel); if (response != null && response.isNotEmpty) { List decodedResponse = json.decode(response) as List; return decodedResponse .map((item) => BlogPostModelV3.fromJson(item)) .toList(); } return []; } catch (error) { return []; } } Future> getComments(String whitelabel, String blogPostId, int limit, String sortBy, String sortOrder, {String? fetchAfter}) async { try { dynamic response; if (fetchAfter != null) { response = await fetchData( 'v3/comment?blogPost=$blogPostId&limit=$limit&fetchAfter=$fetchAfter&sortBy=$sortBy&sortDirection=$sortOrder', whitelabel); } else { response = await fetchData( 'v3/comment?blogPost=$blogPostId&limit=$limit&sortBy=$sortBy&sortDirection=$sortOrder', whitelabel); } if (response != null && response.isNotEmpty) { final List decodedData = json.decode(response) as List; return decodedData .map((item) => CommentModel.fromJson(item as Map)) .toList(); } return []; } catch (error) { return []; } } Future> getReplies(String whitelabel, String comment, String blogPost, int limit, String rid) async { try { final response = await fetchData( 'v3/comment/replies?comment=$comment&blogPost=$blogPost&limit=$limit&rid=$rid', whitelabel); if (response != null && response.isNotEmpty) { final List decodedData = json.decode(response) as List; return decodedData .map((item) => CommentModel.fromJson(item as Map)) .toList(); } return []; } catch (error) { return []; } } Future comment( String whitelabel, String blogPostId, String comment, {String? replyto}) async { try { final response = await postData('v3/comment', whitelabel, { 'blogPost': blogPostId, if (replyto != null) 'replyTo': replyto, 'text': comment }); if (response.isNotEmpty) { final decodedData = json.decode(response) as Map; return CommentModel.fromJson(decodedData); } return null; } catch (error) { return null; } } Future deleteComment(String whitelabel, String commentId) async { final response = await postData('v3/comment/delete', whitelabel, { 'comment': commentId, }); return response; } Future editComment( String whitelabel, String commentId, String text) async { final response = await postData('v3/comment/edit', whitelabel, { 'comment': commentId, 'text': text, }); return response; } Future> getDelivery( String whitelabel, String scenario, String entityId) async { try { final whiteLabel = whitelabels.getWhitelabel(whitelabel); final client = await getHttpClient(whiteLabel.friendlyName); final url = Uri.parse( '${whiteLabel.apiUrl}/v3/delivery/info?scenario=$scenario&entityId=$entityId${scenario != 'download' ? '&outputKind=hls.mpegts' : ''}'); final response = await client.get( url, headers: {'User-Agent': userAgent}, ); _log.info(scenario != 'download' ? 'HLS Delivery URL:' : 'Download URL:'); _log.fine(response.body); return { 'body': response.body, 'headers': response.headers, 'statusCode': response.statusCode, }; } catch (e) { return { 'body': '', 'headers': {}, 'statusCode': 500, }; } } Future getContent(String whitelabel, String type, String id) async { try { final res = await fetchData('v3/content/$type?id=$id', whitelabel); return res; } catch (e) { return ''; } } Future submitVote(String whitelabel, String id, int vote) async { final response = await postData('v3/poll/vote?pollId=$id&optionIndex=$vote', whitelabel); if (response == 'OK') { return 200; } else { return 500; } } //because of the dumb way i handle progress (i have 3 different things that can call progress) we debounce this to avoid spam and stale data. Timer? _progressDebounceTimer; final Map> _pendingProgress = {}; /// Store offline video progress locally Future _storeOfflineProgress( String attachmentId, int progress, String contentType) async { try { final box = await Hive.openBox('offline_progress'); await box.put(attachmentId, { 'progress': progress, 'contentType': contentType, 'timestamp': DateTime.now().toIso8601String(), }); _log.info('Stored offline progress for $attachmentId: $progress seconds'); } catch (e) { _log.warning('Failed to store offline progress: $e'); } } /// Get stored offline progress for a specific attachment Future getOfflineProgress(String attachmentId) async { try { final box = await Hive.openBox('offline_progress'); final data = box.get(attachmentId) as Map?; if (data != null && data['progress'] is int) { _log.info( 'Retrieved offline progress for $attachmentId: ${data['progress']} seconds'); return data['progress'] as int; } } catch (e) { _log.warning('Failed to get offline progress: $e'); } return 0; } /// Sync all stored offline progress to Floatplane Future syncOfflineProgress(String whitelabel) async { try { final box = await Hive.openBox('offline_progress'); final keys = box.keys.toList(); if (keys.isEmpty) { _log.info('No offline progress to sync'); return; } _log.info('Syncing ${keys.length} offline progress entries...'); int synced = 0; for (final key in keys) { try { final data = box.get(key) as Map?; if (data != null) { await postData('v3/content/progress', whitelabel, { 'id': key, 'contentType': data['contentType'], 'progress': data['progress'], }); await box.delete(key); synced++; } } catch (e) { _log.warning('Failed to sync progress for $key: $e'); } } _log.info('Successfully synced $synced offline progress entries'); } catch (e) { _log.warning('Failed to sync offline progress: $e'); } } Future progress( String whitelabel, String id, int progress, String contentType, {bool isOffline = false}) async { if (id.isEmpty) return; // Store offline progress locally instead of sending to API if (isOffline) { await _storeOfflineProgress(id, progress, contentType); return; } _pendingProgress[id] = { 'progress': progress, 'contentType': contentType, }; _progressDebounceTimer?.cancel(); _progressDebounceTimer = Timer(const Duration(seconds: 5), () async { for (final entry in _pendingProgress.entries) { final String entryId = entry.key; final Map params = entry.value; await postData('v3/content/progress', whitelabel, { 'id': entryId, 'contentType': params['contentType'], 'progress': params['progress'], }); } _pendingProgress.clear(); }); } Future iprogress( String whitelabel, String id, int progress, String contentType, {bool isOffline = false}) async { if (id.isEmpty) return; // Store offline progress locally instead of sending to API if (isOffline) { await _storeOfflineProgress(id, progress, contentType); return; } await postData('v3/content/progress', whitelabel, { 'id': id, 'contentType': contentType, 'progress': progress, }); } Future deleteHistory(String whitelabel) async { await postData('v3/content/progress/clear', whitelabel); } Future logout(String whitelabel) async { final whiteLabel = whitelabels.getWhitelabel(whitelabel); final client = await getHttpClient(whiteLabel.friendlyName); final url = Uri.parse('${whiteLabel.apiUrl}/v3/auth/logout'); final response = await client.post( url, headers: {'User-Agent': userAgent}, ); return response.body; } Future> subscribe( String whitelabel, String creatorId) async { final whiteLabel = whitelabels.getWhitelabel(whitelabel); final client = await getHttpClient(whiteLabel.friendlyName); final url = Uri.parse('${whiteLabel.apiUrl}/v3/creator/subscribe?id=$creatorId'); final response = await client.post( url, headers: {'User-Agent': userAgent}, ); return jsonDecode(response.body); } Future unsubscribe(String whitelabel, String creatorId) async { final whiteLabel = whitelabels.getWhitelabel(whitelabel); final client = await getHttpClient(whiteLabel.friendlyName); final url = Uri.parse('${whiteLabel.apiUrl}/v3/creator/unsubscribe?id=$creatorId'); final response = await client.post( url, headers: {'User-Agent': userAgent}, ); return response.body; } }