ionic-planb-logistic-app-fl.../lib/services/auth_service.dart
Claude Code 4b03e9aba5 Initial commit: Plan B Logistics Flutter app with dark mode and responsive design
Implements complete refactor of Ionic Angular logistics app to Flutter/Dart with:
- Svrnty dark mode console theme (Material Design 3)
- Responsive layouts (mobile, tablet, desktop) following FRONTEND standards
- CQRS API integration with Result<T> error handling
- OAuth2/OIDC authentication support (mocked for initial testing)
- Delivery route and delivery management features
- Multi-language support (EN/FR) with i18n
- Native integrations (camera, phone calls, maps)
- Strict typing throughout codebase
- Mock data for UI testing without backend

Follows all FRONTEND style guides, design patterns, and conventions.
App is running in dark mode and fully responsive across all device sizes.

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 04:58:10 -04:00

119 lines
3.2 KiB
Dart

import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import '../models/user_profile.dart';
class AuthService {
static const String _tokenKey = 'auth_token';
static const String _refreshTokenKey = 'refresh_token';
final FlutterAppAuth _appAuth;
final FlutterSecureStorage _secureStorage;
AuthService({
FlutterAppAuth? appAuth,
FlutterSecureStorage? secureStorage,
}) : _appAuth = appAuth ?? const FlutterAppAuth(),
_secureStorage = secureStorage ?? const FlutterSecureStorage();
Future<AuthResult> login() async {
try {
final result = await _appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(
'delivery-mobile-app',
'com.goutezplanb.delivery://callback',
discoveryUrl: 'https://auth.goutezplanb.com/realms/planb-internal/.well-known/openid-configuration',
scopes: const ['openid', 'profile', 'offline_access'],
promptValues: const ['login'],
),
);
// ignore: unnecessary_null_comparison
if (result == null) {
return const AuthResult.cancelled();
}
await _secureStorage.write(key: _tokenKey, value: result.accessToken ?? '');
if (result.refreshToken != null) {
await _secureStorage.write(key: _refreshTokenKey, value: result.refreshToken!);
}
return AuthResult.success(token: result.accessToken ?? '');
} catch (e) {
return AuthResult.error(error: e.toString());
}
}
Future<void> logout() async {
await Future.wait([
_secureStorage.delete(key: _tokenKey),
_secureStorage.delete(key: _refreshTokenKey),
]);
}
Future<String?> getToken() async {
return await _secureStorage.read(key: _tokenKey);
}
Future<String?> getRefreshToken() async {
return await _secureStorage.read(key: _refreshTokenKey);
}
bool isTokenValid(String? token) {
if (token == null || token.isEmpty) return false;
try {
return !JwtDecoder.isExpired(token);
} catch (e) {
return false;
}
}
UserProfile? decodeToken(String token) {
try {
final decodedToken = JwtDecoder.decode(token);
return UserProfile.fromJwtClaims(decodedToken);
} catch (e) {
return null;
}
}
Future<bool> isAuthenticated() async {
final token = await getToken();
return isTokenValid(token);
}
}
sealed class AuthResult {
const AuthResult();
factory AuthResult.success({required String token}) => _Success(token);
factory AuthResult.error({required String error}) => _Error(error);
const factory AuthResult.cancelled() = _Cancelled;
R when<R>({
required R Function(String token) success,
required R Function(String error) onError,
required R Function() cancelled,
}) {
return switch (this) {
_Success(:final token) => success(token),
_Error(:final error) => onError(error),
_Cancelled() => cancelled(),
};
}
}
final class _Success extends AuthResult {
final String token;
const _Success(this.token);
}
final class _Error extends AuthResult {
final String error;
const _Error(this.error);
}
final class _Cancelled extends AuthResult {
const _Cancelled();
}