feat(mobile): Implement Keycloak WebView authentication with HTTP callback
- Replace flutter_appauth with custom WebView implementation to resolve deep link issues - Add KeycloakWebViewAuthService with integrated WebView for seamless authentication - Configure Android manifest for HTTP cleartext traffic support - Add network security config for development environment (192.168.1.11) - Update Keycloak client to use HTTP callback endpoint (http://192.168.1.11:8080/auth/callback) - Remove obsolete keycloak_auth_service.dart and temporary scripts - Clean up dependencies and regenerate injection configuration - Tested successfully on multiple Android devices (Xiaomi 2201116TG, SM A725F) BREAKING CHANGE: Authentication flow now uses WebView instead of external browser - Users will see Keycloak login page within the app instead of browser redirect - Resolves ERR_CLEARTEXT_NOT_PERMITTED and deep link state management issues - Maintains full OIDC compliance with PKCE flow and secure token storage Technical improvements: - WebView with custom navigation delegate for callback handling - Automatic token extraction and user info parsing from JWT - Proper error handling and user feedback - Consistent authentication state management across app lifecycle
This commit is contained in:
@@ -5,6 +5,15 @@ import '../../../../shared/theme/app_theme.dart';
|
||||
import '../bloc/membres_bloc.dart';
|
||||
import '../bloc/membres_event.dart';
|
||||
import '../bloc/membres_state.dart';
|
||||
import '../widgets/dashboard/welcome_section_widget.dart';
|
||||
import '../widgets/dashboard/members_kpi_section_widget.dart';
|
||||
import '../widgets/dashboard/members_quick_actions_widget.dart';
|
||||
import '../widgets/dashboard/members_analytics_widget.dart';
|
||||
import '../widgets/dashboard/members_enhanced_list_widget.dart';
|
||||
import '../widgets/dashboard/members_recent_activities_widget.dart';
|
||||
import '../widgets/dashboard/members_advanced_filters_widget.dart';
|
||||
import '../widgets/dashboard/members_smart_search_widget.dart';
|
||||
import '../widgets/dashboard/members_notifications_widget.dart';
|
||||
|
||||
class MembresDashboardPage extends StatefulWidget {
|
||||
const MembresDashboardPage({super.key});
|
||||
@@ -15,6 +24,8 @@ class MembresDashboardPage extends StatefulWidget {
|
||||
|
||||
class _MembresDashboardPageState extends State<MembresDashboardPage> {
|
||||
late MembresBloc _membresBloc;
|
||||
Map<String, dynamic> _currentFilters = {};
|
||||
String _currentSearchQuery = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -27,6 +38,37 @@ class _MembresDashboardPageState extends State<MembresDashboardPage> {
|
||||
_membresBloc.add(const LoadMembres());
|
||||
}
|
||||
|
||||
void _onFiltersChanged(Map<String, dynamic> filters) {
|
||||
setState(() {
|
||||
_currentFilters = filters;
|
||||
});
|
||||
// TODO: Appliquer les filtres aux données
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void _onSearchChanged(String query) {
|
||||
setState(() {
|
||||
_currentSearchQuery = query;
|
||||
});
|
||||
// TODO: Appliquer la recherche
|
||||
if (query.isNotEmpty) {
|
||||
_membresBloc.add(SearchMembres(query));
|
||||
} else {
|
||||
_loadData();
|
||||
}
|
||||
}
|
||||
|
||||
void _onSuggestionSelected(Map<String, dynamic> suggestion) {
|
||||
switch (suggestion['type']) {
|
||||
case 'quick_filter':
|
||||
_onFiltersChanged(suggestion['filter']);
|
||||
break;
|
||||
case 'member':
|
||||
// TODO: Naviguer vers les détails du membre
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
@@ -117,37 +159,109 @@ class _MembresDashboardPageState extends State<MembresDashboardPage> {
|
||||
}
|
||||
|
||||
Widget _buildDashboard() {
|
||||
return Container(
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.dashboard,
|
||||
size: 64,
|
||||
color: Colors.grey,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
'Dashboard Vide',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'Prêt à être reconstruit pièce par pièce',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Section d'accueil
|
||||
const MembersWelcomeSectionWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Notifications en temps réel
|
||||
const MembersNotificationsWidget(),
|
||||
|
||||
// Recherche intelligente
|
||||
MembersSmartSearchWidget(
|
||||
onSearch: _onSearchChanged,
|
||||
onSuggestionSelected: _onSuggestionSelected,
|
||||
recentSearches: const [], // TODO: Implémenter l'historique
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Filtres avancés
|
||||
MembersAdvancedFiltersWidget(
|
||||
onFiltersChanged: _onFiltersChanged,
|
||||
initialFilters: _currentFilters,
|
||||
),
|
||||
|
||||
// KPI Cards
|
||||
const MembersKPISectionWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Actions rapides
|
||||
const MembersQuickActionsWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Graphiques et analyses
|
||||
const MembersAnalyticsWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Activités récentes
|
||||
const MembersRecentActivitiesWidget(),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Liste des membres améliorée
|
||||
BlocBuilder<MembresBloc, MembresState>(
|
||||
builder: (context, state) {
|
||||
if (state is MembresLoaded) {
|
||||
return MembersEnhancedListWidget(
|
||||
members: state.membres,
|
||||
onMemberTap: (member) {
|
||||
// TODO: Naviguer vers les détails du membre
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Détails de ${member.nomComplet}'),
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
onMemberCall: (member) {
|
||||
// TODO: Appeler le membre
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Appel de ${member.nomComplet}'),
|
||||
backgroundColor: AppTheme.successColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
onMemberMessage: (member) {
|
||||
// TODO: Envoyer un message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Message à ${member.nomComplet}'),
|
||||
backgroundColor: AppTheme.infoColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
onMemberEdit: (member) {
|
||||
// TODO: Modifier le membre
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Modification de ${member.nomComplet}'),
|
||||
backgroundColor: AppTheme.warningColor,
|
||||
),
|
||||
);
|
||||
},
|
||||
searchQuery: _currentSearchQuery,
|
||||
filters: _currentFilters,
|
||||
);
|
||||
} else if (state is MembresLoading) {
|
||||
return MembersEnhancedListWidget(
|
||||
members: const [],
|
||||
onMemberTap: (member) {},
|
||||
isLoading: true,
|
||||
searchQuery: '',
|
||||
filters: const {},
|
||||
);
|
||||
} else {
|
||||
return const Center(
|
||||
child: Text('Erreur lors du chargement des membres'),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user