Files
unionflow-mobile-apps/lib/features/onboarding/presentation/pages/subscription_summary_page.dart
dahoud 70cbd1c873 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
2026-04-07 20:56:03 +00:00

523 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/onboarding_bloc.dart';
import '../../data/models/souscription_status_model.dart';
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
/// Étape 3 — Récapitulatif détaillé avant paiement
class SubscriptionSummaryPage extends StatelessWidget {
final SouscriptionStatusModel souscription;
const SubscriptionSummaryPage({super.key, required this.souscription});
static const _periodeLabels = {
'MENSUEL': 'Mensuel',
'TRIMESTRIEL': 'Trimestriel',
'SEMESTRIEL': 'Semestriel',
'ANNUEL': 'Annuel',
};
static const _periodeRemises = {
'MENSUEL': null,
'TRIMESTRIEL': '5% de remise',
'SEMESTRIEL': '10% de remise',
'ANNUEL': '20% de remise',
};
static const _orgLabels = {
'ASSOCIATION': 'Association / ONG locale',
'MUTUELLE': 'Mutuelle (santé, fonctionnaires…)',
'COOPERATIVE': 'Coopérative / Microfinance',
'FEDERATION': 'Fédération / Grande ONG',
};
static const _plageLabels = {
'PETITE': '1100 membres',
'MOYENNE': '101500 membres',
'GRANDE': '5012 000 membres',
'TRES_GRANDE': '2 000+ membres',
};
@override
Widget build(BuildContext context) {
final montant = souscription.montantTotal ?? 0;
final remise = _periodeRemises[souscription.typePeriode];
return Scaffold(
backgroundColor: UnionFlowColors.background,
body: Column(
children: [
// Header hero
Container(
decoration: const BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
),
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 32),
child: Column(
children: [
// Step bar
Row(
children: List.generate(3, (i) => Expanded(
child: Container(
height: 4,
margin: EdgeInsets.only(right: i < 2 ? 6 : 0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(2),
),
),
)),
),
const SizedBox(height: 6),
Align(
alignment: Alignment.centerLeft,
child: Text(
'Étape 3 sur 3',
style: TextStyle(
color: Colors.white.withOpacity(0.75),
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: 20),
// Montant principal
Container(
width: 90,
height: 90,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withOpacity(0.4), width: 2),
),
child: const Icon(Icons.receipt_long_rounded,
color: Colors.white, size: 44),
),
const SizedBox(height: 14),
Text(
_formatPrix(montant),
style: const TextStyle(
color: Colors.white,
fontSize: 40,
fontWeight: FontWeight.w900,
letterSpacing: -1,
),
),
const Text(
'FCFA à régler',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
fontWeight: FontWeight.w500),
),
if (remise != null) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: UnionFlowColors.gold.withOpacity(0.3),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: UnionFlowColors.goldLight.withOpacity(0.5)),
),
child: Text(
remise,
style: const TextStyle(
color: UnionFlowColors.goldLight,
fontSize: 12,
fontWeight: FontWeight.w700,
),
),
),
],
],
),
),
),
),
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 20, 20, 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Organisation
if (souscription.organisationNom != null) ...[
_DetailCard(
title: 'Organisation',
icon: Icons.business_rounded,
iconColor: UnionFlowColors.indigo,
items: [
_DetailItem(
label: 'Nom',
value: souscription.organisationNom!,
bold: true),
_DetailItem(
label: 'Type',
value: _orgLabels[souscription.typeOrganisation] ??
souscription.typeOrganisation),
],
),
const SizedBox(height: 14),
],
// Formule
_DetailCard(
title: 'Formule souscrite',
icon: Icons.workspace_premium_rounded,
iconColor: UnionFlowColors.gold,
items: [
_DetailItem(
label: 'Niveau',
value: souscription.typeFormule,
bold: true),
_DetailItem(
label: 'Taille',
value: _plageLabels[souscription.plageMembres] ??
souscription.plageLibelle),
if (souscription.montantMensuelBase != null)
_DetailItem(
label: 'Prix de base',
value:
'${_formatPrix(souscription.montantMensuelBase!)} FCFA/mois'),
],
),
const SizedBox(height: 14),
// Facturation
_DetailCard(
title: 'Facturation',
icon: Icons.calendar_today_rounded,
iconColor: UnionFlowColors.unionGreen,
items: [
_DetailItem(
label: 'Période',
value:
_periodeLabels[souscription.typePeriode] ??
souscription.typePeriode),
if (souscription.coefficientApplique != null)
_DetailItem(
label: 'Coefficient',
value:
'×${souscription.coefficientApplique!.toStringAsFixed(4)}'),
if (souscription.dateDebut != null &&
souscription.dateFin != null) ...[
_DetailItem(
label: 'Début',
value: _formatDate(souscription.dateDebut!)),
_DetailItem(
label: 'Fin',
value: _formatDate(souscription.dateFin!)),
],
],
),
const SizedBox(height: 14),
// Montant total
Container(
padding: const EdgeInsets.all(18),
decoration: BoxDecoration(
color: UnionFlowColors.goldPale,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: UnionFlowColors.gold.withOpacity(0.4)),
boxShadow: UnionFlowColors.goldGlowShadow,
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: UnionFlowColors.goldGradient,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.monetization_on_rounded,
color: Colors.white, size: 26),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Total à payer',
style: TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 13),
),
Text(
'${_formatPrix(montant)} FCFA',
style: const TextStyle(
color: UnionFlowColors.textPrimary,
fontSize: 22,
fontWeight: FontWeight.w900,
),
),
],
),
),
],
),
),
const SizedBox(height: 20),
// Notes importantes
_NoteBox(
icon: Icons.security_rounded,
iconColor: UnionFlowColors.unionGreen,
backgroundColor: UnionFlowColors.unionGreenPale,
borderColor: UnionFlowColors.unionGreen.withOpacity(0.25),
title: 'Paiement sécurisé',
message:
'Votre paiement est traité de manière sécurisée via Wave Mobile Money. Une fois le paiement effectué, votre compte sera activé automatiquement.',
),
const SizedBox(height: 10),
_NoteBox(
icon: Icons.bolt_rounded,
iconColor: UnionFlowColors.amber,
backgroundColor: const Color(0xFFFFFBF0),
borderColor: UnionFlowColors.amber.withOpacity(0.3),
title: 'Activation immédiate',
message:
'Dès que le paiement est confirmé par Wave, votre compte d\'administrateur est activé et vous pouvez accéder à toutes les fonctionnalités de votre formule.',
),
const SizedBox(height: 10),
_NoteBox(
icon: Icons.support_agent_rounded,
iconColor: UnionFlowColors.info,
backgroundColor: UnionFlowColors.infoPale,
borderColor: UnionFlowColors.info.withOpacity(0.2),
title: 'Besoin d\'aide ?',
message:
'En cas de problème lors du paiement, contactez notre support à support@unionflow.app — nous vous répondrons sous 24h.',
),
],
),
),
),
],
),
bottomNavigationBar: Container(
padding: EdgeInsets.fromLTRB(
20, 12, 20, MediaQuery.of(context).padding.bottom + 12),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 12,
offset: const Offset(0, -4),
),
],
),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () => context
.read<OnboardingBloc>()
.add(const OnboardingChoixPaiementOuvert()),
icon: const Icon(Icons.payment_rounded),
label: const Text(
'Choisir le moyen de paiement',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
shadowColor: UnionFlowColors.unionGreen.withOpacity(0.4),
elevation: 3,
),
),
),
),
);
}
String _formatPrix(double prix) {
if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M';
final s = prix.toStringAsFixed(0);
if (s.length > 6) {
return '${s.substring(0, s.length - 6)} ${s.substring(s.length - 6, s.length - 3)} ${s.substring(s.length - 3)}';
}
if (s.length > 3) {
return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}';
}
return s;
}
String _formatDate(DateTime date) {
return '${date.day.toString().padLeft(2, '0')}/${date.month.toString().padLeft(2, '0')}/${date.year}';
}
}
// ─── Widgets locaux ──────────────────────────────────────────────────────────
class _DetailItem {
final String label;
final String value;
final bool bold;
const _DetailItem(
{required this.label, required this.value, this.bold = false});
}
class _DetailCard extends StatelessWidget {
final String title;
final IconData icon;
final Color iconColor;
final List<_DetailItem> items;
const _DetailCard({
required this.title,
required this.icon,
required this.iconColor,
required this.items,
});
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(16),
boxShadow: UnionFlowColors.softShadow,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 10),
child: Row(
children: [
Container(
width: 34,
height: 34,
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: iconColor, size: 18),
),
const SizedBox(width: 10),
Text(
title,
style: const TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14,
color: UnionFlowColors.textPrimary,
),
),
],
),
),
const Divider(height: 1, color: UnionFlowColors.border),
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 14),
child: Column(
children: items.map((item) => Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
item.label,
style: const TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 13),
),
),
Expanded(
child: Text(
item.value,
style: TextStyle(
color: UnionFlowColors.textPrimary,
fontSize: 13,
fontWeight: item.bold
? FontWeight.w700
: FontWeight.w500,
),
),
),
],
),
)).toList(),
),
),
],
),
);
}
}
class _NoteBox extends StatelessWidget {
final IconData icon;
final Color iconColor;
final Color backgroundColor;
final Color borderColor;
final String title;
final String message;
const _NoteBox({
required this.icon,
required this.iconColor,
required this.backgroundColor,
required this.borderColor,
required this.title,
required this.message,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: borderColor),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: iconColor, size: 20),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
color: iconColor,
fontWeight: FontWeight.w700,
fontSize: 13,
),
),
const SizedBox(height: 3),
Text(
message,
style: const TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 12,
height: 1.5),
),
],
),
),
],
),
);
}
}