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,
});
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 bool useLocalAuth;
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_state.dart';
final appConfigProvider = Provider<AppConfig>((ref) {
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 appConfigProvider = Provider<AppConfig>((ref) => AppConfig.fromEnvironment());
final secureStorageProvider = Provider<FlutterSecureStorage>((ref) {
return const FlutterSecureStorage();
@ -85,9 +55,11 @@ final dioProvider = Provider<Dio>((ref) {
if (config.useLocalAuth) {
final session = await localDataSource.readLocalSession();
if (session != null) {
options.headers['X-Local-Email'] = session.email;
options.headers['X-Local-Roles'] = session.roles;
options.headers['X-Tenant-Id'] = session.tenantId;
options.headers.addAll({
'X-Local-Email': session.email,
'X-Local-Roles': session.roles,
'X-Tenant-Id': session.tenantId,
});
}
} else {
final token = await localDataSource.readToken();
@ -98,21 +70,18 @@ final dioProvider = Provider<Dio>((ref) {
handler.next(options);
},
onError: (error, handler) async {
if (config.useLocalAuth) {
handler.next(error);
return;
}
final response = error.response;
final requestOptions = error.requestOptions;
if (response?.statusCode != 401 || requestOptions.extra['retried'] == true) {
handler.next(error);
return;
}
final refreshToken = (await localDataSource.readToken())?.refreshToken;
if (refreshToken == null || refreshToken.isEmpty) {
handler.next(error);
return;
if (config.useLocalAuth ||
response?.statusCode != 401 ||
requestOptions.extra['retried'] == true ||
refreshToken == null ||
refreshToken.isEmpty) {
return handler.next(error);
}
try {
final newToken =
await authRemoteDataSource.refreshToken(refreshToken: refreshToken);

View file

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

View file

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

View file

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

View file

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