101 lines
3.1 KiB
Dart
101 lines
3.1 KiB
Dart
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<void> 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);
|
|
}
|
|
}
|