improve Firebase integration across Crashlytics, Analytics, and Performance
- Fix cold-start privacy bug: apply persisted collection preference on launch - Disable Firebase collection in debug builds to avoid polluting prod data - Add Crashlytics custom keys (is_logged_in, channel), user identifier, and WebSocket reconnection breadcrumbs for actionable crash reports - Record non-fatal errors for server failures, WebSocket errors, and global asset fetch failures (skip timeouts/network errors to avoid noise) - Update is_logged_in key and user identifier on login/logout transitions - Log auth reconnection exhaustion before forced logout - Add FirebaseAnalyticsObserver with named routes for automatic screen tracking - Add static routeName constants to avoid duplicated string literals - Add Firebase Performance Dio interceptor for automatic HTTP request tracing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f07fe621bc
commit
6ede000c66
21 changed files with 191 additions and 41 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
|
||||
/// Common type aliases to reduce repetition
|
||||
typedef JsonMap = Map<String, dynamic>;
|
||||
|
|
@ -157,29 +158,35 @@ abstract class BaseApiClient {
|
|||
|
||||
/// Converts DioException to appropriate ApiException subtype
|
||||
ApiException _handleError(DioException error) {
|
||||
switch (error.type) {
|
||||
case DioExceptionType.connectionTimeout:
|
||||
case DioExceptionType.sendTimeout:
|
||||
case DioExceptionType.receiveTimeout:
|
||||
return TimeoutException(_getTimeoutMessage(error.type));
|
||||
|
||||
case DioExceptionType.connectionError:
|
||||
return NetworkException(
|
||||
final exception = switch (error.type) {
|
||||
DioExceptionType.connectionTimeout ||
|
||||
DioExceptionType.sendTimeout ||
|
||||
DioExceptionType.receiveTimeout =>
|
||||
TimeoutException(_getTimeoutMessage(error.type)),
|
||||
DioExceptionType.connectionError => NetworkException(
|
||||
'No internet connection. Please check your network.',
|
||||
),
|
||||
DioExceptionType.badResponse => _handleHttpError(error),
|
||||
DioExceptionType.cancel => ApiException('Request was cancelled'),
|
||||
DioExceptionType.badCertificate =>
|
||||
ApiException('Security certificate error'),
|
||||
DioExceptionType.unknown =>
|
||||
ApiException(error.message ?? 'An unexpected error occurred'),
|
||||
};
|
||||
|
||||
// Record server errors and unexpected failures to Crashlytics.
|
||||
// Skip timeouts, network errors (normal offline conditions), 401s, and cancellations.
|
||||
if (exception is ServerException ||
|
||||
error.type == DioExceptionType.unknown ||
|
||||
error.type == DioExceptionType.badCertificate) {
|
||||
FirebaseCrashlytics.instance.recordError(
|
||||
exception,
|
||||
error.stackTrace,
|
||||
reason: '${error.requestOptions.method} ${error.requestOptions.uri}',
|
||||
);
|
||||
|
||||
case DioExceptionType.badResponse:
|
||||
return _handleHttpError(error);
|
||||
|
||||
case DioExceptionType.cancel:
|
||||
return ApiException('Request was cancelled');
|
||||
|
||||
case DioExceptionType.badCertificate:
|
||||
return ApiException('Security certificate error');
|
||||
|
||||
case DioExceptionType.unknown:
|
||||
return ApiException(error.message ?? 'An unexpected error occurred');
|
||||
}
|
||||
|
||||
return exception;
|
||||
}
|
||||
|
||||
/// Gets specific timeout message based on timeout type
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:frosty/apis/firebase_performance_interceptor.dart';
|
||||
|
||||
/// Centralized Dio client configuration with interceptors and error handling
|
||||
class DioClient {
|
||||
|
|
@ -55,6 +56,9 @@ class DioClient {
|
|||
);
|
||||
}
|
||||
|
||||
// Trace HTTP requests as Firebase Performance metrics
|
||||
dio.interceptors.add(FirebasePerformanceInterceptor());
|
||||
|
||||
// Add any additional interceptors provided
|
||||
if (additionalInterceptors != null) {
|
||||
for (final interceptor in additionalInterceptors) {
|
||||
|
|
|
|||
46
lib/apis/firebase_performance_interceptor.dart
Normal file
46
lib/apis/firebase_performance_interceptor.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:dio/dio.dart';
|
||||
import 'package:firebase_performance/firebase_performance.dart';
|
||||
|
||||
/// Dio interceptor that traces HTTP requests as Firebase Performance metrics.
|
||||
class FirebasePerformanceInterceptor extends Interceptor {
|
||||
static const _metricKey = 'firebase_performance_metric';
|
||||
|
||||
@override
|
||||
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
|
||||
final metric = FirebasePerformance.instance.newHttpMetric(
|
||||
options.uri.toString(),
|
||||
_getHttpMethod(options.method),
|
||||
);
|
||||
metric.start();
|
||||
options.extra[_metricKey] = metric;
|
||||
handler.next(options);
|
||||
}
|
||||
|
||||
@override
|
||||
void onResponse(Response response, ResponseInterceptorHandler handler) {
|
||||
_stopMetric(response.requestOptions, response.statusCode);
|
||||
handler.next(response);
|
||||
}
|
||||
|
||||
@override
|
||||
void onError(DioException err, ErrorInterceptorHandler handler) {
|
||||
_stopMetric(err.requestOptions, err.response?.statusCode);
|
||||
handler.next(err);
|
||||
}
|
||||
|
||||
void _stopMetric(RequestOptions options, int? statusCode) {
|
||||
final metric = options.extra.remove(_metricKey) as HttpMetric?;
|
||||
if (metric == null) return;
|
||||
if (statusCode != null) metric.httpResponseCode = statusCode;
|
||||
metric.stop();
|
||||
}
|
||||
|
||||
static HttpMethod _getHttpMethod(String method) => switch (method.toUpperCase()) {
|
||||
'GET' => HttpMethod.Get,
|
||||
'POST' => HttpMethod.Post,
|
||||
'PUT' => HttpMethod.Put,
|
||||
'DELETE' => HttpMethod.Delete,
|
||||
'PATCH' => HttpMethod.Patch,
|
||||
_ => HttpMethod.Get,
|
||||
};
|
||||
}
|
||||
|
|
@ -3,8 +3,10 @@ import 'dart:convert';
|
|||
|
||||
import 'package:advanced_in_app_review/advanced_in_app_review.dart';
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:firebase_performance/firebase_performance.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
|
@ -77,6 +79,19 @@ void main() async {
|
|||
// Initialize a settings store from the settings JSON string.
|
||||
final settingsStore = SettingsStore.fromJson(jsonDecode(userSettings));
|
||||
|
||||
// Disable Firebase collection in debug builds to avoid polluting production data.
|
||||
// In release builds, respect the user's preference.
|
||||
if (kDebugMode) {
|
||||
FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false);
|
||||
FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(false);
|
||||
FirebasePerformance.instance.setPerformanceCollectionEnabled(false);
|
||||
} else {
|
||||
final shareEnabled = settingsStore.shareCrashLogsAndAnalytics;
|
||||
FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(shareEnabled);
|
||||
FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(shareEnabled);
|
||||
FirebasePerformance.instance.setPerformanceCollectionEnabled(shareEnabled);
|
||||
}
|
||||
|
||||
// Create a MobX reaction that will save the settings on disk every time they are changed.
|
||||
autorun((_) => prefs.setString('settings', jsonEncode(settingsStore)));
|
||||
|
||||
|
|
@ -108,6 +123,10 @@ void main() async {
|
|||
dioClient.interceptors.add(UnauthorizedInterceptor(authStore));
|
||||
|
||||
await authStore.init();
|
||||
FirebaseCrashlytics.instance.setCustomKey('is_logged_in', authStore.isLoggedIn);
|
||||
if (authStore.isLoggedIn && authStore.user.details != null) {
|
||||
FirebaseCrashlytics.instance.setUserIdentifier(authStore.user.details!.id);
|
||||
}
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
|
|
@ -173,6 +192,11 @@ class _MyAppState extends State<MyApp> {
|
|||
: settingsStore.themeType == ThemeType.light
|
||||
? ThemeMode.light
|
||||
: ThemeMode.dark,
|
||||
navigatorObservers: [
|
||||
FirebaseAnalyticsObserver(
|
||||
analytics: FirebaseAnalytics.instance,
|
||||
),
|
||||
],
|
||||
home: widget.firstRun ? const OnboardingIntro() : const Home(),
|
||||
navigatorKey: navigatorKey,
|
||||
);
|
||||
|
|
@ -227,6 +251,7 @@ class _MyAppState extends State<MyApp> {
|
|||
final user = await twitchApi.getUser(userLogin: channelName);
|
||||
|
||||
final route = MaterialPageRoute(
|
||||
settings: const RouteSettings(name: VideoChat.routeName),
|
||||
builder: (context) => VideoChat(
|
||||
userId: user.id,
|
||||
userName: user.displayName,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ import 'package:simple_pip_mode/pip_widget.dart';
|
|||
|
||||
/// Creates a widget that shows the video stream (if live) and chat of the given user.
|
||||
class VideoChat extends StatefulWidget {
|
||||
static const routeName = 'VideoChat';
|
||||
|
||||
final String userId;
|
||||
final String userName;
|
||||
final String userLogin;
|
||||
|
|
|
|||
|
|
@ -416,6 +416,7 @@ class _ChatDetailsState extends State<ChatDetails> {
|
|||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: Settings.routeName),
|
||||
builder: (context) =>
|
||||
Settings(settingsStore: widget.chatStore.settings),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:frosty/apis/twitch_api.dart';
|
||||
|
|
@ -862,7 +863,14 @@ abstract class ChatStoreBase with Store {
|
|||
);
|
||||
}
|
||||
},
|
||||
onError: (error) => debugPrint('7TV events error: ${error.toString()}'),
|
||||
onError: (error) {
|
||||
debugPrint('7TV events error: ${error.toString()}');
|
||||
FirebaseCrashlytics.instance.recordError(
|
||||
error,
|
||||
StackTrace.current,
|
||||
reason: '7TV WebSocket error, channel=$channelName',
|
||||
);
|
||||
},
|
||||
onDone: () {
|
||||
_clearPendingDelayedCallbacks(clearChat: false);
|
||||
debugPrint('7TV events done');
|
||||
|
|
@ -874,6 +882,8 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
@action
|
||||
Future<void> connectToChat({bool isReconnect = false}) async {
|
||||
FirebaseCrashlytics.instance.setCustomKey('channel', channelName);
|
||||
|
||||
// Fetch assets first so they're available for all messages
|
||||
getAssets().catchError(
|
||||
(e) => debugPrint('Failed to fetch chat assets: $e'),
|
||||
|
|
@ -907,7 +917,14 @@ abstract class ChatStoreBase with Store {
|
|||
});
|
||||
}
|
||||
},
|
||||
onError: (error) => debugPrint('Chat error: ${error.toString()}'),
|
||||
onError: (error) {
|
||||
debugPrint('Chat error: ${error.toString()}');
|
||||
FirebaseCrashlytics.instance.recordError(
|
||||
error,
|
||||
StackTrace.current,
|
||||
reason: 'IRC WebSocket error, channel=$channelName',
|
||||
);
|
||||
},
|
||||
onDone: () async {
|
||||
_clearPendingDelayedCallbacks(clearSevenTV: false);
|
||||
|
||||
|
|
@ -924,6 +941,9 @@ abstract class ChatStoreBase with Store {
|
|||
}
|
||||
|
||||
if (_retries >= _maxRetries) {
|
||||
FirebaseCrashlytics.instance.log(
|
||||
'WebSocket reconnection failed after $_maxRetries attempts, channel=$channelName',
|
||||
);
|
||||
// Remove the reconnect message before showing final disconnect notice
|
||||
if (_reconnectMessage != null) {
|
||||
final index = _messages.indexOf(_reconnectMessage!);
|
||||
|
|
@ -949,6 +969,9 @@ abstract class ChatStoreBase with Store {
|
|||
|
||||
// Increment the retry count.
|
||||
_retries++;
|
||||
FirebaseCrashlytics.instance.log(
|
||||
'WebSocket reconnecting: attempt $_retries/$_maxRetries, channel=$channelName',
|
||||
);
|
||||
|
||||
// Start exponential backoff only after first failed attempt (capped at 8s).
|
||||
// First retry is immediate to catch brief network hiccups.
|
||||
|
|
|
|||
|
|
@ -202,6 +202,7 @@ class StreamInfoBar extends StatelessWidget {
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CategoryStreams.routeName),
|
||||
builder: (context) =>
|
||||
CategoryStreams(
|
||||
categoryId:
|
||||
|
|
@ -293,6 +294,7 @@ class StreamInfoBar extends StatelessWidget {
|
|||
onDoubleTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CategoryStreams.routeName),
|
||||
builder: (context) => CategoryStreams(
|
||||
categoryId:
|
||||
streamInfo?.gameId ?? '',
|
||||
|
|
|
|||
|
|
@ -44,7 +44,10 @@ class _HomeState extends State<Home> {
|
|||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const ReleaseNotes()),
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: ReleaseNotes.routeName),
|
||||
builder: (context) => const ReleaseNotes(),
|
||||
),
|
||||
).then((_) => prefs.setString('last_shown_version', currentVersion));
|
||||
}
|
||||
}
|
||||
|
|
@ -121,6 +124,7 @@ class _HomeState extends State<Home> {
|
|||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: Settings.routeName),
|
||||
builder: (context) => Settings(
|
||||
settingsStore: context.read<SettingsStore>(),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: VideoChat.routeName),
|
||||
builder: (context) => VideoChat(
|
||||
userId: channelInfo.broadcasterId,
|
||||
userName: channelInfo.broadcasterName,
|
||||
|
|
@ -120,6 +121,7 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
|
|||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: VideoChat.routeName),
|
||||
builder: (context) => VideoChat(
|
||||
userId: channel.id,
|
||||
userName: channel.displayName,
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ class LargeStreamCard extends StatelessWidget {
|
|||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: VideoChat.routeName),
|
||||
builder: (context) => VideoChat(
|
||||
userId: streamInfo.userId,
|
||||
userName: streamInfo.userName,
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class OfflineChannelCard extends StatelessWidget {
|
|||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: VideoChat.routeName),
|
||||
builder: (context) => VideoChat(
|
||||
userId: channelInfo.broadcasterId,
|
||||
userName: channelInfo.broadcasterName,
|
||||
|
|
|
|||
|
|
@ -168,6 +168,7 @@ class StreamCard extends StatelessWidget {
|
|||
? () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CategoryStreams.routeName),
|
||||
builder: (context) =>
|
||||
CategoryStreams(categoryId: streamInfo.gameId),
|
||||
),
|
||||
|
|
@ -203,6 +204,7 @@ class StreamCard extends StatelessWidget {
|
|||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: VideoChat.routeName),
|
||||
builder: (context) => VideoChat(
|
||||
userId: streamInfo.userId,
|
||||
userName: streamInfo.userName,
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ class CategoryCard extends StatelessWidget {
|
|||
? () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: CategoryStreams.routeName),
|
||||
builder: (context) => CategoryStreams(categoryId: category.id),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -13,6 +13,8 @@ import 'package:provider/provider.dart';
|
|||
|
||||
/// A widget that displays a list of streams under the provided [categoryId].
|
||||
class CategoryStreams extends StatefulWidget {
|
||||
static const routeName = 'CategoryStreams';
|
||||
|
||||
/// The category id, used for fetching the relevant streams in the [ListStore].
|
||||
final String categoryId;
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ class _OtherSettingsState extends State<OtherSettings> {
|
|||
title: const Text('Release notes'),
|
||||
onTap: () => Navigator.of(
|
||||
context,
|
||||
).push(MaterialPageRoute(builder: (context) => const ReleaseNotes())),
|
||||
).push(MaterialPageRoute(
|
||||
settings: const RouteSettings(name: ReleaseNotes.routeName),
|
||||
builder: (context) => const ReleaseNotes(),
|
||||
)),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete_outline_rounded),
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import 'package:simple_icons/simple_icons.dart';
|
|||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class Settings extends StatelessWidget {
|
||||
static const routeName = 'Settings';
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
const Settings({super.key, required this.settingsStore});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:frosty/apis/base_api_client.dart';
|
||||
|
|
@ -250,6 +251,7 @@ abstract class AuthBase with Store {
|
|||
}
|
||||
}
|
||||
|
||||
FirebaseCrashlytics.instance.setCustomKey('is_logged_in', _isLoggedIn);
|
||||
_error = null;
|
||||
} catch (e) {
|
||||
debugPrint(e.toString());
|
||||
|
|
@ -277,6 +279,8 @@ abstract class AuthBase with Store {
|
|||
// Set the login status to logged in.
|
||||
if (user.details != null) {
|
||||
_isLoggedIn = true;
|
||||
FirebaseCrashlytics.instance.setCustomKey('is_logged_in', true);
|
||||
FirebaseCrashlytics.instance.setUserIdentifier(user.details!.id);
|
||||
_stopReconnectLoop();
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -307,6 +311,8 @@ abstract class AuthBase with Store {
|
|||
|
||||
// Set the login status to logged out.
|
||||
_isLoggedIn = false;
|
||||
FirebaseCrashlytics.instance.setCustomKey('is_logged_in', false);
|
||||
FirebaseCrashlytics.instance.setUserIdentifier('');
|
||||
|
||||
debugPrint('Successfully logged out');
|
||||
} catch (e) {
|
||||
|
|
@ -321,6 +327,9 @@ abstract class AuthBase with Store {
|
|||
try {
|
||||
_reconnectAttempts++;
|
||||
if (_reconnectAttempts > _maxReconnectAttempts) {
|
||||
FirebaseCrashlytics.instance.log(
|
||||
'Auth reconnection exhausted after $_maxReconnectAttempts attempts',
|
||||
);
|
||||
await logout();
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import 'package:provider/provider.dart';
|
|||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
class ReleaseNotes extends StatefulWidget {
|
||||
static const routeName = 'ReleaseNotes';
|
||||
|
||||
const ReleaseNotes({super.key});
|
||||
|
||||
@override
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ class SettingsTileRoute extends StatelessWidget {
|
|||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
onTap: () => Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
settings: RouteSettings(name: title),
|
||||
builder: (context) => useScaffold
|
||||
? _BlurredSettingsPage(title: title, child: child)
|
||||
: child,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:frosty/apis/bttv_api.dart';
|
||||
import 'package:frosty/apis/ffz_api.dart';
|
||||
|
|
@ -166,10 +167,13 @@ abstract class GlobalAssetsStoreBase with Store {
|
|||
required bool showFFZEmotes,
|
||||
required bool showFFZBadges,
|
||||
}) async {
|
||||
// Error handler for emotes
|
||||
List<Emote> onEmoteError(dynamic error) {
|
||||
debugPrint('GlobalAssetsStore emote error: $error');
|
||||
return <Emote>[];
|
||||
void onAssetError(dynamic error, StackTrace stack) {
|
||||
debugPrint('GlobalAssetsStore error: $error');
|
||||
FirebaseCrashlytics.instance.recordError(
|
||||
error,
|
||||
stack,
|
||||
reason: 'Global asset fetch failed',
|
||||
);
|
||||
}
|
||||
|
||||
await Future.wait([
|
||||
|
|
@ -178,8 +182,9 @@ abstract class GlobalAssetsStoreBase with Store {
|
|||
twitchApi
|
||||
.getEmotesGlobal()
|
||||
.then((emotes) => _twitchGlobalEmotes = emotes)
|
||||
.catchError((e) {
|
||||
_twitchGlobalEmotes = onEmoteError(e);
|
||||
.catchError((e, StackTrace stack) {
|
||||
onAssetError(e, stack);
|
||||
_twitchGlobalEmotes = <Emote>[];
|
||||
return _twitchGlobalEmotes;
|
||||
}),
|
||||
// 7TV global emotes
|
||||
|
|
@ -187,8 +192,9 @@ abstract class GlobalAssetsStoreBase with Store {
|
|||
sevenTVApi
|
||||
.getEmotesGlobal()
|
||||
.then((emotes) => _sevenTVGlobalEmotes = emotes)
|
||||
.catchError((e) {
|
||||
_sevenTVGlobalEmotes = onEmoteError(e);
|
||||
.catchError((e, StackTrace stack) {
|
||||
onAssetError(e, stack);
|
||||
_sevenTVGlobalEmotes = <Emote>[];
|
||||
return _sevenTVGlobalEmotes;
|
||||
}),
|
||||
// BTTV global emotes
|
||||
|
|
@ -196,8 +202,9 @@ abstract class GlobalAssetsStoreBase with Store {
|
|||
bttvApi
|
||||
.getEmotesGlobal()
|
||||
.then((emotes) => _bttvGlobalEmotes = emotes)
|
||||
.catchError((e) {
|
||||
_bttvGlobalEmotes = onEmoteError(e);
|
||||
.catchError((e, StackTrace stack) {
|
||||
onAssetError(e, stack);
|
||||
_bttvGlobalEmotes = <Emote>[];
|
||||
return _bttvGlobalEmotes;
|
||||
}),
|
||||
// FFZ global emotes
|
||||
|
|
@ -205,8 +212,9 @@ abstract class GlobalAssetsStoreBase with Store {
|
|||
ffzApi
|
||||
.getEmotesGlobal()
|
||||
.then((emotes) => _ffzGlobalEmotes = emotes)
|
||||
.catchError((e) {
|
||||
_ffzGlobalEmotes = onEmoteError(e);
|
||||
.catchError((e, StackTrace stack) {
|
||||
onAssetError(e, stack);
|
||||
_ffzGlobalEmotes = <Emote>[];
|
||||
return _ffzGlobalEmotes;
|
||||
}),
|
||||
|
||||
|
|
@ -215,8 +223,8 @@ abstract class GlobalAssetsStoreBase with Store {
|
|||
twitchApi
|
||||
.getBadgesGlobal()
|
||||
.then((badges) => _twitchGlobalBadges = badges)
|
||||
.catchError((e) {
|
||||
debugPrint('GlobalAssetsStore badge error: $e');
|
||||
.catchError((e, StackTrace stack) {
|
||||
onAssetError(e, stack);
|
||||
_twitchGlobalBadges = <String, ChatBadge>{};
|
||||
return _twitchGlobalBadges;
|
||||
}),
|
||||
|
|
@ -224,8 +232,9 @@ abstract class GlobalAssetsStoreBase with Store {
|
|||
if (showBTTVBadges)
|
||||
bttvApi.getBadges().then((badges) => _bttvBadges = badges).catchError((
|
||||
e,
|
||||
StackTrace stack,
|
||||
) {
|
||||
debugPrint('GlobalAssetsStore badge error: $e');
|
||||
onAssetError(e, stack);
|
||||
_bttvBadges = <String, ChatBadge>{};
|
||||
return _bttvBadges;
|
||||
}),
|
||||
|
|
@ -233,8 +242,9 @@ abstract class GlobalAssetsStoreBase with Store {
|
|||
if (showFFZBadges)
|
||||
ffzApi.getBadges().then((badges) => _ffzBadges = badges).catchError((
|
||||
e,
|
||||
StackTrace stack,
|
||||
) {
|
||||
debugPrint('GlobalAssetsStore badge error: $e');
|
||||
onAssetError(e, stack);
|
||||
_ffzBadges = <String, List<ChatBadge>>{};
|
||||
return _ffzBadges;
|
||||
}),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue