Main coves client
1import 'package:dio/dio.dart';
2import 'package:flutter/foundation.dart';
3
4import '../models/coves_session.dart';
5
6/// Creates a Dio interceptor that handles authentication and automatic
7/// token refresh on 401 errors.
8///
9/// This shared utility eliminates duplication between VoteService and
10/// CommentService by providing a single implementation of:
11/// - Adding Authorization headers with fresh tokens on each request
12/// - Automatic retry with token refresh on 401 responses
13/// - Sign-out handling when refresh fails
14///
15/// Usage:
16/// ```dart
17/// _dio.interceptors.add(
18/// createAuthInterceptor(
19/// sessionGetter: () async => authProvider.session,
20/// tokenRefresher: authProvider.refreshToken,
21/// signOutHandler: authProvider.signOut,
22/// serviceName: 'MyService',
23/// ),
24/// );
25/// ```
26InterceptorsWrapper createAuthInterceptor({
27 required Future<CovesSession?> Function()? sessionGetter,
28 required Future<bool> Function()? tokenRefresher,
29 required Future<void> Function()? signOutHandler,
30 required String serviceName,
31 required Dio dio,
32}) {
33 return InterceptorsWrapper(
34 onRequest: (options, handler) async {
35 // Fetch fresh token before each request
36 final session = await sessionGetter?.call();
37 if (session != null) {
38 options.headers['Authorization'] = 'Bearer ${session.token}';
39 if (kDebugMode) {
40 debugPrint('🔐 $serviceName: Adding fresh Authorization header');
41 }
42 } else {
43 if (kDebugMode) {
44 debugPrint(
45 '⚠️ $serviceName: Session getter returned null - '
46 'making unauthenticated request',
47 );
48 }
49 }
50 return handler.next(options);
51 },
52 onError: (error, handler) async {
53 // Handle 401 errors with automatic token refresh
54 if (error.response?.statusCode == 401 && tokenRefresher != null) {
55 if (kDebugMode) {
56 debugPrint(
57 '🔄 $serviceName: 401 detected, attempting token refresh...',
58 );
59 }
60
61 // Check if we already retried this request (prevent infinite loop)
62 if (error.requestOptions.extra['retried'] == true) {
63 if (kDebugMode) {
64 debugPrint(
65 '⚠️ $serviceName: Request already retried after token refresh, '
66 'signing out user',
67 );
68 }
69 // Already retried once, don't retry again
70 if (signOutHandler != null) {
71 await signOutHandler();
72 }
73 return handler.next(error);
74 }
75
76 try {
77 // Attempt to refresh the token
78 final refreshSucceeded = await tokenRefresher();
79
80 if (refreshSucceeded) {
81 if (kDebugMode) {
82 debugPrint(
83 '✅ $serviceName: Token refresh successful, retrying request',
84 );
85 }
86
87 // Get the new session
88 final newSession = await sessionGetter?.call();
89
90 if (newSession != null) {
91 // Mark this request as retried to prevent infinite loops
92 error.requestOptions.extra['retried'] = true;
93
94 // Update the Authorization header with the new token
95 error.requestOptions.headers['Authorization'] =
96 'Bearer ${newSession.token}';
97
98 // Retry the original request with the new token
99 try {
100 final response = await dio.fetch(error.requestOptions);
101 return handler.resolve(response);
102 } on DioException catch (retryError) {
103 // If retry failed with 401 and already retried, we already
104 // signed out in the retry limit check above, so just pass
105 // the error through without signing out again
106 if (retryError.response?.statusCode == 401 &&
107 retryError.requestOptions.extra['retried'] == true) {
108 return handler.next(retryError);
109 }
110 // For other errors during retry, rethrow to outer catch
111 rethrow;
112 }
113 }
114 }
115
116 // Refresh failed, sign out the user
117 if (kDebugMode) {
118 debugPrint(
119 '❌ $serviceName: Token refresh failed, signing out user',
120 );
121 }
122 if (signOutHandler != null) {
123 await signOutHandler();
124 }
125 } on Exception catch (e) {
126 if (kDebugMode) {
127 debugPrint('❌ $serviceName: Error during token refresh: $e');
128 }
129 // Only sign out if we haven't already (avoid double sign-out)
130 // Check if this is a DioException from a retried request
131 final isRetriedRequest =
132 e is DioException &&
133 e.response?.statusCode == 401 &&
134 e.requestOptions.extra['retried'] == true;
135
136 if (!isRetriedRequest && signOutHandler != null) {
137 await signOutHandler();
138 }
139 }
140 }
141
142 // Log the error for debugging
143 if (kDebugMode) {
144 debugPrint('❌ $serviceName API Error: ${error.message}');
145 if (error.response != null) {
146 debugPrint(' Status: ${error.response?.statusCode}');
147 debugPrint(' Data: ${error.response?.data}');
148 }
149 }
150 return handler.next(error);
151 },
152 );
153}