Merge NDM-App-233-user-onboarding-persistence-save-to-db-load-on-sta into dev
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user