Phase 1 du MVP : schéma SQLite porté depuis le prototype web, seed des règles/récompenses, flux d'onboarding en 4 étapes et écran d'accueil minimal. Co-authored-by: Cursor <cursoragent@cursor.com>
130 lines
3.5 KiB
Dart
130 lines
3.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../providers/database_provider.dart';
|
|
import '../dashboard/dashboard_screen.dart';
|
|
import 'onboarding_state.dart';
|
|
import 'steps/child_step.dart';
|
|
import 'steps/pin_step.dart';
|
|
import 'steps/rules_step.dart';
|
|
import 'steps/welcome_step.dart';
|
|
|
|
class OnboardingFlow extends ConsumerStatefulWidget {
|
|
const OnboardingFlow({super.key});
|
|
|
|
@override
|
|
ConsumerState<OnboardingFlow> createState() => _OnboardingFlowState();
|
|
}
|
|
|
|
class _OnboardingFlowState extends ConsumerState<OnboardingFlow> {
|
|
final _pageController = PageController();
|
|
bool _isSaving = false;
|
|
|
|
@override
|
|
void dispose() {
|
|
_pageController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _goTo(int page) {
|
|
_pageController.animateToPage(
|
|
page,
|
|
duration: const Duration(milliseconds: 300),
|
|
curve: Curves.easeInOut,
|
|
);
|
|
}
|
|
|
|
Future<void> _finish() async {
|
|
final draft = ref.read(onboardingDraftProvider);
|
|
if (draft.childName.trim().isEmpty) {
|
|
_showError('Indiquez le prénom de l\'enfant.');
|
|
_goTo(1);
|
|
return;
|
|
}
|
|
if (draft.pin.length < 4) {
|
|
_showError('Le PIN doit contenir au moins 4 chiffres.');
|
|
_goTo(2);
|
|
return;
|
|
}
|
|
if (draft.pin != draft.pinConfirm) {
|
|
_showError('Les deux PIN ne correspondent pas.');
|
|
_goTo(2);
|
|
return;
|
|
}
|
|
|
|
setState(() => _isSaving = true);
|
|
try {
|
|
final repo = ref.read(onboardingRepositoryProvider);
|
|
await repo.finish(
|
|
childName: draft.childName,
|
|
color: draft.color,
|
|
pin: draft.pin,
|
|
importDefaultRules: draft.importDefaultRules,
|
|
);
|
|
ref.invalidate(onboardingCompleteProvider);
|
|
ref.invalidate(childrenProvider);
|
|
|
|
if (!mounted) return;
|
|
Navigator.of(context).pushReplacement(
|
|
MaterialPageRoute<void>(builder: (_) => const DashboardScreen()),
|
|
);
|
|
} catch (e) {
|
|
_showError('Erreur lors de l\'enregistrement. Réessayez.');
|
|
} finally {
|
|
if (mounted) setState(() => _isSaving = false);
|
|
}
|
|
}
|
|
|
|
void _showError(String message) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(message), backgroundColor: Colors.red.shade700),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PageView(
|
|
controller: _pageController,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
onPageChanged: (_) {},
|
|
children: [
|
|
WelcomeStep(onNext: () => _goTo(1)),
|
|
ChildStep(
|
|
step: 2,
|
|
onBack: () => _goTo(0),
|
|
onNext: () {
|
|
final name = ref.read(onboardingDraftProvider).childName.trim();
|
|
if (name.isEmpty) {
|
|
_showError('Indiquez le prénom de l\'enfant.');
|
|
return;
|
|
}
|
|
_goTo(2);
|
|
},
|
|
),
|
|
PinStep(
|
|
step: 3,
|
|
onBack: () => _goTo(1),
|
|
onNext: () {
|
|
final draft = ref.read(onboardingDraftProvider);
|
|
if (draft.pin.length < 4) {
|
|
_showError('Le PIN doit contenir au moins 4 chiffres.');
|
|
return;
|
|
}
|
|
if (draft.pin != draft.pinConfirm) {
|
|
_showError('Les deux PIN ne correspondent pas.');
|
|
return;
|
|
}
|
|
_goTo(3);
|
|
},
|
|
),
|
|
RulesStep(
|
|
step: 4,
|
|
isSaving: _isSaving,
|
|
onBack: () => _goTo(2),
|
|
onFinish: _finish,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|