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

274 lines
9.5 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../bloc/onboarding_bloc.dart';
import '../../../../core/di/injection.dart';
import '../../../../features/authentication/presentation/bloc/auth_bloc.dart';
import '../../../../shared/design_system/tokens/unionflow_colors.dart';
import 'plan_selection_page.dart';
import 'period_selection_page.dart';
import 'subscription_summary_page.dart';
import 'payment_method_page.dart';
import 'wave_payment_page.dart';
import 'awaiting_validation_page.dart';
/// Page conteneur du workflow d'onboarding.
/// Reçoit l'état initial du backend (onboardingState) et dispatch au bon écran.
class OnboardingFlowPage extends StatelessWidget {
final String onboardingState;
final String? souscriptionId;
final String organisationId;
final String? typeOrganisation;
const OnboardingFlowPage({
super.key,
required this.onboardingState,
required this.organisationId,
this.typeOrganisation,
this.souscriptionId,
});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => getIt<OnboardingBloc>()
..add(OnboardingStarted(
initialState: onboardingState,
existingSouscriptionId: souscriptionId,
typeOrganisation: typeOrganisation,
organisationId: organisationId.isNotEmpty ? organisationId : null,
)),
child: const _OnboardingFlowView(),
);
}
}
class _OnboardingFlowView extends StatelessWidget {
const _OnboardingFlowView();
@override
Widget build(BuildContext context) {
return BlocConsumer<OnboardingBloc, OnboardingState>(
listener: (context, state) {
// Paiement confirmé → re-check du statut (auto-activation backend)
if (state is OnboardingPaiementConfirme) {
context.read<AuthBloc>().add(const AuthStatusChecked());
}
},
builder: (context, state) {
if (state is OnboardingLoading || state is OnboardingInitial || state is OnboardingPaiementConfirme) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const CircularProgressIndicator(
color: UnionFlowColors.unionGreen,
),
const SizedBox(height: 16),
Text(
state is OnboardingPaiementConfirme
? 'Activation de votre compte…'
: 'Chargement…',
style: const TextStyle(
color: UnionFlowColors.textSecondary,
fontSize: 15,
),
),
],
),
),
);
}
if (state is OnboardingError) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
body: Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: UnionFlowColors.errorPale,
shape: BoxShape.circle,
),
child: const Icon(Icons.error_outline,
size: 40, color: UnionFlowColors.error),
),
const SizedBox(height: 20),
Text(
state.message,
textAlign: TextAlign.center,
style: const TextStyle(
color: UnionFlowColors.textPrimary, fontSize: 15),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => context.read<OnboardingBloc>().add(
const OnboardingStarted(
initialState: 'NO_SUBSCRIPTION'),
),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
),
child: const Text('Réessayer'),
),
],
),
),
),
);
}
if (state is OnboardingStepFormule) {
return PlanSelectionPage(formules: state.formules);
}
if (state is OnboardingStepPeriode) {
return PeriodSelectionPage(
codeFormule: state.codeFormule,
plage: state.plage,
formules: state.formules,
);
}
if (state is OnboardingStepSummary) {
return SubscriptionSummaryPage(souscription: state.souscription);
}
if (state is OnboardingStepChoixPaiement) {
return PaymentMethodPage(souscription: state.souscription);
}
if (state is OnboardingStepPaiement) {
return WavePaymentPage(
souscription: state.souscription,
waveLaunchUrl: state.waveLaunchUrl,
);
}
if (state is OnboardingStepAttente) {
return AwaitingValidationPage(souscription: state.souscription);
}
if (state is OnboardingRejected) {
return _RejectedPage(commentaire: state.commentaire);
}
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
},
);
}
}
class _RejectedPage extends StatelessWidget {
final String? commentaire;
const _RejectedPage({this.commentaire});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: UnionFlowColors.background,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: UnionFlowColors.errorPale,
shape: BoxShape.circle,
),
child: const Icon(Icons.cancel_outlined,
size: 52, color: UnionFlowColors.error),
),
const SizedBox(height: 28),
const Text(
'Demande rejetée',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: UnionFlowColors.textPrimary,
),
),
const SizedBox(height: 12),
const Text(
'Votre demande de souscription a été refusée.',
textAlign: TextAlign.center,
style: TextStyle(color: UnionFlowColors.textSecondary),
),
if (commentaire != null && commentaire!.isNotEmpty) ...[
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: UnionFlowColors.errorPale,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: UnionFlowColors.error.withOpacity(0.3)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.comment_outlined,
color: UnionFlowColors.error, size: 18),
const SizedBox(width: 10),
Expanded(
child: Text(
commentaire!,
style: const TextStyle(
color: UnionFlowColors.textPrimary,
fontSize: 14,
height: 1.5),
),
),
],
),
),
],
const SizedBox(height: 36),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => context.read<OnboardingBloc>().add(
const OnboardingStarted(
initialState: 'NO_SUBSCRIPTION'),
),
style: ElevatedButton.styleFrom(
backgroundColor: UnionFlowColors.unionGreen,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: const Text('Soumettre une nouvelle demande',
style: TextStyle(fontSize: 15)),
),
),
const SizedBox(height: 12),
TextButton(
onPressed: () =>
context.read<AuthBloc>().add(const AuthLogoutRequested()),
child: const Text('Se déconnecter',
style:
TextStyle(color: UnionFlowColors.textSecondary)),
),
],
),
),
),
);
}
}