fix(mobile): URL changement mdp corrigée + v3.0 — multi-org, AppAuth, sécurité prod

Auth:
- profile_repository.dart: /api/auth/change-password → /api/membres/auth/change-password

Multi-org (Phase 3):
- OrgSelectorPage, OrgSwitcherBloc, OrgSwitcherEntry
- org_context_service.dart: headers X-Active-Organisation-Id + X-Active-Role

Navigation:
- MorePage: navigation conditionnelle par typeOrganisation
- Suppression adaptive_navigation (remplacé par main_navigation_layout)

Auth AppAuth:
- keycloak_webview_auth_service: fixes AppAuth Android
- AuthBloc: gestion REAUTH_REQUIS + premierLoginComplet

Onboarding:
- Nouveaux états: payment_method_page, onboarding_shared_widgets
- SouscriptionStatusModel mis à jour StatutValidationSouscription

Android:
- build.gradle: ProGuard/R8, network_security_config
- Gradle wrapper mis à jour
This commit is contained in:
dahoud
2026-04-07 20:56:03 +00:00
parent 22f9c7e9a1
commit 70cbd1c873
63 changed files with 9316 additions and 6122 deletions

View File

@@ -5,6 +5,8 @@ import '../../features/authentication/data/models/user_role.dart';
import '../../shared/design_system/unionflow_design_system.dart';
import '../../shared/widgets/core_card.dart';
import '../../shared/widgets/mini_avatar.dart';
import '../di/injection_container.dart';
import '../network/org_context_service.dart';
import '../../features/admin/presentation/pages/user_management_page.dart';
import '../../features/settings/presentation/pages/system_settings_page.dart';
@@ -12,18 +14,22 @@ import '../../features/backup/presentation/pages/backup_page.dart';
import '../../features/logs/presentation/pages/logs_page.dart';
import '../../features/reports/presentation/pages/reports_page_wrapper.dart';
import '../../features/epargne/presentation/pages/epargne_page.dart';
import '../../features/contributions/presentation/pages/contributions_page_wrapper.dart';
import '../../features/contributions/presentation/pages/contributions_page_wrapper.dart' show CotisationsPageWrapper;
import '../../features/adhesions/presentation/pages/adhesions_page_wrapper.dart';
import '../../features/solidarity/presentation/pages/demandes_aide_page_wrapper.dart';
import '../../features/organizations/presentation/pages/organizations_page_wrapper.dart';
import '../../features/organizations/presentation/pages/org_selector_page.dart';
import '../../features/organizations/bloc/org_switcher_bloc.dart';
import '../../features/profile/presentation/pages/profile_page_wrapper.dart';
/// Page "Plus" avec les fonctions avancées selon le rôle
/// Page "Plus" avec les fonctions avancées selon le rôle et les modules actifs.
class MorePage extends StatelessWidget {
const MorePage({super.key});
@override
Widget build(BuildContext context) {
final orgCtx = sl<OrgContextService>();
return BlocBuilder<AuthBloc, AuthState>(
builder: (context, state) {
if (state is! AuthAuthenticated) {
@@ -43,11 +49,12 @@ class MorePage extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildUserProfile(context, state),
_buildUserProfile(context, state, orgCtx),
const SizedBox(height: SpacingTokens.md),
..._buildRoleBasedOptions(context, state),
..._buildRoleBasedOptions(context, state, orgCtx),
..._buildModuleOptions(context, state, orgCtx),
const SizedBox(height: SpacingTokens.md),
..._buildCommonOptions(context),
..._buildCommonOptions(context, orgCtx),
],
),
),
@@ -56,7 +63,8 @@ class MorePage extends StatelessWidget {
);
}
Widget _buildUserProfile(BuildContext context, AuthAuthenticated state) {
Widget _buildUserProfile(
BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final textColor = isDark ? AppColors.textPrimaryDark : AppColors.textPrimaryLight;
final roleColor = isDark ? AppColors.brandGreenLight : AppColors.primaryGreen;
@@ -90,6 +98,10 @@ class MorePage extends StatelessWidget {
fontWeight: FontWeight.bold,
),
),
if (orgCtx.hasContext) ...[
const SizedBox(height: 4),
_OrgBadge(orgCtx: orgCtx, onTap: () => _openOrgSelector(context)),
],
],
),
),
@@ -101,7 +113,20 @@ class MorePage extends StatelessWidget {
);
}
List<Widget> _buildRoleBasedOptions(BuildContext context, AuthAuthenticated state) {
void _openOrgSelector(BuildContext context) {
// Vérifier que OrgSwitcherBloc est disponible (fourni par un ancêtre)
try {
showOrgSelector(context);
} catch (_) {
// OrgSwitcherBloc pas fourni dans ce contexte, navigation vers ProfilePage
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ProfilePageWrapper()),
);
}
}
List<Widget> _buildRoleBasedOptions(
BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) {
final options = <Widget>[];
if (state.effectiveRole == UserRole.superAdmin) {
@@ -195,7 +220,115 @@ class MorePage extends StatelessWidget {
return options;
}
List<Widget> _buildCommonOptions(BuildContext context) {
/// Sections de modules métier — visibles uniquement si le module est actif.
List<Widget> _buildModuleOptions(
BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) {
final options = <Widget>[];
final isAdmin = state.effectiveRole == UserRole.orgAdmin ||
state.effectiveRole == UserRole.superAdmin;
// Module TONTINE
if (orgCtx.isModuleActif('TONTINE')) {
options.add(_buildSectionTitle(context, 'Tontine'));
if (isAdmin) {
options.add(_buildOptionTile(context,
icon: Icons.autorenew,
title: 'Gestion Tontine',
subtitle: 'Cycles, cotisations et remises',
onTap: () => Navigator.pushNamed(context, '/tontine'),
));
} else {
options.add(_buildOptionTile(context,
icon: Icons.autorenew,
title: 'Ma Tontine',
subtitle: 'Mes cycles et cotisations',
onTap: () => Navigator.pushNamed(context, '/tontine'),
));
}
}
// Module EPARGNE
if (orgCtx.isModuleActif('EPARGNE')) {
options.add(_buildSectionTitle(context, 'Épargne'));
options.add(_buildOptionTile(context,
icon: Icons.savings,
title: isAdmin ? 'Gestion Épargne' : 'Mon Épargne',
subtitle: isAdmin ? 'Comptes épargne et transactions' : 'Mon compte épargne',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EpargnePage())),
));
}
// Module CREDIT
if (orgCtx.isModuleActif('CREDIT')) {
options.add(_buildSectionTitle(context, 'Crédit'));
options.add(_buildOptionTile(context,
icon: Icons.account_balance,
title: isAdmin ? 'Gestion Crédit' : 'Mon Crédit',
subtitle: isAdmin ? 'Demandes et suivi des crédits' : 'Mes demandes de crédit',
onTap: () => Navigator.pushNamed(context, '/credit'),
));
}
// Module AGRICULTURE
if (orgCtx.isModuleActif('AGRICULTURE')) {
options.add(_buildSectionTitle(context, 'Agriculture'));
options.add(_buildOptionTile(context,
icon: Icons.eco,
title: 'Campagnes Agricoles',
subtitle: 'Parcelles, récoltes et stocks',
onTap: () => Navigator.pushNamed(context, '/agricole'),
));
}
// Module COLLECTE_FONDS
if (orgCtx.isModuleActif('COLLECTE_FONDS')) {
options.add(_buildSectionTitle(context, 'Collecte de Fonds'));
options.add(_buildOptionTile(context,
icon: Icons.volunteer_activism,
title: 'Campagnes de Collecte',
subtitle: 'Dons et levées de fonds',
onTap: () => Navigator.pushNamed(context, '/collecte'),
));
}
// Module PROJETS_ONG
if (orgCtx.isModuleActif('PROJETS_ONG')) {
options.add(_buildSectionTitle(context, 'Projets ONG'));
options.add(_buildOptionTile(context,
icon: Icons.public,
title: 'Projets',
subtitle: 'Gérer et suivre les projets',
onTap: () => Navigator.pushNamed(context, '/projets-ong'),
));
}
// Module CULTE_DONS
if (orgCtx.isModuleActif('CULTE_DONS')) {
options.add(_buildSectionTitle(context, 'Culte & Dons'));
options.add(_buildOptionTile(context,
icon: Icons.church,
title: 'Dons et Offrandes',
subtitle: 'Gestion des dons religieux',
onTap: () => Navigator.pushNamed(context, '/culte'),
));
}
// Module VOTES
if (orgCtx.isModuleActif('VOTES')) {
options.add(_buildSectionTitle(context, 'Votes'));
options.add(_buildOptionTile(context,
icon: Icons.how_to_vote,
title: 'Votes & Élections',
subtitle: 'Campagnes et résultats',
onTap: () => Navigator.pushNamed(context, '/votes'),
));
}
return options;
}
List<Widget> _buildCommonOptions(BuildContext context, OrgContextService orgCtx) {
return [
_buildSectionTitle(context, 'Général'),
_buildOptionTile(context,
@@ -219,17 +352,20 @@ class MorePage extends StatelessWidget {
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const DemandesAidePageWrapper())),
),
_buildOptionTile(context,
icon: Icons.savings_outlined,
title: 'Comptes épargne',
subtitle: 'Mutuelle épargne dépôts (LCB-FT)',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EpargnePage())),
),
// Épargne — affiché en commun uniquement si le module n'est PAS actif (évite doublon avec section module)
if (!orgCtx.isModuleActif('EPARGNE'))
_buildOptionTile(context,
icon: Icons.savings_outlined,
title: 'Comptes épargne',
subtitle: 'Mutuelle épargne dépôts (LCB-FT)',
onTap: () => Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const EpargnePage())),
),
];
}
Widget _buildSectionTitle(BuildContext context, String title) {
final isDark = Theme.of(context).brightness == Brightness.dark;
return Padding(
padding: const EdgeInsets.only(top: 16, bottom: 6, left: 4),
@@ -252,6 +388,7 @@ class MorePage extends StatelessWidget {
required VoidCallback onTap,
Color? accentColor,
}) {
final isDark = Theme.of(context).brightness == Brightness.dark;
final accent = accentColor ?? AppColors.primaryGreen;
final titleColor = accentColor != null
@@ -297,3 +434,46 @@ class MorePage extends StatelessWidget {
);
}
}
/// Badge compact affichant l'organisation active avec bouton de changement.
class _OrgBadge extends StatelessWidget {
final OrgContextService orgCtx;
final VoidCallback onTap;
const _OrgBadge({required this.orgCtx, required this.onTap});
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: colorScheme.primaryContainer.withValues(alpha: 0.4),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.business, size: 12, color: colorScheme.primary),
const SizedBox(width: 4),
Flexible(
child: Text(
orgCtx.activeOrganisationNom ?? '',
style: TextStyle(
fontSize: 11,
color: colorScheme.primary,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 2),
Icon(Icons.swap_horiz, size: 12, color: colorScheme.primary),
],
),
),
);
}
}