Refactored error handling, configuration, and telemetry logic for improved readability, efficiency, and maintainability.

This commit is contained in:
oskar 2026-01-12 22:58:19 +01:00
parent d8f91f42e5
commit 73597b9bca
6 changed files with 80 additions and 89 deletions

View file

@ -11,6 +11,40 @@ class AppConfig {
required this.keycloakRedirectUrl, required this.keycloakRedirectUrl,
}); });
factory AppConfig.fromEnvironment() {
const apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: '');
const useLocalAuth =
bool.fromEnvironment('USE_LOCAL_AUTH', defaultValue: false);
const localTenantId = String.fromEnvironment(
'LOCAL_TENANT_ID',
defaultValue: '11111111-1111-1111-1111-111111111111',
);
const localRoles =
String.fromEnvironment('LOCAL_ROLES', defaultValue: 'CAREGIVER');
const keycloakIssuer =
String.fromEnvironment('KEYCLOAK_ISSUER', defaultValue: '');
const keycloakIssuerUri =
String.fromEnvironment('KEYCLOAK_ISSUER_URI', defaultValue: '');
final resolvedIssuer =
keycloakIssuer.isNotEmpty ? keycloakIssuer : keycloakIssuerUri;
const keycloakClientId =
String.fromEnvironment('KEYCLOAK_CLIENT_ID', defaultValue: '');
const keycloakRedirectUrl = String.fromEnvironment(
'KEYCLOAK_REDIRECT_URL',
defaultValue: '',
);
return AppConfig(
apiBaseUrl: apiBaseUrl,
useLocalAuth: useLocalAuth,
localTenantId: localTenantId,
localRoles: localRoles,
keycloakIssuer: resolvedIssuer,
keycloakClientId: keycloakClientId,
keycloakRedirectUrl: keycloakRedirectUrl,
);
}
final String apiBaseUrl; final String apiBaseUrl;
final bool useLocalAuth; final bool useLocalAuth;
final String localTenantId; final String localTenantId;

View file

@ -22,37 +22,7 @@ import '../features/telemetry/domain/token_provider.dart';
import '../features/telemetry/presentation/telemetry_controller.dart'; import '../features/telemetry/presentation/telemetry_controller.dart';
import '../features/telemetry/presentation/telemetry_state.dart'; import '../features/telemetry/presentation/telemetry_state.dart';
final appConfigProvider = Provider<AppConfig>((ref) { final appConfigProvider = Provider<AppConfig>((ref) => AppConfig.fromEnvironment());
const apiBaseUrl = String.fromEnvironment('API_BASE_URL', defaultValue: '');
const useLocalAuth =
bool.fromEnvironment('USE_LOCAL_AUTH', defaultValue: false);
const localTenantId = String.fromEnvironment(
'LOCAL_TENANT_ID',
defaultValue: '11111111-1111-1111-1111-111111111111',
);
const localRoles =
String.fromEnvironment('LOCAL_ROLES', defaultValue: 'CAREGIVER');
const keycloakIssuer = String.fromEnvironment('KEYCLOAK_ISSUER', defaultValue: '');
const keycloakIssuerUri =
String.fromEnvironment('KEYCLOAK_ISSUER_URI', defaultValue: '');
final resolvedIssuer =
keycloakIssuer.isNotEmpty ? keycloakIssuer : keycloakIssuerUri;
const keycloakClientId =
String.fromEnvironment('KEYCLOAK_CLIENT_ID', defaultValue: '');
const keycloakRedirectUrl = String.fromEnvironment(
'KEYCLOAK_REDIRECT_URL',
defaultValue: '',
);
return AppConfig(
apiBaseUrl: apiBaseUrl,
useLocalAuth: useLocalAuth,
localTenantId: localTenantId,
localRoles: localRoles,
keycloakIssuer: resolvedIssuer,
keycloakClientId: keycloakClientId,
keycloakRedirectUrl: keycloakRedirectUrl,
);
});
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) { final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
return const FlutterSecureStorage(); return const FlutterSecureStorage();
@ -85,9 +55,11 @@ final dioProvider = Provider<Dio>((ref) {
if (config.useLocalAuth) { if (config.useLocalAuth) {
final session = await localDataSource.readLocalSession(); final session = await localDataSource.readLocalSession();
if (session != null) { if (session != null) {
options.headers['X-Local-Email'] = session.email; options.headers.addAll({
options.headers['X-Local-Roles'] = session.roles; 'X-Local-Email': session.email,
options.headers['X-Tenant-Id'] = session.tenantId; 'X-Local-Roles': session.roles,
'X-Tenant-Id': session.tenantId,
});
} }
} else { } else {
final token = await localDataSource.readToken(); final token = await localDataSource.readToken();
@ -98,21 +70,18 @@ final dioProvider = Provider<Dio>((ref) {
handler.next(options); handler.next(options);
}, },
onError: (error, handler) async { onError: (error, handler) async {
if (config.useLocalAuth) {
handler.next(error);
return;
}
final response = error.response; final response = error.response;
final requestOptions = error.requestOptions; final requestOptions = error.requestOptions;
if (response?.statusCode != 401 || requestOptions.extra['retried'] == true) {
handler.next(error);
return;
}
final refreshToken = (await localDataSource.readToken())?.refreshToken; final refreshToken = (await localDataSource.readToken())?.refreshToken;
if (refreshToken == null || refreshToken.isEmpty) {
handler.next(error); if (config.useLocalAuth ||
return; response?.statusCode != 401 ||
requestOptions.extra['retried'] == true ||
refreshToken == null ||
refreshToken.isEmpty) {
return handler.next(error);
} }
try { try {
final newToken = final newToken =
await authRemoteDataSource.refreshToken(refreshToken: refreshToken); await authRemoteDataSource.refreshToken(refreshToken: refreshToken);

View file

@ -40,9 +40,15 @@ class AuthLocalDataSource {
} }
Future<LocalAuthSession?> readLocalSession() async { Future<LocalAuthSession?> readLocalSession() async {
final email = await _storage.read(key: _localEmailKey); final results = await Future.wait([
final roles = await _storage.read(key: _localRolesKey); _storage.read(key: _localEmailKey),
final tenantId = await _storage.read(key: _localTenantKey); _storage.read(key: _localRolesKey),
_storage.read(key: _localTenantKey),
]);
final email = results[0];
final roles = results[1];
final tenantId = results[2];
if (email == null || tenantId == null) { if (email == null || tenantId == null) {
return null; return null;
} }
@ -54,11 +60,13 @@ class AuthLocalDataSource {
} }
Future<void> clear() async { Future<void> clear() async {
await _storage.delete(key: _accessTokenKey); await Future.wait([
await _storage.delete(key: _refreshTokenKey); _storage.delete(key: _accessTokenKey),
await _storage.delete(key: _localEmailKey); _storage.delete(key: _refreshTokenKey),
await _storage.delete(key: _localRolesKey); _storage.delete(key: _localEmailKey),
await _storage.delete(key: _localTenantKey); _storage.delete(key: _localRolesKey),
_storage.delete(key: _localTenantKey),
]);
} }
} }

View file

@ -76,9 +76,8 @@ class AuthController extends Notifier<AuthState> {
String _friendlyError(Object error) { String _friendlyError(Object error) {
final message = error.toString(); final message = error.toString();
if (message.startsWith('Exception: ')) { return message.startsWith('Exception: ')
return message.substring('Exception: '.length); ? message.substring('Exception: '.length)
} : message;
return message;
} }
} }

View file

@ -13,13 +13,9 @@ class HomePage extends ConsumerStatefulWidget {
} }
class _HomePageState extends ConsumerState<HomePage> { class _HomePageState extends ConsumerState<HomePage> {
late final ProviderSubscription<TelemetryState> _telemetrySubscription;
@override @override
void initState() { Widget build(BuildContext context) {
super.initState(); ref.listen<TelemetryState>(telemetryControllerProvider, (previous, next) {
_telemetrySubscription =
ref.listenManual<TelemetryState>(telemetryControllerProvider, (previous, next) {
if (previous?.lastOutcome == next.lastOutcome || if (previous?.lastOutcome == next.lastOutcome ||
next.lastOutcome == null || next.lastOutcome == null ||
!mounted) { !mounted) {
@ -37,16 +33,7 @@ class _HomePageState extends ConsumerState<HomePage> {
); );
} }
}); });
}
@override
void dispose() {
_telemetrySubscription.close();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!; final l10n = AppLocalizations.of(context)!;
final telemetryState = ref.watch(telemetryControllerProvider); final telemetryState = ref.watch(telemetryControllerProvider);

View file

@ -28,17 +28,14 @@ class TelemetryServiceImpl implements TelemetryService {
} }
try { try {
final payload = _payloadBuilder.build();
final response = await _remoteDataSource.sendTestTelemetry( final response = await _remoteDataSource.sendTestTelemetry(
accessToken: accessToken, accessToken: accessToken,
payload: payload, payload: _payloadBuilder.build(),
); );
if (response.statusCode == 200 || response.statusCode == 201) { if (response.statusCode != 200 && response.statusCode != 201) {
return;
}
throw _failureFromStatus(response.statusCode); throw _failureFromStatus(response.statusCode);
}
} on HttpClientException catch (error) { } on HttpClientException catch (error) {
throw _failureFromHttp(error); throw _failureFromHttp(error);
} on TelemetryFailure { } on TelemetryFailure {
@ -53,19 +50,16 @@ class TelemetryServiceImpl implements TelemetryService {
} }
TelemetryFailure _failureFromStatus(int statusCode) { TelemetryFailure _failureFromStatus(int statusCode) {
if (statusCode == 401 || statusCode == 403) { final isUnauthorized = statusCode == 401 || statusCode == 403;
return TelemetryFailure( return TelemetryFailure(
'Not authorized', isUnauthorized ? 'Not authorized' : 'Telemetry request failed',
type: TelemetryFailureType.unauthorized, type: isUnauthorized
? TelemetryFailureType.unauthorized
: TelemetryFailureType.server,
statusCode: statusCode, statusCode: statusCode,
debugMessage: 'Telemetry request unauthorized ($statusCode)', debugMessage: isUnauthorized
); ? 'Telemetry request unauthorized ($statusCode)'
} : 'Telemetry request failed with status $statusCode',
return TelemetryFailure(
'Telemetry request failed',
type: TelemetryFailureType.server,
statusCode: statusCode,
debugMessage: 'Telemetry request failed with status $statusCode',
); );
} }