app/lib/screens/onboarding_screen.dart
John Mizerek 915e5f952b Fix back button from baseline leading to black screen
Changed pushReplacement to push in onboarding navigation so the
onboarding screen stays in the nav stack when moving to baseline.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-25 20:39:31 -08:00

309 lines
9.3 KiB
Dart

import 'package:flutter/material.dart';
import '../storage/local_storage.dart';
import '../theme.dart';
import 'baseline_screen.dart';
import 'welcome_screen.dart';
class OnboardingScreen extends StatefulWidget {
const OnboardingScreen({super.key});
@override
State<OnboardingScreen> createState() => _OnboardingScreenState();
}
class _OnboardingScreenState extends State<OnboardingScreen> {
final _controller = PageController();
int _currentPage = 0;
final _slides = const [
_SlideData(
icon: Icons.person_outline,
title: 'Tell us your baseline',
description: 'Age, sex, country, and any existing health conditions.',
),
_SlideData(
icon: Icons.checklist_outlined,
title: 'Answer a few questions',
description: 'Simple inputs about sleep, activity, and daily habits.',
),
_SlideData(
icon: Icons.insights_outlined,
title: 'See your biggest lever',
description: 'Discover which single change could add the most months to your life.',
),
];
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
// Header
Padding(
padding: const EdgeInsets.fromLTRB(32, 24, 32, 0),
child: Column(
children: [
Text(
'Add Months',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 4),
Text(
'Evidence-based lifespan optimization',
style: Theme.of(context).textTheme.bodyMedium,
),
],
),
),
const SizedBox(height: 16),
// Progress bar
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: _buildProgressBar(),
),
// Slides
Expanded(
child: PageView.builder(
controller: _controller,
itemCount: _slides.length,
onPageChanged: (index) => setState(() => _currentPage = index),
itemBuilder: (context, index) =>
_buildSlide(index, _slides[index]),
),
),
// Bottom section
Padding(
padding: const EdgeInsets.fromLTRB(32, 0, 32, 32),
child: Column(
children: [
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _onButtonPressed,
child: Text(_currentPage == _slides.length - 1
? 'Get Started'
: 'Next'),
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: _skip,
child: Text(
'Skip',
style: TextStyle(color: AppColors.textTertiary),
),
),
const SizedBox(width: 24),
TextButton(
onPressed: _showSkipForeverConfirmation,
child: Text(
'Skip Forever',
style: TextStyle(color: AppColors.textTertiary),
),
),
],
),
],
),
),
],
),
),
);
}
Widget _buildProgressBar() {
return Row(
children: List.generate(_slides.length, (index) {
final isCompleted = index < _currentPage;
final isCurrent = index == _currentPage;
return Expanded(
child: Container(
margin: EdgeInsets.only(right: index < _slides.length - 1 ? 8 : 0),
child: Column(
children: [
// Step number row
Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: isCompleted || isCurrent
? AppColors.primary
: AppColors.divider,
shape: BoxShape.circle,
),
child: Center(
child: isCompleted
? const Icon(Icons.check,
size: 14, color: Colors.white)
: Text(
'${index + 1}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isCurrent
? Colors.white
: AppColors.textTertiary,
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Container(
height: 3,
decoration: BoxDecoration(
color: isCompleted
? AppColors.primary
: AppColors.divider,
borderRadius: BorderRadius.circular(2),
),
),
),
],
),
],
),
),
);
}),
);
}
Widget _buildSlide(int index, _SlideData slide) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Step indicator
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: AppColors.primary.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'Step ${index + 1} of ${_slides.length}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.primary,
),
),
),
const SizedBox(height: 32),
// Icon
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppColors.primary.withAlpha(26),
shape: BoxShape.circle,
),
child: Icon(
slide.icon,
size: 48,
color: AppColors.primary,
),
),
const SizedBox(height: 32),
// Title
Text(
slide.title,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
// Description
Text(
slide.description,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: AppColors.textSecondary,
),
textAlign: TextAlign.center,
),
],
),
);
}
void _onButtonPressed() {
if (_currentPage < _slides.length - 1) {
_controller.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
} else {
_navigateToBaseline();
}
}
void _skip() {
_navigateToBaseline();
}
void _navigateToBaseline() {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const BaselineScreen()),
);
}
void _showSkipForeverConfirmation() {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Skip Forever?'),
content: const Text(
'You won\'t be asked to complete the questionnaire again. '
'You can still access the app from the About screen.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
await LocalStorage.setSkipForever(true);
Navigator.pop(dialogContext);
if (mounted) {
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (_) => const WelcomeScreen()),
(route) => false,
);
}
},
child: const Text('Skip Forever'),
),
],
),
);
}
}
class _SlideData {
final IconData icon;
final String title;
final String description;
const _SlideData({
required this.icon,
required this.title,
required this.description,
});
}