Files
unionflow-server-impl-quarkus/unionflow-mobile-apps/lib/features/members/presentation/widgets/membre_enhanced_card.dart
DahoudG f89f6167cc 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
2025-09-15 01:44:16 +00:00

391 lines
11 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../../../../core/models/membre_model.dart';
import '../../../../shared/theme/app_theme.dart';
/// Carte membre améliorée avec différents modes d'affichage
class MembreEnhancedCard extends StatelessWidget {
final MembreModel membre;
final String viewMode;
final VoidCallback? onTap;
final VoidCallback? onEdit;
final VoidCallback? onDelete;
final VoidCallback? onCall;
final VoidCallback? onMessage;
const MembreEnhancedCard({
super.key,
required this.membre,
this.viewMode = 'card',
this.onTap,
this.onEdit,
this.onDelete,
this.onCall,
this.onMessage,
});
@override
Widget build(BuildContext context) {
switch (viewMode) {
case 'list':
return _buildListView();
case 'grid':
return _buildGridView();
case 'card':
default:
return _buildCardView();
}
}
Widget _buildCardView() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// En-tête avec avatar et actions
Row(
children: [
_buildAvatar(size: 50),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
membre.nomComplet,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
const SizedBox(height: 4),
Text(
membre.numeroMembre,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
),
_buildStatusBadge(),
],
),
const SizedBox(height: 12),
// Informations de contact
_buildContactInfo(),
const SizedBox(height: 12),
// Actions
_buildActionButtons(),
],
),
),
),
);
}
Widget _buildListView() {
return Card(
elevation: 1,
margin: const EdgeInsets.symmetric(vertical: 4),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: ListTile(
onTap: onTap,
leading: _buildAvatar(size: 40),
title: Text(
membre.nomComplet,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
membre.numeroMembre,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
),
const SizedBox(height: 2),
Text(
membre.telephone,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textHint,
),
),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildStatusBadge(),
const SizedBox(width: 8),
PopupMenuButton<String>(
onSelected: _handleMenuAction,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'call',
child: Row(
children: [
Icon(Icons.phone, size: 16),
SizedBox(width: 8),
Text('Appeler'),
],
),
),
const PopupMenuItem(
value: 'message',
child: Row(
children: [
Icon(Icons.message, size: 16),
SizedBox(width: 8),
Text('Message'),
],
),
),
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 16),
SizedBox(width: 8),
Text('Modifier'),
],
),
),
],
),
],
),
),
);
}
Widget _buildGridView() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
children: [
_buildAvatar(size: 60),
const SizedBox(height: 8),
Text(
membre.prenom,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.textPrimary,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
Text(
membre.nom,
style: const TextStyle(
fontSize: 12,
color: AppTheme.textSecondary,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
_buildStatusBadge(),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildGridAction(Icons.phone, onCall),
_buildGridAction(Icons.message, onMessage),
_buildGridAction(Icons.edit, onEdit),
],
),
],
),
),
),
);
}
Widget _buildAvatar({required double size}) {
return CircleAvatar(
radius: size / 2,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
child: Text(
membre.initiales,
style: TextStyle(
fontSize: size * 0.4,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
);
}
Widget _buildStatusBadge() {
Color color;
switch (membre.statut.toUpperCase()) {
case 'ACTIF':
color = AppTheme.successColor;
break;
case 'INACTIF':
color = AppTheme.warningColor;
break;
case 'SUSPENDU':
color = AppTheme.errorColor;
break;
default:
color = AppTheme.textSecondary;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3)),
),
child: Text(
membre.statutLibelle,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: color,
),
),
);
}
Widget _buildContactInfo() {
return Column(
children: [
Row(
children: [
const Icon(Icons.phone, size: 16, color: AppTheme.textHint),
const SizedBox(width: 8),
Text(
membre.telephone,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.email, size: 16, color: AppTheme.textHint),
const SizedBox(width: 8),
Expanded(
child: Text(
membre.email,
style: const TextStyle(
fontSize: 14,
color: AppTheme.textSecondary,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
],
);
}
Widget _buildActionButtons() {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: onCall,
icon: const Icon(Icons.phone, size: 16),
label: const Text('Appeler'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.primaryColor,
side: BorderSide(color: AppTheme.primaryColor.withOpacity(0.3)),
),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: onMessage,
icon: const Icon(Icons.message, size: 16),
label: const Text('Message'),
style: OutlinedButton.styleFrom(
foregroundColor: AppTheme.secondaryColor,
side: BorderSide(color: AppTheme.secondaryColor.withOpacity(0.3)),
),
),
),
],
);
}
Widget _buildGridAction(IconData icon, VoidCallback? onPressed) {
return GestureDetector(
onTap: () {
HapticFeedback.lightImpact();
onPressed?.call();
},
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 16,
color: AppTheme.primaryColor,
),
),
);
}
void _handleMenuAction(String action) {
HapticFeedback.lightImpact();
switch (action) {
case 'call':
onCall?.call();
break;
case 'message':
onMessage?.call();
break;
case 'edit':
onEdit?.call();
break;
}
}
}