import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:frosty/main.dart'; import 'package:frosty/screens/onboarding/login_webview.dart'; import 'package:frosty/screens/settings/stores/auth_store.dart'; import 'package:frosty/utils/twitch_oauth_scopes.dart'; import 'package:frosty/widgets/frosty_dialog.dart'; import 'package:provider/provider.dart'; /// Dio interceptor that catches 401 Unauthorized errors and shows a login dialog class UnauthorizedInterceptor extends Interceptor { final AuthStore _authStore; UnauthorizedInterceptor(this._authStore); @override void onError(DioException err, ErrorInterceptorHandler handler) { // Check if this is a 401 Unauthorized error on a Twitch Helix/OAuth request if (err.response?.statusCode == 401 && _isTwitchHelixOrOAuth(err.requestOptions.uri)) { // For token validation requests, let the error propagate so validateToken can handle it if (err.requestOptions.uri.path.endsWith('/validate')) { handler.next(err); return; } // For other requests, show login dialog and reject so the Future completes _showLoginDialog(); handler.reject(err); return; } // For non-401 errors, continue with normal error handling handler.next(err); } void _showLoginDialog() { // Use the global navigator key to show dialog without needing BuildContext final context = navigatorKey.currentContext; if (context == null) return; // Use a flag to prevent multiple dialogs if (_isDialogShowing) return; _isDialogShowing = true; // Determine if user is logged in but missing scopes vs completely logged out final isLoggedIn = _authStore.isLoggedIn; final title = isLoggedIn ? 'Missing permissions' : 'Session expired'; final message = unauthorizedDialogMessage( isLoggedIn: isLoggedIn, grantedScopes: _authStore.grantedScopes, ); showDialog( context: context, barrierDismissible: false, // User must choose an action builder: (BuildContext dialogContext) { return FrostyDialog( title: title, message: message, actions: [ TextButton( onPressed: () { _isDialogShowing = false; Navigator.of(dialogContext).pop(); // Close dialog }, child: const Text('Cancel'), ), FilledButton( onPressed: () { _isDialogShowing = false; Navigator.of(dialogContext).pop(); // Close dialog // Navigate to login WebView Navigator.of(context).push( MaterialPageRoute( builder: (context) => Provider.value( value: _authStore, child: const LoginWebView(), ), ), ); }, child: Text('Log in'), ), ], ); }, ).then((_) { // Reset flag when dialog is dismissed _isDialogShowing = false; }); } static bool _isDialogShowing = false; bool _isTwitchHelixOrOAuth(Uri uri) { final url = uri.toString(); return url.startsWith('https://api.twitch.tv/helix') || url.startsWith('https://id.twitch.tv/oauth2'); } }