From 70cbd1c87353672d391ae8f409b56cd8394f4009 Mon Sep 17 00:00:00 2001
From: dahoud <41957584+DahoudG@users.noreply.github.com>
Date: Tue, 7 Apr 2026 20:56:03 +0000
Subject: [PATCH] =?UTF-8?q?fix(mobile):=20URL=20changement=20mdp=20corrig?=
=?UTF-8?q?=C3=A9e=20+=20v3.0=20=E2=80=94=20multi-org,=20AppAuth,=20s?=
=?UTF-8?q?=C3=A9curit=C3=A9=20prod?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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
---
android/app/build.gradle | 7 +-
android/app/src/main/AndroidManifest.xml | 7 -
.../unionflow_mobile_apps/MainActivity.kt | 17 -
.../main/res/xml/network_security_config.xml | 3 +-
.../gradle/wrapper/gradle-wrapper.properties | 2 +-
android/settings.gradle | 4 +-
l10n.yaml | 1 +
lib/app/app.dart | 6 +-
lib/core/di/injection.dart | 2 +-
lib/core/di/injection_container.dart | 2 +-
lib/core/navigation/adaptive_navigation.dart | 563 -----
.../navigation/main_navigation_layout.dart | 30 +-
lib/core/navigation/more_page.dart | 210 +-
lib/core/network/api_client.dart | 10 +-
lib/core/network/org_context_service.dart | 70 +
lib/core/websocket/websocket_service.dart | 43 +-
.../presentation/pages/adhesions_page.dart | 4 +-
.../keycloak_webview_auth_service.dart | 9 +-
.../presentation/bloc/auth_bloc.dart | 51 +-
.../pages/keycloak_webview_auth_page.dart | 555 +----
.../presentation/pages/login_page.dart | 209 +-
.../presentation/bloc/dashboard_bloc.dart | 29 +-
.../pages/connected_dashboard_page.dart | 760 -------
.../role_dashboards/visitor_dashboard.dart | 537 ++---
.../navigation/dashboard_navigation.dart | 416 ----
lib/features/events/bloc/evenements_bloc.dart | 101 +-
lib/features/members/bloc/membres_bloc.dart | 217 +-
lib/features/members/bloc/membres_event.dart | 87 +
lib/features/members/bloc/membres_state.dart | 72 +
.../data/models/membre_complete_model.dart | 35 +-
.../data/models/membre_complete_model.g.dart | 14 +-
.../repositories/membre_repository_impl.dart | 113 +-
.../repositories/membre_repository.dart | 20 +
.../presentation/pages/members_page.dart | 1981 -----------------
.../pages/members_page_connected.dart | 749 +++++--
.../pages/members_page_wrapper.dart | 144 +-
.../widgets/add_member_dialog.dart | 1595 ++++++++++---
.../onboarding/bloc/onboarding_bloc.dart | 61 +-
.../datasources/souscription_datasource.dart | 27 +-
.../models/souscription_status_model.dart | 10 +
.../pages/awaiting_validation_page.dart | 260 ++-
.../pages/onboarding_flow_page.dart | 176 +-
.../pages/onboarding_shared_widgets.dart | 169 ++
.../pages/payment_method_page.dart | 428 ++++
.../pages/period_selection_page.dart | 502 +++--
.../pages/plan_selection_page.dart | 468 ++--
.../pages/subscription_summary_page.dart | 572 ++++-
.../presentation/pages/wave_payment_page.dart | 422 +++-
.../organizations/bloc/org_switcher_bloc.dart | 144 ++
.../data/models/org_switcher_entry.dart | 73 +
.../presentation/pages/org_selector_page.dart | 384 ++++
.../pages/organizations_page.dart | 12 +-
.../data/repositories/profile_repository.dart | 6 +-
.../presentation/pages/profile_page.dart | 27 +
.../pages/profile_page_wrapper.dart | 15 +-
lib/l10n/app_localizations.dart | 1435 ++++++++++++
lib/l10n/app_localizations_en.dart | 673 ++++++
lib/l10n/app_localizations_fr.dart | 674 ++++++
lib/main.dart | 4 +-
.../theme/app_theme_sophisticated.dart | 6 +-
pubspec.lock | 163 +-
pubspec.yaml | 4 +-
.../members/bloc/membres_bloc_test.dart | 48 +
63 files changed, 9316 insertions(+), 6122 deletions(-)
delete mode 100644 lib/core/navigation/adaptive_navigation.dart
create mode 100644 lib/core/network/org_context_service.dart
delete mode 100644 lib/features/dashboard/presentation/pages/connected_dashboard_page.dart
delete mode 100644 lib/features/dashboard/presentation/widgets/navigation/dashboard_navigation.dart
delete mode 100644 lib/features/members/presentation/pages/members_page.dart
create mode 100644 lib/features/onboarding/presentation/pages/onboarding_shared_widgets.dart
create mode 100644 lib/features/onboarding/presentation/pages/payment_method_page.dart
create mode 100644 lib/features/organizations/bloc/org_switcher_bloc.dart
create mode 100644 lib/features/organizations/data/models/org_switcher_entry.dart
create mode 100644 lib/features/organizations/presentation/pages/org_selector_page.dart
create mode 100644 lib/l10n/app_localizations.dart
create mode 100644 lib/l10n/app_localizations_en.dart
create mode 100644 lib/l10n/app_localizations_fr.dart
diff --git a/android/app/build.gradle b/android/app/build.gradle
index 3b53302..a20812f 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -7,10 +7,11 @@ plugins {
android {
namespace = "dev.lions.unionflow_mobile_apps"
- compileSdk = 35
+ compileSdk = 36
ndkVersion = flutter.ndkVersion
compileOptions {
+ coreLibraryDesugaringEnabled true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
@@ -53,3 +54,7 @@ android {
flutter {
source = "../.."
}
+
+dependencies {
+ coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
+}
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 6b2ad5e..0d9c2e3 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -34,13 +34,6 @@
-
-
-
-
-
-
-
diff --git a/android/app/src/main/kotlin/dev/lions/unionflow_mobile_apps/MainActivity.kt b/android/app/src/main/kotlin/dev/lions/unionflow_mobile_apps/MainActivity.kt
index 2eb5899..1f4ee59 100644
--- a/android/app/src/main/kotlin/dev/lions/unionflow_mobile_apps/MainActivity.kt
+++ b/android/app/src/main/kotlin/dev/lions/unionflow_mobile_apps/MainActivity.kt
@@ -1,29 +1,12 @@
package dev.lions.unionflow_mobile_apps
import android.content.Intent
-import android.os.Bundle
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity() {
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- handleIntent(intent)
- }
-
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
- handleIntent(intent)
- }
-
- private fun handleIntent(intent: Intent?) {
- if (intent?.action == Intent.ACTION_VIEW) {
- val data = intent.data
- if (data != null && data.scheme == "dev.lions.unionflow-mobile") {
- // L'intent sera automatiquement traité par flutter_appauth
- android.util.Log.d("MainActivity", "Deep link reçu: $data")
- }
- }
}
}
diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml
index b7247b4..f9213f7 100644
--- a/android/app/src/main/res/xml/network_security_config.xml
+++ b/android/app/src/main/res/xml/network_security_config.xml
@@ -9,8 +9,7 @@
- 192.168.1.4
- localhost
+ 192.168.1.9
localhost
10.0.2.2
127.0.0.1
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
index 3c85cfe..348c409 100644
--- a/android/gradle/wrapper/gradle-wrapper.properties
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-all.zip
diff --git a/android/settings.gradle b/android/settings.gradle
index b9e43bd..cb7d7dd 100644
--- a/android/settings.gradle
+++ b/android/settings.gradle
@@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
- id "com.android.application" version "8.1.0" apply false
- id "org.jetbrains.kotlin.android" version "1.8.22" apply false
+ id "com.android.application" version "8.7.3" apply false
+ id "org.jetbrains.kotlin.android" version "2.1.0" apply false
}
include ":app"
diff --git a/l10n.yaml b/l10n.yaml
index 1363b25..b8c0e90 100644
--- a/l10n.yaml
+++ b/l10n.yaml
@@ -1,4 +1,5 @@
arb-dir: lib/l10n
template-arb-file: app_fr.arb
output-localization-file: app_localizations.dart
+output-dir: lib/l10n
diff --git a/lib/app/app.dart b/lib/app/app.dart
index b4090bb..eed9bbe 100644
--- a/lib/app/app.dart
+++ b/lib/app/app.dart
@@ -7,9 +7,10 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import '../l10n/app_localizations.dart';
import '../shared/design_system/theme/app_theme_sophisticated.dart';
import '../features/authentication/presentation/bloc/auth_bloc.dart';
+import '../features/organizations/bloc/org_switcher_bloc.dart';
import '../core/l10n/locale_provider.dart';
import '../core/theme/theme_provider.dart';
import '../core/di/injection.dart';
@@ -39,6 +40,9 @@ class UnionFlowApp extends StatelessWidget {
BlocProvider(
create: (context) => getIt()..add(const AuthStatusChecked()),
),
+ BlocProvider(
+ create: (context) => getIt(),
+ ),
],
child: Consumer2(
builder: (context, locale, theme, child) {
diff --git a/lib/core/di/injection.dart b/lib/core/di/injection.dart
index 2a2f0ad..dd0ef7c 100644
--- a/lib/core/di/injection.dart
+++ b/lib/core/di/injection.dart
@@ -10,4 +10,4 @@ final GetIt getIt = GetIt.instance;
preferRelativeImports: true, // default
asExtension: true, // default
)
-void configureDependencies() => getIt.init();
+Future configureDependencies() => getIt.init();
diff --git a/lib/core/di/injection_container.dart b/lib/core/di/injection_container.dart
index 619f451..1fc214d 100644
--- a/lib/core/di/injection_container.dart
+++ b/lib/core/di/injection_container.dart
@@ -10,7 +10,7 @@ final GetIt sl = getIt;
/// Initialise toutes les dépendances de l'application
Future initializeDependencies() async {
- configureDependencies();
+ await configureDependencies();
}
/// Nettoie toutes les dépendances (optionnel, pour les tests)
diff --git a/lib/core/navigation/adaptive_navigation.dart b/lib/core/navigation/adaptive_navigation.dart
deleted file mode 100644
index 1e5ad93..0000000
--- a/lib/core/navigation/adaptive_navigation.dart
+++ /dev/null
@@ -1,563 +0,0 @@
-/// Système de navigation adaptatif basé sur les rôles
-/// Navigation qui s'adapte selon les permissions et rôles utilisateurs
-library adaptive_navigation;
-
-import 'package:flutter/material.dart';
-import '../../shared/design_system/tokens/app_colors.dart';
-import '../../shared/design_system/tokens/color_tokens.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-import '../../features/authentication/presentation/bloc/auth_bloc.dart';
-import '../../features/authentication/data/models/user_role.dart';
-import '../../features/authentication/data/models/permission_matrix.dart';
-import '../../shared/widgets/adaptive_widget.dart';
-
-/// Élément de navigation adaptatif
-class AdaptiveNavigationItem {
- /// Icône de l'élément
- final IconData icon;
-
- /// Icône sélectionnée (optionnelle)
- final IconData? selectedIcon;
-
- /// Libellé de l'élément
- final String label;
-
- /// Route de destination
- final String route;
-
- /// Permissions requises pour afficher cet élément
- final List requiredPermissions;
-
- /// Rôles minimum requis
- final UserRole? minimumRole;
-
- /// Badge de notification (optionnel)
- final String? badge;
-
- /// Couleur personnalisée (optionnelle)
- final Color? color;
-
- const AdaptiveNavigationItem({
- required this.icon,
- this.selectedIcon,
- required this.label,
- required this.route,
- this.requiredPermissions = const [],
- this.minimumRole,
- this.badge,
- this.color,
- });
-}
-
-/// Drawer de navigation adaptatif
-class AdaptiveNavigationDrawer extends StatelessWidget {
- /// Callback de navigation
- final Function(String route) onNavigate;
-
- /// Callback de déconnexion
- final VoidCallback onLogout;
-
- /// Éléments de navigation personnalisés
- final List? customItems;
-
- const AdaptiveNavigationDrawer({
- super.key,
- required this.onNavigate,
- required this.onLogout,
- this.customItems,
- });
-
- @override
- Widget build(BuildContext context) {
- return AdaptiveWidget(
- roleWidgets: {
- UserRole.superAdmin: () => _buildSuperAdminDrawer(context),
- UserRole.orgAdmin: () => _buildOrgAdminDrawer(context),
- UserRole.moderator: () => _buildModeratorDrawer(context),
- UserRole.activeMember: () => _buildActiveMemberDrawer(context),
- UserRole.simpleMember: () => _buildSimpleMemberDrawer(context),
- UserRole.visitor: () => _buildVisitorDrawer(context),
- },
- fallbackWidget: _buildBasicDrawer(context),
- loadingWidget: _buildLoadingDrawer(context),
- );
- }
-
- /// Drawer pour Super Admin
- Widget _buildSuperAdminDrawer(BuildContext context) {
- final items = [
- const AdaptiveNavigationItem(
- icon: Icons.dashboard,
- label: 'Command Center',
- route: '/dashboard',
- requiredPermissions: [PermissionMatrix.SYSTEM_ADMIN],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.business,
- label: 'Organisations',
- route: '/organizations',
- requiredPermissions: [PermissionMatrix.ORG_CREATE],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.people,
- label: 'Utilisateurs Globaux',
- route: '/global-users',
- requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_ALL],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.settings,
- label: 'Administration',
- route: '/system-admin',
- requiredPermissions: [PermissionMatrix.SYSTEM_CONFIG],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.analytics,
- label: 'Analytics',
- route: '/analytics',
- requiredPermissions: [PermissionMatrix.DASHBOARD_ANALYTICS],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.security,
- label: 'Sécurité',
- route: '/security',
- requiredPermissions: [PermissionMatrix.SYSTEM_SECURITY],
- ),
- ];
-
- return _buildDrawer(
- context,
- 'Super Administrateur',
- AppColors.brandGreen,
- Icons.admin_panel_settings,
- items,
- );
- }
-
- /// Drawer pour Org Admin
- Widget _buildOrgAdminDrawer(BuildContext context) {
- final items = [
- const AdaptiveNavigationItem(
- icon: Icons.dashboard,
- label: 'Control Panel',
- route: '/dashboard',
- requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.people,
- label: 'Membres',
- route: '/members',
- requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_ALL],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.account_balance_wallet,
- label: 'Finances',
- route: '/finances',
- requiredPermissions: [PermissionMatrix.FINANCES_VIEW_ALL],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.event,
- label: 'Événements',
- route: '/events',
- requiredPermissions: [PermissionMatrix.EVENTS_VIEW_ALL],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.volunteer_activism,
- label: 'Solidarité',
- route: '/solidarity',
- requiredPermissions: [PermissionMatrix.SOLIDARITY_VIEW_ALL],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.assessment,
- label: 'Rapports',
- route: '/reports',
- requiredPermissions: [PermissionMatrix.REPORTS_GENERATE],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.settings,
- label: 'Configuration',
- route: '/org-settings',
- requiredPermissions: [PermissionMatrix.ORG_CONFIG],
- ),
- ];
-
- return _buildDrawer(
- context,
- 'Administrateur',
- AppColors.primaryGreen,
- Icons.business_center,
- items,
- );
- }
-
- /// Drawer pour Modérateur
- Widget _buildModeratorDrawer(BuildContext context) {
- final items = [
- const AdaptiveNavigationItem(
- icon: Icons.dashboard,
- label: 'Management Hub',
- route: '/dashboard',
- requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.gavel,
- label: 'Modération',
- route: '/moderation',
- requiredPermissions: [PermissionMatrix.MODERATION_CONTENT],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.people,
- label: 'Membres',
- route: '/members',
- requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_ALL],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.event,
- label: 'Événements',
- route: '/events',
- requiredPermissions: [PermissionMatrix.EVENTS_VIEW_ALL],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.message,
- label: 'Communication',
- route: '/communication',
- requiredPermissions: [PermissionMatrix.COMM_MODERATE],
- ),
- ];
-
- return _buildDrawer(
- context,
- 'Modérateur',
- ColorTokens.secondaryDark,
- Icons.manage_accounts,
- items,
- );
- }
-
- /// Drawer pour Membre Actif
- Widget _buildActiveMemberDrawer(BuildContext context) {
- final items = [
- const AdaptiveNavigationItem(
- icon: Icons.dashboard,
- label: 'Activity Center',
- route: '/dashboard',
- requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.person,
- label: 'Mon Profil',
- route: '/profile',
- requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_OWN],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.event,
- label: 'Événements',
- route: '/events',
- requiredPermissions: [PermissionMatrix.EVENTS_VIEW_ALL],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.volunteer_activism,
- label: 'Solidarité',
- route: '/solidarity',
- requiredPermissions: [PermissionMatrix.SOLIDARITY_VIEW_ALL],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.payment,
- label: 'Mes Cotisations',
- route: '/my-finances',
- requiredPermissions: [PermissionMatrix.FINANCES_VIEW_OWN],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.message,
- label: 'Messages',
- route: '/messages',
- requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
- ),
- ];
-
- return _buildDrawer(
- context,
- 'Membre Actif',
- AppColors.brandGreenLight,
- Icons.groups,
- items,
- );
- }
-
- /// Drawer pour Membre Simple
- Widget _buildSimpleMemberDrawer(BuildContext context) {
- final items = [
- const AdaptiveNavigationItem(
- icon: Icons.dashboard,
- label: 'Mon Espace',
- route: '/dashboard',
- requiredPermissions: [PermissionMatrix.DASHBOARD_VIEW],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.person,
- label: 'Mon Profil',
- route: '/profile',
- requiredPermissions: [PermissionMatrix.MEMBERS_VIEW_OWN],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.event,
- label: 'Événements',
- route: '/events',
- requiredPermissions: [PermissionMatrix.EVENTS_VIEW_PUBLIC],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.payment,
- label: 'Mes Cotisations',
- route: '/my-finances',
- requiredPermissions: [PermissionMatrix.FINANCES_VIEW_OWN],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.help,
- label: 'Aide',
- route: '/help',
- requiredPermissions: [],
- ),
- ];
-
- return _buildDrawer(
- context,
- 'Membre',
- ColorTokens.secondary,
- Icons.person,
- items,
- );
- }
-
- /// Drawer pour Visiteur
- Widget _buildVisitorDrawer(BuildContext context) {
- final items = [
- const AdaptiveNavigationItem(
- icon: Icons.home,
- label: 'Accueil',
- route: '/dashboard',
- requiredPermissions: [],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.info,
- label: 'À Propos',
- route: '/about',
- requiredPermissions: [],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.event,
- label: 'Événements Publics',
- route: '/public-events',
- requiredPermissions: [PermissionMatrix.EVENTS_VIEW_PUBLIC],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.contact_mail,
- label: 'Contact',
- route: '/contact',
- requiredPermissions: [],
- ),
- const AdaptiveNavigationItem(
- icon: Icons.login,
- label: 'Se Connecter',
- route: '/login',
- requiredPermissions: [],
- ),
- ];
-
- return _buildDrawer(
- context,
- 'Visiteur',
- AppColors.brandMint,
- Icons.waving_hand,
- items,
- );
- }
-
- /// Drawer basique de fallback
- Widget _buildBasicDrawer(BuildContext context) {
- return _buildDrawer(
- context,
- 'UnionFlow',
- AppColors.textSecondaryLight,
- Icons.dashboard,
- [
- const AdaptiveNavigationItem(
- icon: Icons.home,
- label: 'Accueil',
- route: '/dashboard',
- ),
- ],
- );
- }
-
- /// Drawer de chargement
- Widget _buildLoadingDrawer(BuildContext context) {
- return Drawer(
- child: Container(
- decoration: const BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topCenter,
- end: Alignment.bottomCenter,
- colors: [AppColors.brandGreen, AppColors.primaryGreen],
- ),
- ),
- child: const Center(
- child: CircularProgressIndicator(
- valueColor: AlwaysStoppedAnimation(Colors.white),
- ),
- ),
- ),
- );
- }
-
- /// Construit un drawer avec les éléments spécifiés
- Widget _buildDrawer(
- BuildContext context,
- String title,
- Color color,
- IconData icon,
- List items,
- ) {
- return Drawer(
- child: Column(
- children: [
- // En-tête du drawer
- _buildDrawerHeader(context, title, color, icon),
-
- // Éléments de navigation
- Expanded(
- child: ListView(
- padding: EdgeInsets.zero,
- children: [
- ...items.map((item) => _buildNavigationItem(context, item)),
- const Divider(),
- _buildLogoutItem(context),
- ],
- ),
- ),
- ],
- ),
- );
- }
-
- /// Construit l'en-tête du drawer
- Widget _buildDrawerHeader(
- BuildContext context,
- String title,
- Color color,
- IconData icon,
- ) {
- return DrawerHeader(
- decoration: BoxDecoration(
- gradient: LinearGradient(
- begin: Alignment.topLeft,
- end: Alignment.bottomRight,
- colors: [color, color.withOpacity(0.8)],
- ),
- ),
- child: BlocBuilder(
- builder: (context, state) {
- if (state is AuthAuthenticated) {
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Row(
- children: [
- Icon(icon, color: Colors.white, size: 32),
- const SizedBox(width: 12),
- Text(
- title,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 20,
- fontWeight: FontWeight.bold,
- ),
- ),
- ],
- ),
- const SizedBox(height: 16),
- Text(
- state.user.fullName,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 16,
- ),
- ),
- Text(
- state.user.email,
- style: TextStyle(
- color: Colors.white.withOpacity(0.8),
- fontSize: 14,
- ),
- ),
- ],
- );
- }
-
- return Row(
- children: [
- Icon(icon, color: Colors.white, size: 32),
- const SizedBox(width: 12),
- Text(
- title,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 20,
- fontWeight: FontWeight.bold,
- ),
- ),
- ],
- );
- },
- ),
- );
- }
-
- /// Construit un élément de navigation
- Widget _buildNavigationItem(
- BuildContext context,
- AdaptiveNavigationItem item,
- ) {
- return SecureWidget(
- requiredPermissions: item.requiredPermissions,
- child: ListTile(
- leading: Icon(item.icon, color: item.color),
- title: Text(item.label),
- trailing: item.badge != null
- ? Container(
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
- decoration: BoxDecoration(
- color: AppColors.error,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Text(
- item.badge!,
- style: const TextStyle(
- color: Colors.white,
- fontSize: 12,
- fontWeight: FontWeight.bold,
- ),
- ),
- )
- : null,
- onTap: () {
- Navigator.of(context).pop();
- onNavigate(item.route);
- },
- ),
- );
- }
-
- /// Construit l'élément de déconnexion
- Widget _buildLogoutItem(BuildContext context) {
- return ListTile(
- leading: Icon(Icons.logout, color: AppColors.error),
- title: const Text(
- 'Déconnexion',
- style: TextStyle(color: AppColors.error),
- ),
- onTap: () {
- Navigator.of(context).pop();
- onLogout();
- },
- );
- }
-}
diff --git a/lib/core/navigation/main_navigation_layout.dart b/lib/core/navigation/main_navigation_layout.dart
index 617cad2..f8e13d8 100644
--- a/lib/core/navigation/main_navigation_layout.dart
+++ b/lib/core/navigation/main_navigation_layout.dart
@@ -31,6 +31,10 @@ class _MainNavigationLayoutState extends State {
String? _lastOrgId;
Widget _getDashboardForRole(UserRole role, String userId, String? orgId) {
+ // Compte sans rôle métier — afficher la page d'attente sans appel API
+ if (role == UserRole.visitor) {
+ return const VisitorDashboard();
+ }
if (role == UserRole.orgAdmin && (orgId == null || orgId.isEmpty)) {
return OrgAdminDashboardLoader(userId: userId);
}
@@ -83,8 +87,8 @@ class _MainNavigationLayoutState extends State {
MembersPageWrapper(
organisationId: role == UserRole.orgAdmin ? orgId : null,
),
- const EventsPageWrapper(),
- const MorePage(),
+ if (role != UserRole.visitor) const EventsPageWrapper(),
+ if (role != UserRole.visitor) const MorePage(),
];
return _cachedPages!;
}
@@ -150,16 +154,18 @@ class _MainNavigationLayoutState extends State {
activeIcon: Icons.people_rounded,
label: 'Membres',
),
- const _NavItem(
- icon: Icons.event_outlined,
- activeIcon: Icons.event_rounded,
- label: 'Événements',
- ),
- const _NavItem(
- icon: Icons.more_horiz_rounded,
- activeIcon: Icons.more_horiz_rounded,
- label: 'Plus',
- ),
+ if (role != UserRole.visitor) ...[
+ const _NavItem(
+ icon: Icons.event_outlined,
+ activeIcon: Icons.event_rounded,
+ label: 'Événements',
+ ),
+ const _NavItem(
+ icon: Icons.more_horiz_rounded,
+ activeIcon: Icons.more_horiz_rounded,
+ label: 'Plus',
+ ),
+ ],
];
}
}
diff --git a/lib/core/navigation/more_page.dart b/lib/core/navigation/more_page.dart
index 4c312ca..54e49dd 100644
--- a/lib/core/navigation/more_page.dart
+++ b/lib/core/navigation/more_page.dart
@@ -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();
+
return BlocBuilder(
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 _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 _buildRoleBasedOptions(
+ BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) {
final options = [];
if (state.effectiveRole == UserRole.superAdmin) {
@@ -195,7 +220,115 @@ class MorePage extends StatelessWidget {
return options;
}
- List _buildCommonOptions(BuildContext context) {
+ /// Sections de modules métier — visibles uniquement si le module est actif.
+ List _buildModuleOptions(
+ BuildContext context, AuthAuthenticated state, OrgContextService orgCtx) {
+ final options = [];
+ 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 _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),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/core/network/api_client.dart b/lib/core/network/api_client.dart
index 76ed685..e4a5065 100644
--- a/lib/core/network/api_client.dart
+++ b/lib/core/network/api_client.dart
@@ -10,18 +10,19 @@ import '../error/error_handler.dart';
import '../utils/logger.dart';
import '../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../features/authentication/data/datasources/keycloak_auth_service.dart';
+import 'org_context_service.dart';
/// Client réseau unifié basé sur Dio (Version DRY & Minimaliste).
@lazySingleton
class ApiClient {
late final Dio _dio;
-
+
static const FlutterSecureStorage _storage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock_this_device),
);
- ApiClient() {
+ ApiClient(OrgContextService orgContextService) {
_dio = Dio(
BaseOptions(
baseUrl: AppConfig.apiBaseUrl,
@@ -53,6 +54,11 @@ class ApiClient {
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
+ // Injecter l'organisation active si disponible
+ if (orgContextService.hasContext) {
+ options.headers[OrgContextService.headerName] =
+ orgContextService.activeOrganisationId;
+ }
return handler.next(options);
},
onError: (DioException e, handler) async {
diff --git a/lib/core/network/org_context_service.dart b/lib/core/network/org_context_service.dart
new file mode 100644
index 0000000..5f3f239
--- /dev/null
+++ b/lib/core/network/org_context_service.dart
@@ -0,0 +1,70 @@
+/// Service singleton qui maintient le contexte d'organisation actif.
+///
+/// Injecté dans [ApiClient] pour ajouter automatiquement le header
+/// [X-Active-Organisation-Id] à chaque requête backend.
+library org_context_service;
+
+import 'package:injectable/injectable.dart';
+import '../utils/logger.dart';
+
+const _kHeaderActiveOrg = 'X-Active-Organisation-Id';
+
+@lazySingleton
+class OrgContextService {
+ String? _activeOrganisationId;
+ String? _activeOrganisationNom;
+ String? _activeOrganisationType;
+ Set _modulesActifs = {};
+
+ /// L'UUID de l'organisation active (null si non sélectionnée).
+ String? get activeOrganisationId => _activeOrganisationId;
+
+ /// Le nom lisible de l'organisation active.
+ String? get activeOrganisationNom => _activeOrganisationNom;
+
+ /// Le type de l'organisation active (ex: ASSOCIATION, TONTINE...).
+ String? get activeOrganisationType => _activeOrganisationType;
+
+ /// Modules actifs de l'organisation active (en majuscules).
+ Set get modulesActifs => Set.unmodifiable(_modulesActifs);
+
+ /// Nom du header HTTP utilisé par le backend.
+ static const String headerName = _kHeaderActiveOrg;
+
+ /// Indique si un contexte est disponible.
+ bool get hasContext => _activeOrganisationId != null;
+
+ /// Vérifie si un module spécifique est actif.
+ bool isModuleActif(String module) =>
+ _modulesActifs.contains(module.toUpperCase());
+
+ /// Définit l'organisation active.
+ void setActiveOrganisation({
+ required String organisationId,
+ required String nom,
+ String? type,
+ String? modulesActifsCsv,
+ }) {
+ _activeOrganisationId = organisationId;
+ _activeOrganisationNom = nom;
+ _activeOrganisationType = type;
+ _modulesActifs = _parseModules(modulesActifsCsv);
+ AppLogger.info(
+ 'OrgContextService: organisation active → $nom ($organisationId)'
+ ' | modules: $_modulesActifs');
+ }
+
+ /// Réinitialise le contexte (ex: à la déconnexion).
+ void clear() {
+ _activeOrganisationId = null;
+ _activeOrganisationNom = null;
+ _activeOrganisationType = null;
+ _modulesActifs = {};
+ AppLogger.info('OrgContextService: contexte org effacé');
+ }
+
+ Set _parseModules(String? csv) {
+ if (csv == null || csv.isEmpty) return {};
+ return csv.split(',').map((m) => m.trim().toUpperCase()).toSet();
+ }
+}
diff --git a/lib/core/websocket/websocket_service.dart b/lib/core/websocket/websocket_service.dart
index 3988348..db26673 100644
--- a/lib/core/websocket/websocket_service.dart
+++ b/lib/core/websocket/websocket_service.dart
@@ -25,8 +25,10 @@ abstract class WebSocketEvent {
factory WebSocketEvent.fromJson(Map json) {
final eventType = json['eventType'] as String;
- final timestamp = DateTime.parse(json['timestamp'] as String);
- final data = json['data'] as Map;
+ final timestamp = json['timestamp'] != null
+ ? DateTime.parse(json['timestamp'] as String)
+ : DateTime.now();
+ final data = (json['data'] as Map?) ?? {};
switch (eventType) {
case 'APPROVAL_PENDING':
@@ -242,6 +244,26 @@ class WebSocketService {
return '$baseUrl/ws/dashboard';
}
+ // ─────────────────────────────────────────────────────────────────────────
+ // Helpers de conversion de types (Flutter Web / dart2js compatibility)
+ // ─────────────────────────────────────────────────────────────────────────
+
+ /// Convertit récursivement un objet JSON (potentiellement JS) en types Dart natifs.
+ /// Nécessaire sur Flutter Web où jsonDecode peut retourner des LegacyJavaScriptObject.
+ static dynamic _toDart(dynamic value) {
+ if (value is Map) {
+ return Map.fromEntries(
+ (value as Map).entries.map(
+ (e) => MapEntry(e.key.toString(), _toDart(e.value)),
+ ),
+ );
+ }
+ if (value is List) {
+ return (value as List).map(_toDart).toList();
+ }
+ return value;
+ }
+
/// Gestion des messages reçus
void _onMessage(dynamic message) {
try {
@@ -249,12 +271,25 @@ class WebSocketService {
AppLogger.debug('WebSocket message reçu: $message');
}
- final json = jsonDecode(message as String) as Map;
+ // Sur Flutter Web (web_socket_channel ^3.0.x avec package:web), les messages
+ // text peuvent arriver comme JSString/LegacyJavaScriptObject plutôt que String.
+ // toString() fonctionne pour les strings JS primitifs.
+ final String rawMessage = message is String ? message : message.toString();
+ if (rawMessage.isEmpty) return;
+
+ // Convertir en types Dart natifs pour éviter les LegacyJavaScriptObject imbriqués
+ final dynamic decoded = jsonDecode(rawMessage);
+ if (decoded is! Map) {
+ AppLogger.warning('WebSocket: message ignoré (non-objet): type=${decoded.runtimeType}');
+ return;
+ }
+ final json = _toDart(decoded) as Map;
final type = json['type'] as String?;
// Gérer les messages système
if (type == 'connected') {
- AppLogger.info('🔗 WebSocket: ${json['data']['message']}');
+ final connectedMsg = (json['data'] as Map?)?['message'] ?? 'WebSocket connecté';
+ AppLogger.info('🔗 WebSocket: $connectedMsg');
return;
}
diff --git a/lib/features/adhesions/presentation/pages/adhesions_page.dart b/lib/features/adhesions/presentation/pages/adhesions_page.dart
index 6cacd4b..8f1839e 100644
--- a/lib/features/adhesions/presentation/pages/adhesions_page.dart
+++ b/lib/features/adhesions/presentation/pages/adhesions_page.dart
@@ -80,7 +80,9 @@ class _AdhesionsPageState extends State
action: SnackBarAction(
label: 'Réessayer',
textColor: Colors.white,
- onPressed: () => _loadTab(_tabController.index),
+ onPressed: () {
+ if (mounted) _loadTab(_tabController.index);
+ },
),
),
);
diff --git a/lib/features/authentication/data/datasources/keycloak_webview_auth_service.dart b/lib/features/authentication/data/datasources/keycloak_webview_auth_service.dart
index ed344ad..1c80bcb 100644
--- a/lib/features/authentication/data/datasources/keycloak_webview_auth_service.dart
+++ b/lib/features/authentication/data/datasources/keycloak_webview_auth_service.dart
@@ -126,10 +126,11 @@ class KeycloakWebViewAuthService {
),
);
- // Clés de stockage sécurisé
- static const String _accessTokenKey = 'keycloak_webview_access_token';
- static const String _idTokenKey = 'keycloak_webview_id_token';
- static const String _refreshTokenKey = 'keycloak_webview_refresh_token';
+ // Clés de stockage sécurisé — alignées avec KeycloakAuthService pour éviter IC-03
+ // KeycloakAuthService lit 'kc_access' / 'kc_refresh' / 'kc_id' ; ApiClient aussi.
+ static const String _accessTokenKey = 'kc_access';
+ static const String _idTokenKey = 'kc_id';
+ static const String _refreshTokenKey = 'kc_refresh';
static const String _userInfoKey = 'keycloak_webview_user_info';
static const String _authStateKey = 'keycloak_webview_auth_state';
diff --git a/lib/features/authentication/presentation/bloc/auth_bloc.dart b/lib/features/authentication/presentation/bloc/auth_bloc.dart
index 907a2fb..9c4dc65 100644
--- a/lib/features/authentication/presentation/bloc/auth_bloc.dart
+++ b/lib/features/authentication/presentation/bloc/auth_bloc.dart
@@ -6,6 +6,7 @@ import '../../data/models/user_role.dart';
import '../../data/datasources/keycloak_auth_service.dart';
import '../../data/datasources/permission_engine.dart';
import '../../../../core/config/environment.dart';
+import '../../../../core/network/org_context_service.dart';
import '../../../../core/storage/dashboard_cache_manager.dart';
import '../../../../core/utils/logger.dart';
import '../../../../core/di/injection.dart';
@@ -87,16 +88,32 @@ class AuthPendingOnboarding extends AuthState {
List