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: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/pet_screen.dart';
import '../../features/zen_mode/screens/zen_screen.dart';
@@ -9,9 +11,28 @@ import '../../shared/widgets/app_shell.dart';
import 'routes.dart';
final appRouterProvider = Provider<GoRouter>((ref) {
final currentUser = ref.watch(currentUserProvider);
return GoRouter(
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: [
// Onboarding — outside shell, no bottom nav
GoRoute(
path: Routes.onboarding,
builder: (context, state) => const OnboardingScreen(),
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return AppShell(navigationShell: navigationShell);
+1
View File
@@ -4,4 +4,5 @@ abstract final class Routes {
static const pet = '/pet';
static const zen = '/zen';
static const profile = '/profile';
static const onboarding = '/onboarding';
}
@@ -1,11 +1,14 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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_typography.dart';
import '../../../domain/entities/pet_type.dart';
import '../../tasks/providers/task_providers.dart';
import '../providers/onboarding_providers.dart';
class OnboardingScreen extends ConsumerStatefulWidget {
@@ -43,13 +46,23 @@ class _OnboardingScreenState extends ConsumerState<OnboardingScreen> {
}
Future<void> _completeOnboarding() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool('onboarding_complete', true);
await prefs.setString(
'pet_type',
ref.read(selectedPetTypeProvider).name,
final repo = ref.read(userRepositoryProvider);
final petType = ref.read(selectedPetTypeProvider);
final petName = ref.read(petNameProvider);
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) {
context.go('/tasks');
@@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../domain/entities/pet_state_entity.dart';
import '../../../domain/entities/pet_type.dart';
import '../../../domain/entities/user_entity.dart';
import 'task_providers.dart';
/// Pet mood states.
enum PetMood {
@@ -21,18 +22,34 @@ enum PetMood {
class UserNotifier extends Notifier<UserEntity> {
@override
UserEntity build() => UserEntity(
id: '1',
UserEntity build() {
final currentUser = ref.watch(currentUserProvider).value;
if (currentUser != null) return currentUser;
return UserEntity(
id: '',
name: 'Friend',
petType: PetType.cat,
petName: 'Yoshi',
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
}
void updateName(String name) => state = state.copyWith(name: name);
void updatePetName(String petName) =>
state = state.copyWith(petName: petName);
void updateName(String name) {
state = state.copyWith(name: name, updatedAt: DateTime.now());
_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 =
@@ -2,8 +2,10 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../data/db/app_database.dart';
import '../../../data/repositories/task_repository.dart';
import '../../../data/repositories/user_repository.dart';
import '../../../domain/entities/task_entity.dart';
import '../../../domain/entities/task_status.dart';
import '../../../domain/entities/user_entity.dart';
// ── Infrastructure providers ──
@@ -19,14 +21,25 @@ final taskRepositoryProvider = Provider<TaskRepository>((ref) {
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.
final tasksProvider = StreamProvider<List<TaskEntity>>((ref) {
final user = ref.watch(currentUserProvider).value;
if (user == null) return Stream.value([]);
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.
@@ -7,6 +7,7 @@ import '../../../core/theme/kittie_colors.dart';
import '../../../core/theme/kittie_typography.dart';
import '../../../domain/entities/task_entity.dart';
import '../providers/add_task_providers.dart';
import '../providers/pet_providers.dart';
import '../providers/task_providers.dart';
/// Shows the Add Task bottom sheet. Call from any screen:
@@ -59,7 +60,7 @@ class _AddTaskSheetState extends ConsumerState<_AddTaskSheet> {
final entity = TaskEntity(
id: const Uuid().v4(),
userId: 'user-1',
userId: ref.read(userProvider).id,
title: title,
description: note.isEmpty ? null : note,
energyLevel: energy,