feat: User/onboarding persistence — save to DB, load on startup [AITM-233]

This commit is contained in:
TrochtaOndrej
2026-03-18 21:15:22 +01:00
parent 23fcc86fd2
commit 88c5ffe51f
5 changed files with 73 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,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');
+27 -11
View File
@@ -22,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 =
@@ -34,12 +34,12 @@ final currentUserProvider = FutureProvider<UserEntity?>((ref) {
// ── Task list (reactive stream from DB) ── // ── Task list (reactive stream from DB) ──
const _userId = 'user-1';
/// 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,