Merge NDM-App-233-user-onboarding-persistence-save-to-db-load-on-sta into dev

This commit is contained in:
TrochtaOndrej
2026-03-18 21:15:22 +01:00
6 changed files with 88 additions and 22 deletions
+21
View File
@@ -1,6 +1,8 @@
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../features/onboarding/screens/onboarding_screen.dart';
import '../../features/tasks/providers/task_providers.dart';
import '../../features/tasks/screens/tasks_screen.dart'; import '../../features/tasks/screens/tasks_screen.dart';
import '../../features/tasks/screens/pet_screen.dart'; import '../../features/tasks/screens/pet_screen.dart';
import '../../features/zen_mode/screens/zen_screen.dart'; import '../../features/zen_mode/screens/zen_screen.dart';
@@ -9,9 +11,28 @@ import '../../shared/widgets/app_shell.dart';
import 'routes.dart'; import 'routes.dart';
final appRouterProvider = Provider<GoRouter>((ref) { final appRouterProvider = Provider<GoRouter>((ref) {
final currentUser = ref.watch(currentUserProvider);
return GoRouter( return GoRouter(
initialLocation: Routes.tasks, initialLocation: Routes.tasks,
redirect: (context, state) {
final isOnboarding = state.matchedLocation == Routes.onboarding;
// While loading user from DB, don't redirect
if (currentUser.isLoading) return null;
final hasUser = currentUser.value != null;
if (!hasUser && !isOnboarding) return Routes.onboarding;
if (hasUser && isOnboarding) return Routes.tasks;
return null;
},
routes: [ routes: [
// Onboarding — outside shell, no bottom nav
GoRoute(
path: Routes.onboarding,
builder: (context, state) => const OnboardingScreen(),
),
StatefulShellRoute.indexedStack( StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) { builder: (context, state, navigationShell) {
return AppShell(navigationShell: navigationShell); return AppShell(navigationShell: navigationShell);
+1
View File
@@ -4,4 +4,5 @@ abstract final class Routes {
static const pet = '/pet'; static const pet = '/pet';
static const zen = '/zen'; static const zen = '/zen';
static const profile = '/profile'; static const profile = '/profile';
static const onboarding = '/onboarding';
} }
@@ -1,11 +1,14 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart';
import '../../../core/theme/kittie_colors.dart'; import '../../../core/theme/kittie_colors.dart';
import '../../../core/theme/kittie_typography.dart'; import '../../../core/theme/kittie_typography.dart';
import '../../../domain/entities/pet_type.dart'; import '../../../domain/entities/pet_type.dart';
import '../../tasks/providers/task_providers.dart';
import '../providers/onboarding_providers.dart'; import '../providers/onboarding_providers.dart';
class OnboardingScreen extends ConsumerStatefulWidget { class OnboardingScreen extends ConsumerStatefulWidget {
@@ -43,13 +46,23 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
} }
Future<void> _completeOnboarding() async { Future<void> _completeOnboarding() async {
final prefs = await SharedPreferences.getInstance(); final repo = ref.read(userRepositoryProvider);
await prefs.setBool('onboarding_complete', true); final petType = ref.read(selectedPetTypeProvider);
await prefs.setString( final petName = ref.read(petNameProvider);
'pet_type',
ref.read(selectedPetTypeProvider).name, const uuid = Uuid();
final userId = uuid.v4();
final deviceId = '${Platform.operatingSystem}-${uuid.v4()}';
await repo.createUser(
id: userId,
deviceId: deviceId,
name: 'Friend',
petType: petType,
petName: petName,
); );
await prefs.setString('pet_name', ref.read(petNameProvider));
ref.invalidate(currentUserProvider);
if (mounted) { if (mounted) {
context.go('/tasks'); context.go('/tasks');
+28 -11
View File
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../domain/entities/pet_state_entity.dart'; import '../../../domain/entities/pet_state_entity.dart';
import '../../../domain/entities/pet_type.dart'; import '../../../domain/entities/pet_type.dart';
import '../../../domain/entities/user_entity.dart'; import '../../../domain/entities/user_entity.dart';
import 'task_providers.dart';
/// Pet mood states. /// Pet mood states.
enum PetMood { enum PetMood {
@@ -21,18 +22,34 @@ enum PetMood {
class UserNotifier extends Notifier<UserEntity> { class UserNotifier extends Notifier<UserEntity> {
@override @override
UserEntity build() => UserEntity( UserEntity build() {
id: '1', final currentUser = ref.watch(currentUserProvider).value;
name: 'Friend', if (currentUser != null) return currentUser;
petType: PetType.cat, return UserEntity(
petName: 'Yoshi', id: '',
createdAt: DateTime.now(), name: 'Friend',
updatedAt: DateTime.now(), petType: PetType.cat,
); petName: 'Yoshi',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
void updateName(String name) => state = state.copyWith(name: name); void updateName(String name) {
void updatePetName(String petName) => state = state.copyWith(name: name, updatedAt: DateTime.now());
state = state.copyWith(petName: petName); _persist();
}
void updatePetName(String petName) {
state = state.copyWith(petName: petName, updatedAt: DateTime.now());
_persist();
}
void _persist() {
if (state.id.isEmpty) return;
final repo = ref.read(userRepositoryProvider);
repo.updateUser(state);
}
} }
final userProvider = final userProvider =
@@ -2,8 +2,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../data/db/app_database.dart'; import '../../../data/db/app_database.dart';
import '../../../data/repositories/task_repository.dart'; import '../../../data/repositories/task_repository.dart';
import '../../../data/repositories/user_repository.dart';
import '../../../domain/entities/task_entity.dart'; import '../../../domain/entities/task_entity.dart';
import '../../../domain/entities/task_status.dart'; import '../../../domain/entities/task_status.dart';
import '../../../domain/entities/user_entity.dart';
// ── Infrastructure providers ── // ── Infrastructure providers ──
@@ -19,14 +21,25 @@ final taskRepositoryProvider = Provider<TaskRepository>((ref) {
return TaskRepository(ref.watch(appDatabaseProvider)); return TaskRepository(ref.watch(appDatabaseProvider));
}); });
// ── Task list (reactive stream from DB) ── /// User repository backed by Drift.
final userRepositoryProvider = Provider<UserRepository>((ref) {
return UserRepository(ref.watch(appDatabaseProvider));
});
const _userId = 'user-1'; /// Current user loaded from the database. Null if onboarding not complete.
final currentUserProvider = FutureProvider<UserEntity?>((ref) {
final repo = ref.watch(userRepositoryProvider);
return repo.getCurrentUser();
});
// ── Task list (reactive stream from DB) ──
/// All tasks for the current user, streamed from Drift. /// All tasks for the current user, streamed from Drift.
final tasksProvider = StreamProvider<List<TaskEntity>>((ref) { final tasksProvider = StreamProvider<List<TaskEntity>>((ref) {
final user = ref.watch(currentUserProvider).value;
if (user == null) return Stream.value([]);
final repo = ref.watch(taskRepositoryProvider); final repo = ref.watch(taskRepositoryProvider);
return repo.watchTasks(_userId); return repo.watchTasks(user.id);
}); });
/// Alias for [tasksProvider]. Returns all tasks — no date filtering in MVP. /// Alias for [tasksProvider]. Returns all tasks — no date filtering in MVP.
@@ -7,6 +7,7 @@ import '../../../core/theme/kittie_colors.dart';
import '../../../core/theme/kittie_typography.dart'; import '../../../core/theme/kittie_typography.dart';
import '../../../domain/entities/task_entity.dart'; import '../../../domain/entities/task_entity.dart';
import '../providers/add_task_providers.dart'; import '../providers/add_task_providers.dart';
import '../providers/pet_providers.dart';
import '../providers/task_providers.dart'; import '../providers/task_providers.dart';
/// Shows the Add Task bottom sheet. Call from any screen: /// Shows the Add Task bottom sheet. Call from any screen:
@@ -59,7 +60,7 @@ class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
final entity = TaskEntity( final entity = TaskEntity(
id: const Uuid().v4(), id: const Uuid().v4(),
userId: 'user-1', userId: ref.read(userProvider).id,
title: title, title: title,
description: note.isEmpty ? null : note, description: note.isEmpty ? null : note,
energyLevel: energy, energyLevel: energy,