Files
unionflow-mobile-apps/lib/features/onboarding/presentation/pages/payment_method_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

429 lines
15 KiB
Dart

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';
/// Écran de sélection du moyen de paiement
class PaymentMethodPage extends StatefulWidget {
final SouscriptionStatusModel souscription;
const PaymentMethodPage({super.key, required this.souscription});
@override
State<PaymentMethodPage> createState() => _PaymentMethodPageState();
}
class _PaymentMethodPageState extends State<PaymentMethodPage> {
String? _selected;
static const _methods = [
_PayMethod(
id: 'WAVE',
name: 'Wave Mobile Money',
description: 'Paiement rapide via votre compte Wave',
logoAsset: 'assets/images/payment_methods/wave/logo.png',
color: Color(0xFF00B9F1),
available: true,
badge: 'Recommandé',
),
_PayMethod(
id: 'ORANGE_MONEY',
name: 'Orange Money',
description: 'Paiement via Orange Money',
logoAsset: 'assets/images/payment_methods/orange_money/logo-black.png',
color: Color(0xFFFF6600),
available: false,
badge: 'Prochainement',
),
];
@override
Widget build(BuildContext context) {
final montant = widget.souscription.montantTotal ?? 0;
return Scaffold(
backgroundColor: UnionFlowColors.background,
body: Column(
children: [
// Header
Container(
decoration: const BoxDecoration(
gradient: UnionFlowColors.primaryGradient,
),
child: SafeArea(
bottom: false,
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IconButton(
onPressed: () => Navigator.of(context).maybePop(),
icon: const Icon(Icons.arrow_back_rounded,
color: Colors.white),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
),
const SizedBox(height: 12),
const Text(
'Moyen de paiement',
style: TextStyle(
color: Colors.white,
fontSize: 24,
fontWeight: FontWeight.w800,
),
),
const SizedBox(height: 4),
Text(
'Choisissez comment régler votre souscription',
style: TextStyle(
color: Colors.white.withOpacity(0.8), fontSize: 13),
),
],
),
),
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Montant rappel
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.surface,
borderRadius: BorderRadius.circular(14),
boxShadow: UnionFlowColors.softShadow,
),
child: Row(
children: [
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: UnionFlowColors.goldGradient,
borderRadius: BorderRadius.circular(10),
),
child: const Icon(Icons.receipt_rounded,
color: Colors.white, size: 22),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Montant total',
style: TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 12),
),
Text(
'${_formatPrix(montant)} FCFA',
style: const TextStyle(
color: UnionFlowColors.textPrimary,
fontSize: 20,
fontWeight: FontWeight.w900,
),
),
],
),
),
if (widget.souscription.organisationNom != null)
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text(
'Organisation',
style: TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 11),
),
Text(
widget.souscription.organisationNom!,
style: const TextStyle(
color: UnionFlowColors.textPrimary,
fontSize: 12,
fontWeight: FontWeight.w600),
),
],
),
],
),
),
const SizedBox(height: 24),
const Text(
'Sélectionnez un moyen de paiement',
style: TextStyle(
color: UnionFlowColors.textPrimary,
fontWeight: FontWeight.w700,
fontSize: 15,
),
),
const SizedBox(height: 12),
..._methods.map((m) => _MethodCard(
method: m,
selected: _selected == m.id,
onTap: m.available
? () => setState(() => _selected = m.id)
: null,
)),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: UnionFlowColors.unionGreenPale,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
UnionFlowColors.unionGreen.withOpacity(0.25)),
),
child: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.lock_rounded,
color: UnionFlowColors.unionGreen, size: 18),
SizedBox(width: 10),
Expanded(
child: Text(
'Vos informations de paiement sont sécurisées et ne sont jamais stockées sur nos serveurs. La transaction est traitée directement par Wave.',
style: TextStyle(
fontSize: 12,
color: UnionFlowColors.unionGreen,
height: 1.4),
),
),
],
),
),
],
),
),
),
],
),
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: _selected == 'WAVE'
? () => context
.read<OnboardingBloc>()
.add(const OnboardingPaiementInitie())
: null,
icon: const Icon(Icons.open_in_new_rounded),
label: Text(
_selected == 'WAVE'
? 'Payer avec Wave'
: 'Sélectionnez un moyen de paiement',
style:
const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00B9F1),
disabledBackgroundColor: UnionFlowColors.border,
foregroundColor: Colors.white,
disabledForegroundColor: UnionFlowColors.textSecondary,
padding: const EdgeInsets.symmetric(vertical: 15),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(14)),
elevation: _selected == 'WAVE' ? 3 : 0,
shadowColor: const Color(0xFF00B9F1).withOpacity(0.4),
),
),
),
),
);
}
String _formatPrix(double prix) {
if (prix >= 1000000) return '${(prix / 1000000).toStringAsFixed(1)} M';
final s = prix.toStringAsFixed(0);
if (s.length > 3) {
return '${s.substring(0, s.length - 3)} ${s.substring(s.length - 3)}';
}
return s;
}
}
class _PayMethod {
final String id, name, description, logoAsset;
final Color color;
final bool available;
final String badge;
const _PayMethod({
required this.id,
required this.name,
required this.description,
required this.logoAsset,
required this.color,
required this.available,
required this.badge,
});
}
class _MethodCard extends StatelessWidget {
final _PayMethod method;
final bool selected;
final VoidCallback? onTap;
const _MethodCard({
required this.method,
required this.selected,
this.onTap,
});
@override
Widget build(BuildContext context) {
final disabled = onTap == null;
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.only(bottom: 10),
decoration: BoxDecoration(
color: disabled
? UnionFlowColors.surfaceVariant
: selected
? method.color.withOpacity(0.06)
: UnionFlowColors.surface,
border: Border.all(
color: selected ? method.color : UnionFlowColors.border,
width: selected ? 2 : 1,
),
borderRadius: BorderRadius.circular(14),
boxShadow: disabled
? []
: selected
? [
BoxShadow(
color: method.color.withOpacity(0.15),
blurRadius: 16,
offset: const Offset(0, 6),
)
]
: UnionFlowColors.softShadow,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
// Logo image
Container(
width: 56,
height: 48,
decoration: BoxDecoration(
color: disabled
? UnionFlowColors.border
: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: UnionFlowColors.border),
),
padding: const EdgeInsets.all(6),
child: Image.asset(
method.logoAsset,
fit: BoxFit.contain,
color: disabled ? UnionFlowColors.textTertiary : null,
colorBlendMode: disabled ? BlendMode.srcIn : null,
errorBuilder: (_, __, ___) => Icon(
Icons.account_balance_wallet_rounded,
color: disabled ? UnionFlowColors.textTertiary : method.color,
size: 24,
),
),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
method.name,
style: TextStyle(
fontWeight: FontWeight.w700,
fontSize: 14,
color: disabled
? UnionFlowColors.textTertiary
: UnionFlowColors.textPrimary,
),
),
Text(
method.description,
style: TextStyle(
fontSize: 12,
color: disabled
? UnionFlowColors.textTertiary
: UnionFlowColors.textSecondary,
),
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: disabled
? UnionFlowColors.border
: method.available
? method.color.withOpacity(0.1)
: UnionFlowColors.surfaceVariant,
borderRadius: BorderRadius.circular(20),
),
child: Text(
method.badge,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.w700,
color: disabled
? UnionFlowColors.textTertiary
: method.available
? method.color
: UnionFlowColors.textSecondary,
),
),
),
if (!disabled) ...[
const SizedBox(height: 6),
Icon(
selected
? Icons.check_circle_rounded
: Icons.radio_button_unchecked,
color: selected ? method.color : UnionFlowColors.border,
size: 20,
),
],
],
),
],
),
),
),
);
}
}