Implement Notifications and Pad Tracking Enhancements
This commit is contained in:
@@ -4,12 +4,13 @@ import 'package:smooth_page_indicator/smooth_page_indicator.dart';
|
||||
import 'package:hive_flutter/hive_flutter.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../../theme/app_theme.dart';
|
||||
import 'package:christian_period_tracker/models/user_profile.dart';
|
||||
import 'package:christian_period_tracker/models/cycle_entry.dart';
|
||||
import '../../models/user_profile.dart';
|
||||
import '../../models/cycle_entry.dart';
|
||||
import '../home/home_screen.dart';
|
||||
import '../husband/husband_home_screen.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../services/notification_service.dart';
|
||||
|
||||
class OnboardingScreen extends ConsumerStatefulWidget {
|
||||
const OnboardingScreen({super.key});
|
||||
@@ -35,6 +36,10 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
int _maxCycleLength = 35;
|
||||
bool _isPadTrackingEnabled = false;
|
||||
|
||||
// Connection options
|
||||
bool _useExampleData = false;
|
||||
bool _skipPartnerConnection = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
@@ -45,14 +50,15 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
if (_isNavigating) return;
|
||||
_isNavigating = true;
|
||||
|
||||
// Husband Flow: Role (0) -> Name (1) -> Finish
|
||||
// Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4)
|
||||
// Husband Flow: Role (0) -> Name (1) -> Connect (2) -> Finish
|
||||
// Wife Flow: Role (0) -> Name (1) -> Relationship (2) -> [Fertility (3)] -> Cycle (4) -> [Connect (5) if married]
|
||||
|
||||
int nextPage = _currentPage + 1;
|
||||
|
||||
// Logic for skipping pages
|
||||
if (_role == UserRole.husband) {
|
||||
if (_currentPage == 1) {
|
||||
if (_currentPage == 2) {
|
||||
// Finish after connect page
|
||||
await _completeOnboarding();
|
||||
return;
|
||||
}
|
||||
@@ -63,10 +69,21 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
// Skip fertility goal (page 3) if not married
|
||||
nextPage = 4;
|
||||
}
|
||||
if (_currentPage == 4 &&
|
||||
_relationshipStatus != RelationshipStatus.married) {
|
||||
// Skip connect page (page 5) if not married - finish now
|
||||
await _completeOnboarding();
|
||||
return;
|
||||
}
|
||||
if (_currentPage == 5) {
|
||||
// Finish after connect page (married wife)
|
||||
await _completeOnboarding();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (nextPage <= 4) {
|
||||
// Max pages
|
||||
final maxPages = _role == UserRole.husband ? 2 : 5;
|
||||
if (nextPage <= maxPages) {
|
||||
await _pageController.animateToPage(
|
||||
nextPage,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
@@ -124,18 +141,24 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
? _fertilityGoal
|
||||
: null,
|
||||
averageCycleLength: _averageCycleLength,
|
||||
minCycleLength: _minCycleLength,
|
||||
maxCycleLength: _maxCycleLength,
|
||||
lastPeriodStartDate: _lastPeriodStart,
|
||||
isIrregularCycle: _isIrregularCycle,
|
||||
isPadTrackingEnabled: _isPadTrackingEnabled,
|
||||
hasCompletedOnboarding: true,
|
||||
useExampleData: _useExampleData,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
await ref.read(userProfileProvider.notifier).updateProfile(userProfile);
|
||||
|
||||
// Trigger partner connection notification if applicable
|
||||
if (!_skipPartnerConnection && !_useExampleData) {
|
||||
await NotificationService().showPartnerUpdateNotification(
|
||||
title: 'Connection Successful!',
|
||||
body: 'You are now connected with your partner. Tap to start sharing.',
|
||||
);
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
// Navigate to appropriate home screen
|
||||
if (_role == UserRole.husband) {
|
||||
@@ -174,7 +197,11 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: SmoothPageIndicator(
|
||||
controller: _pageController,
|
||||
count: isHusband ? 2 : 5,
|
||||
count: isHusband
|
||||
? 3
|
||||
: (_relationshipStatus == RelationshipStatus.married
|
||||
? 6
|
||||
: 5),
|
||||
effect: WormEffect(
|
||||
dotHeight: 8,
|
||||
dotWidth: 8,
|
||||
@@ -190,17 +217,22 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
physics:
|
||||
const NeverScrollableScrollPhysics(), // Disable swipe
|
||||
physics: const NeverScrollableScrollPhysics(), // Disable swipe
|
||||
onPageChanged: (index) {
|
||||
setState(() => _currentPage = index);
|
||||
},
|
||||
children: [
|
||||
_buildRolePage(), // Page 0
|
||||
_buildNamePage(), // Page 1
|
||||
_buildRelationshipPage(), // Page 2 (Wife only)
|
||||
_buildFertilityGoalPage(), // Page 3 (Wife married only)
|
||||
_buildCyclePage(), // Page 4 (Wife only)
|
||||
if (_role == UserRole.husband)
|
||||
_buildHusbandConnectPage() // Page 2 (Husband only)
|
||||
else ...[
|
||||
_buildRelationshipPage(), // Page 2 (Wife only)
|
||||
_buildFertilityGoalPage(), // Page 3 (Wife married only)
|
||||
_buildCyclePage(), // Page 4 (Wife only)
|
||||
if (_relationshipStatus == RelationshipStatus.married)
|
||||
_buildWifeConnectPage(), // Page 5 (Wife married only)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -223,10 +255,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
AppColors.blushPink,
|
||||
AppColors.rose.withOpacity(0.7)
|
||||
],
|
||||
colors: [AppColors.blushPink, AppColors.rose.withOpacity(0.7)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
@@ -304,9 +333,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? activeColor
|
||||
: theme.colorScheme.surfaceVariant,
|
||||
color:
|
||||
isSelected ? activeColor : theme.colorScheme.surfaceVariant,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
@@ -340,8 +368,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Icon(Icons.check_circle, color: activeColor),
|
||||
if (isSelected) Icon(Icons.check_circle, color: activeColor),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -351,8 +378,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
Widget _buildNamePage() {
|
||||
final theme = Theme.of(context);
|
||||
final isHusband = _role == UserRole.husband;
|
||||
final activeColor =
|
||||
isHusband ? AppColors.navyBlue : AppColors.sageGreen;
|
||||
final activeColor = isHusband ? AppColors.navyBlue : AppColors.sageGreen;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
@@ -413,9 +439,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: ElevatedButton(
|
||||
onPressed: (_name.isNotEmpty && !_isNavigating)
|
||||
? _nextPage
|
||||
: null,
|
||||
onPressed:
|
||||
(_name.isNotEmpty && !_isNavigating) ? _nextPage : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: activeColor,
|
||||
),
|
||||
@@ -557,11 +582,8 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface)),
|
||||
const SizedBox(height: 32),
|
||||
_buildGoalOption(
|
||||
FertilityGoal.tryingToConceive,
|
||||
'Trying to Conceive',
|
||||
'Track fertile days',
|
||||
Icons.child_care_outlined),
|
||||
_buildGoalOption(FertilityGoal.tryingToConceive, 'Trying to Conceive',
|
||||
'Track fertile days', Icons.child_care_outlined),
|
||||
const SizedBox(height: 12),
|
||||
_buildGoalOption(
|
||||
FertilityGoal.tryingToAvoid,
|
||||
@@ -692,8 +714,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
),
|
||||
Text('$_averageCycleLength days',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.sageGreen)),
|
||||
fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -720,12 +741,14 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: RangeSlider(
|
||||
values: RangeValues(_minCycleLength.toDouble(), _maxCycleLength.toDouble()),
|
||||
values: RangeValues(
|
||||
_minCycleLength.toDouble(), _maxCycleLength.toDouble()),
|
||||
min: 21,
|
||||
max: 45,
|
||||
divisions: 24,
|
||||
activeColor: AppColors.sageGreen,
|
||||
labels: RangeLabels('$_minCycleLength days', '$_maxCycleLength days'),
|
||||
labels: RangeLabels(
|
||||
'$_minCycleLength days', '$_maxCycleLength days'),
|
||||
onChanged: (values) {
|
||||
setState(() {
|
||||
_minCycleLength = values.start.round();
|
||||
@@ -739,8 +762,7 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
Center(
|
||||
child: Text('$_minCycleLength - $_maxCycleLength days',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.sageGreen)),
|
||||
fontWeight: FontWeight.w600, color: AppColors.sageGreen)),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -840,4 +862,267 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Husband Connect Page - Choose to connect with wife or use example data
|
||||
Widget _buildHusbandConnectPage() {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.navyBlue.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(Icons.link, size: 32, color: AppColors.navyBlue),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Connect with your wife',
|
||||
style: theme.textTheme.displaySmall?.copyWith(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'See her cycle info and prayer requests to support her better.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Option 1: Connect with Wife (placeholder for now)
|
||||
_buildConnectOption(
|
||||
icon: Icons.qr_code_scanner,
|
||||
title: 'Connect with Wife',
|
||||
subtitle: 'Enter connection code from her app',
|
||||
isSelected: !_useExampleData,
|
||||
onTap: () => setState(() => _useExampleData = false),
|
||||
color: AppColors.navyBlue,
|
||||
isDark: isDark,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Option 2: Use Example Data
|
||||
_buildConnectOption(
|
||||
icon: Icons.auto_awesome,
|
||||
title: 'Use Example Data',
|
||||
subtitle: 'Explore the app with sample data',
|
||||
isSelected: _useExampleData,
|
||||
onTap: () => setState(() => _useExampleData = true),
|
||||
color: AppColors.navyBlue,
|
||||
isDark: isDark,
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: OutlinedButton(
|
||||
onPressed: _previousPage,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.navyBlue,
|
||||
side: const BorderSide(color: AppColors.navyBlue),
|
||||
),
|
||||
child: const Text('Back'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: ElevatedButton(
|
||||
onPressed: !_isNavigating ? _nextPage : null,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.navyBlue,
|
||||
),
|
||||
child: const Text('Finish Setup'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Wife Connect Page - Invite husband or skip
|
||||
Widget _buildWifeConnectPage() {
|
||||
final theme = Theme.of(context);
|
||||
final isDark = theme.brightness == Brightness.dark;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 40),
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.sageGreen.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Icon(Icons.favorite, size: 32, color: AppColors.sageGreen),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'Invite your husband',
|
||||
style: theme.textTheme.displaySmall?.copyWith(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Share your cycle info and prayer requests so he can support you.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// Option 1: Invite Husband
|
||||
_buildConnectOption(
|
||||
icon: Icons.share,
|
||||
title: 'Invite Husband',
|
||||
subtitle: 'Generate a connection code to share',
|
||||
isSelected: !_skipPartnerConnection,
|
||||
onTap: () => setState(() => _skipPartnerConnection = false),
|
||||
color: AppColors.sageGreen,
|
||||
isDark: isDark,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Option 2: Skip for Now
|
||||
_buildConnectOption(
|
||||
icon: Icons.schedule,
|
||||
title: 'Skip for Now',
|
||||
subtitle: 'You can invite him later in settings',
|
||||
isSelected: _skipPartnerConnection,
|
||||
onTap: () => setState(() => _skipPartnerConnection = true),
|
||||
color: AppColors.sageGreen,
|
||||
isDark: isDark,
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: OutlinedButton(
|
||||
onPressed: _previousPage,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.sageGreen,
|
||||
side: const BorderSide(color: AppColors.sageGreen),
|
||||
),
|
||||
child: const Text('Back'),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: SizedBox(
|
||||
height: 54,
|
||||
child: ElevatedButton(
|
||||
onPressed: !_isNavigating ? _nextPage : null,
|
||||
child: const Text('Get Started'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Helper for connection option cards
|
||||
Widget _buildConnectOption({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String subtitle,
|
||||
required bool isSelected,
|
||||
required VoidCallback onTap,
|
||||
required Color color,
|
||||
required bool isDark,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? color.withOpacity(isDark ? 0.3 : 0.1)
|
||||
: theme.cardTheme.color,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color:
|
||||
isSelected ? color : theme.colorScheme.outline.withOpacity(0.1),
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? color : theme.colorScheme.surfaceVariant,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: isSelected
|
||||
? Colors.white
|
||||
: theme.colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected) Icon(Icons.check_circle, color: color),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user