import 'package:dio/dio.dart'; import 'package:fancy_dio_inspector/fancy_dio_inspector.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../config/app_config.dart'; import '../storage/secure_identity_storage.dart'; import '../../features/auth/application/auth_session_coordinator.dart'; import 'api_exception.dart'; import 'auth_interceptor.dart'; import 'kratos_error_interceptor.dart'; part 'dio_provider.g.dart'; const _protobufContentType = 'application/x-protobuf'; /// Per-request override for native protobuf-over-HTTP endpoints. Options protobufOptions() => Options( responseType: ResponseType.bytes, contentType: _protobufContentType, headers: const {'Accept': _protobufContentType}, ); @Riverpod(keepAlive: true) AppConfig appConfig(Ref ref) => AppConfig.fromEnvironment(); @Riverpod(keepAlive: true) Dio dio(Ref ref) { final config = ref.watch(appConfigProvider); final sessionStorage = ref.watch(authSessionStorageProvider); final coordinator = ref.watch(authSessionCoordinatorProvider); final backendOrigin = Uri.parse(config.apiBaseUrl); final d = Dio( BaseOptions( baseUrl: config.apiBaseUrl, connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 30), responseType: ResponseType.bytes, contentType: _protobufContentType, headers: const { 'Accept': _protobufContentType, 'Content-Type': _protobufContentType, }, ), ); d.interceptors.add( AuthInterceptor(sessionStorage, backendOrigin: backendOrigin), ); d.interceptors.add(KratosErrorInterceptor()); d.interceptors.add(_SessionExpiryInterceptor(coordinator)); if (!kReleaseMode) { d.interceptors.add(LogInterceptor(requestBody: false, responseBody: false)); d.interceptors.add(FancyDioInterceptor()); } return d; } /// Explicit name for new code. [dioProvider] remains as a compatibility alias /// while existing feature clients are migrated incrementally. final backendDioProvider = dioProvider; @Riverpod(keepAlive: true) Dio publicDio(Ref ref) => Dio( BaseOptions( connectTimeout: const Duration(seconds: 15), receiveTimeout: const Duration(seconds: 30), ), ); class _SessionExpiryInterceptor extends Interceptor { _SessionExpiryInterceptor(this._coordinator); final AuthSessionCoordinator _coordinator; @override Future onError( DioException err, ErrorInterceptorHandler handler, ) async { final exception = ApiException.from(err); if (exception.isUnauthorized) { // Network code only emits the session event. Navigation and presentation // observe AuthState elsewhere. try { await _coordinator.expireIfNeeded(exception); } catch (error, stackTrace) { FlutterError.reportError( FlutterErrorDetails( exception: error, stack: stackTrace, library: 'jinzhi network', context: ErrorDescription('while expiring an unauthorized session'), ), ); } } handler.next(err); } }