feat: User/onboarding persistence — save to DB, load on startup [AITM-233]
This commit is contained in:
@@ -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,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');
|
||||
|
||||
@@ -22,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 =
|
||||
|
||||
@@ -34,12 +34,12 @@ final currentUserProvider = FutureProvider<UserEntity?>((ref) {
|
||||
|
||||
// ── Task list (reactive stream from DB) ──
|
||||
|
||||
const _userId = 'user-1';
|
||||
|
||||
/// 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,
|
||||
|
||||
Reference in New Issue
Block a user