Files
unionflow-server-impl-quarkus/unionflow-mobile-apps/lib/core/models/membre_model.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

213 lines
5.1 KiB
Dart

import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
part 'membre_model.g.dart';
/// Modèle de données pour un membre UnionFlow
/// Aligné avec MembreDTO du serveur API
@JsonSerializable()
class MembreModel extends Equatable {
/// ID unique du membre
final String? id;
/// Numéro unique du membre (format: UF-YYYY-XXXXXXXX)
@JsonKey(name: 'numeroMembre')
final String numeroMembre;
/// Nom de famille du membre
final String nom;
/// Prénom du membre
final String prenom;
/// Adresse email
final String email;
/// Numéro de téléphone
final String telephone;
/// Date de naissance
@JsonKey(name: 'dateNaissance')
final DateTime? dateNaissance;
/// Adresse complète
final String? adresse;
/// Ville
final String? ville;
/// Code postal
@JsonKey(name: 'codePostal')
final String? codePostal;
/// Pays
final String? pays;
/// Profession
final String? profession;
/// Statut du membre (ACTIF, INACTIF, SUSPENDU)
final String statut;
/// Date d'adhésion
@JsonKey(name: 'dateAdhesion')
final DateTime dateAdhesion;
/// Date de création
@JsonKey(name: 'dateCreation')
final DateTime dateCreation;
/// Date de dernière modification
@JsonKey(name: 'dateModification')
final DateTime? dateModification;
/// Indique si le membre est actif
final bool actif;
/// Version pour optimistic locking
final int version;
const MembreModel({
this.id,
required this.numeroMembre,
required this.nom,
required this.prenom,
required this.email,
required this.telephone,
this.dateNaissance,
this.adresse,
this.ville,
this.codePostal,
this.pays,
this.profession,
required this.statut,
required this.dateAdhesion,
required this.dateCreation,
this.dateModification,
required this.actif,
required this.version,
});
/// Constructeur depuis JSON
factory MembreModel.fromJson(Map<String, dynamic> json) =>
_$MembreModelFromJson(json);
/// Conversion vers JSON
Map<String, dynamic> toJson() => _$MembreModelToJson(this);
/// Nom complet du membre
String get nomComplet => '$prenom $nom';
/// Initiales du membre
String get initiales {
final prenomInitial = prenom.isNotEmpty ? prenom[0].toUpperCase() : '';
final nomInitial = nom.isNotEmpty ? nom[0].toUpperCase() : '';
return '$prenomInitial$nomInitial';
}
/// Adresse complète formatée
String get adresseComplete {
final parts = <String>[];
if (adresse?.isNotEmpty == true) parts.add(adresse!);
if (ville?.isNotEmpty == true) parts.add(ville!);
if (codePostal?.isNotEmpty == true) parts.add(codePostal!);
if (pays?.isNotEmpty == true) parts.add(pays!);
return parts.join(', ');
}
/// Libellé du statut formaté
String get statutLibelle {
switch (statut.toUpperCase()) {
case 'ACTIF':
return 'Actif';
case 'INACTIF':
return 'Inactif';
case 'SUSPENDU':
return 'Suspendu';
default:
return statut;
}
}
/// Âge calculé à partir de la date de naissance
int get age {
if (dateNaissance == null) return 0;
final now = DateTime.now();
int age = now.year - dateNaissance!.year;
if (now.month < dateNaissance!.month ||
(now.month == dateNaissance!.month && now.day < dateNaissance!.day)) {
age--;
}
return age;
}
/// Copie avec modifications
MembreModel copyWith({
String? id,
String? numeroMembre,
String? nom,
String? prenom,
String? email,
String? telephone,
DateTime? dateNaissance,
String? adresse,
String? ville,
String? codePostal,
String? pays,
String? profession,
String? statut,
DateTime? dateAdhesion,
DateTime? dateCreation,
DateTime? dateModification,
bool? actif,
int? version,
}) {
return MembreModel(
id: id ?? this.id,
numeroMembre: numeroMembre ?? this.numeroMembre,
nom: nom ?? this.nom,
prenom: prenom ?? this.prenom,
email: email ?? this.email,
telephone: telephone ?? this.telephone,
dateNaissance: dateNaissance ?? this.dateNaissance,
adresse: adresse ?? this.adresse,
ville: ville ?? this.ville,
codePostal: codePostal ?? this.codePostal,
pays: pays ?? this.pays,
profession: profession ?? this.profession,
statut: statut ?? this.statut,
dateAdhesion: dateAdhesion ?? this.dateAdhesion,
dateCreation: dateCreation ?? this.dateCreation,
dateModification: dateModification ?? this.dateModification,
actif: actif ?? this.actif,
version: version ?? this.version,
);
}
@override
List<Object?> get props => [
id,
numeroMembre,
nom,
prenom,
email,
telephone,
dateNaissance,
adresse,
ville,
codePostal,
pays,
profession,
statut,
dateAdhesion,
dateCreation,
dateModification,
actif,
version,
];
@override
String toString() => 'MembreModel(id: $id, numeroMembre: $numeroMembre, '
'nomComplet: $nomComplet, email: $email, statut: $statut)';
}