sojorn/sojorn_app/lib/widgets/onboarding_modal.dart

398 lines
13 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:shared_preferences/shared_preferences.dart';
import '../theme/app_theme.dart';
import '../theme/tokens.dart';
/// 3-screen swipeable onboarding modal shown on first app launch.
/// Stores completion in SharedPreferences so it only shows once.
class OnboardingModal extends StatefulWidget {
const OnboardingModal({super.key});
static const _prefKey = 'onboarding_completed';
/// Shows the onboarding modal if the user hasn't completed it yet.
/// Call this from HomeShell.initState via addPostFrameCallback.
static Future<void> showIfNeeded(BuildContext context) async {
final prefs = await SharedPreferences.getInstance();
if (prefs.getBool(_prefKey) == true) return;
if (!context.mounted) return;
showGeneralDialog(
context: context,
barrierDismissible: false,
barrierColor: Colors.black54,
pageBuilder: (_, __, ___) => const OnboardingModal(),
);
}
/// Resets the onboarding flag so it shows again (for Settings → "Show Tutorial Again").
static Future<void> reset() async {
final prefs = await SharedPreferences.getInstance();
await prefs.remove(_prefKey);
}
@override
State<OnboardingModal> createState() => _OnboardingModalState();
}
class _OnboardingModalState extends State<OnboardingModal> {
final _controller = PageController();
int _currentPage = 0;
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Future<void> _complete() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(OnboardingModal._prefKey, true);
if (mounted) Navigator.of(context).pop();
}
void _next() {
if (_currentPage < 2) {
_controller.nextPage(duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
} else {
_complete();
}
}
@override
Widget build(BuildContext context) {
return Center(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 24),
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 520),
decoration: BoxDecoration(
color: AppTheme.cardSurface,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.2),
blurRadius: 24,
offset: const Offset(0, 8),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(24),
child: Material(
color: Colors.transparent,
child: Column(
children: [
Expanded(
child: PageView(
controller: _controller,
onPageChanged: (i) => setState(() => _currentPage = i),
children: const [
_WelcomePage(),
_FeaturesPage(),
_HarmonyPage(),
],
),
),
// Page indicator + button
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 24),
child: Column(
children: [
// Dots
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (i) => Container(
width: _currentPage == i ? 24 : 8,
height: 8,
margin: const EdgeInsets.symmetric(horizontal: 3),
decoration: BoxDecoration(
color: _currentPage == i
? AppTheme.navyBlue
: AppTheme.navyBlue.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(4),
),
)),
),
const SizedBox(height: 20),
// CTA button
SizedBox(
width: double.infinity,
height: 48,
child: ElevatedButton(
onPressed: _next,
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.navyBlue,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
elevation: 0,
),
child: Text(
_currentPage == 2 ? 'Get Started' : 'Next',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w700),
),
),
),
if (_currentPage < 2) ...[
const SizedBox(height: 8),
TextButton(
onPressed: _complete,
child: Text('Skip', style: TextStyle(
color: AppTheme.navyBlue.withValues(alpha: 0.5),
fontSize: 13,
)),
),
],
],
),
),
],
),
),
),
),
);
}
}
// ── Screen 1: Welcome ─────────────────────────────────────────────────────
class _WelcomePage extends StatelessWidget {
const _WelcomePage();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(28, 40, 28, 8),
child: Column(
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [AppTheme.navyBlue, AppTheme.brightNavy],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
),
child: const Icon(Icons.shield_outlined, color: Colors.white, size: 40),
),
const SizedBox(height: 28),
Text('Welcome to Your Sanctuary', style: TextStyle(
fontSize: 22, fontWeight: FontWeight.w800, color: AppTheme.navyBlue,
), textAlign: TextAlign.center),
const SizedBox(height: 14),
Text(
'A private, intentional social space.\nYour posts are encrypted. Your data belongs to you.',
style: TextStyle(
fontSize: 14, color: SojornColors.postContentLight, height: 1.5,
),
textAlign: TextAlign.center,
),
const Spacer(),
Icon(Icons.lock_outline, size: 28, color: AppTheme.navyBlue.withValues(alpha: 0.15)),
const SizedBox(height: 8),
],
),
);
}
}
// ── Screen 2: Four Ways to Connect ────────────────────────────────────────
class _FeaturesPage extends StatelessWidget {
const _FeaturesPage();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(28, 36, 28, 8),
child: Column(
children: [
Text('Four Ways to Connect', style: TextStyle(
fontSize: 20, fontWeight: FontWeight.w800, color: AppTheme.navyBlue,
)),
const SizedBox(height: 24),
_FeatureRow(
icon: Icons.article_outlined,
color: const Color(0xFF2196F3),
title: 'Posts',
subtitle: 'Share thoughts with your circle',
),
const SizedBox(height: 14),
_FeatureRow(
icon: Icons.play_circle_outline,
color: const Color(0xFF9C27B0),
title: 'Quips',
subtitle: 'Short videos, your stories',
),
const SizedBox(height: 14),
_FeatureRow(
icon: Icons.forum_outlined,
color: const Color(0xFFFF9800),
title: 'Chains',
subtitle: 'Deep conversations, threaded replies',
),
const SizedBox(height: 14),
_FeatureRow(
icon: Icons.sensors,
color: const Color(0xFF4CAF50),
title: 'Beacons',
subtitle: 'Local alerts and real-time updates',
),
const Spacer(),
],
),
);
}
}
class _FeatureRow extends StatelessWidget {
final IconData icon;
final Color color;
final String title;
final String subtitle;
const _FeatureRow({
required this.icon,
required this.color,
required this.title,
required this.subtitle,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: 44, height: 44,
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(icon, color: color, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: const TextStyle(
fontSize: 15, fontWeight: FontWeight.w700,
)),
const SizedBox(height: 2),
Text(subtitle, style: TextStyle(
fontSize: 12, color: SojornColors.textDisabled,
)),
],
),
),
],
);
}
}
// ── Screen 3: Build Your Harmony ──────────────────────────────────────────
class _HarmonyPage extends StatelessWidget {
const _HarmonyPage();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(28, 36, 28, 8),
child: Column(
children: [
Container(
width: 72, height: 72,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50).withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.auto_graph, color: Color(0xFF4CAF50), size: 36),
),
const SizedBox(height: 24),
Text('Build Your Harmony', style: TextStyle(
fontSize: 20, fontWeight: FontWeight.w800, color: AppTheme.navyBlue,
)),
const SizedBox(height: 14),
Text(
'Your Harmony State grows as you contribute positively. Higher harmony means greater reach.',
style: TextStyle(
fontSize: 14, color: SojornColors.postContentLight, height: 1.5,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// Mini progression chart
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppTheme.navyBlue.withValues(alpha: 0.04),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppTheme.navyBlue.withValues(alpha: 0.08)),
),
child: Column(
children: [
_HarmonyLevel(label: 'New', range: '0100', multiplier: '1.0x', isActive: true),
const SizedBox(height: 8),
_HarmonyLevel(label: 'Trusted', range: '100500', multiplier: '1.5x', isActive: false),
const SizedBox(height: 8),
_HarmonyLevel(label: 'Pillar', range: '500+', multiplier: '2.0x', isActive: false),
],
),
),
const Spacer(),
],
),
);
}
}
class _HarmonyLevel extends StatelessWidget {
final String label;
final String range;
final String multiplier;
final bool isActive;
const _HarmonyLevel({
required this.label,
required this.range,
required this.multiplier,
required this.isActive,
});
@override
Widget build(BuildContext context) {
return Row(
children: [
Container(
width: 10, height: 10,
decoration: BoxDecoration(
color: isActive ? const Color(0xFF4CAF50) : AppTheme.navyBlue.withValues(alpha: 0.15),
shape: BoxShape.circle,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(label, style: TextStyle(
fontSize: 13, fontWeight: isActive ? FontWeight.w700 : FontWeight.w500,
color: isActive ? AppTheme.navyBlue : SojornColors.textDisabled,
)),
),
Text(range, style: TextStyle(fontSize: 11, color: SojornColors.textDisabled)),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: isActive
? const Color(0xFF4CAF50).withValues(alpha: 0.1)
: AppTheme.navyBlue.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(8),
),
child: Text(multiplier, style: TextStyle(
fontSize: 11, fontWeight: FontWeight.w700,
color: isActive ? const Color(0xFF4CAF50) : SojornColors.textDisabled,
)),
),
],
);
}
}