Phase 2: Household Creation & Invites - 6 plans in 4 waves covering SHARE-01 through SHARE-05 - Data models, repository pattern, and Supabase integration - Database schema with RLS policies for multi-tenant isolation - State management with Riverpod and business logic use cases - Complete UI components for household management - Navigation integration with authentication flow Ready for execution: /gsd:execute-phase 2
676 lines
24 KiB
Markdown
676 lines
24 KiB
Markdown
---
|
|
phase: 02-household-creation
|
|
plan: 06
|
|
type: execute
|
|
wave: 4
|
|
depends_on: [02-05, 02-03]
|
|
files_modified:
|
|
- lib/core/router/app_router.dart
|
|
- lib/features/home/presentation/pages/home_page.dart
|
|
- lib/providers/auth_provider.dart
|
|
- lib/main.dart
|
|
autonomous: true
|
|
|
|
must_haves:
|
|
truths:
|
|
- "Navigation system integrates household selection into app flow"
|
|
- "Auth state changes automatically load user households"
|
|
- "Home page displays current household context with navigation to household management"
|
|
- "Routing handles all household states (no household, single household, multiple households)"
|
|
- "Real-time household updates reflect across the app"
|
|
artifacts:
|
|
- path: "lib/core/router/app_router.dart"
|
|
provides: "Household-aware navigation routes"
|
|
contains: "HouseholdListRoute", "CreateHouseholdRoute", "JoinHouseholdRoute"
|
|
- path: "lib/features/home/presentation/pages/home_page.dart"
|
|
provides: "Home page with household context"
|
|
contains: "current household display", "switch household action"
|
|
- path: "lib/providers/auth_provider.dart"
|
|
provides: "Auth state with household integration"
|
|
contains: "household loading on auth change"
|
|
key_links:
|
|
- from: "lib/core/router/app_router.dart"
|
|
to: "lib/features/household/presentation/pages/household_list_page.dart"
|
|
via: "route definition"
|
|
pattern: "HouseholdListRoute.*HouseholdListPage"
|
|
- from: "lib/providers/auth_provider.dart"
|
|
to: "lib/features/household/providers/household_provider.dart"
|
|
via: "state synchronization"
|
|
pattern: "ref.read.*householdProvider.*loadUserHouseholds"
|
|
---
|
|
|
|
<objective>
|
|
Integrate household functionality into app navigation and authentication flow for seamless user experience.
|
|
|
|
Purpose: Connect household management with existing app architecture so users naturally transition from authentication to household setup and inventory management.
|
|
Output: Fully integrated household system with navigation, auth integration, and state management ready for production.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@~/.opencode/get-shit-done/workflows/execute-plan.md
|
|
@~/.opencode/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/PROJECT.md
|
|
@.planning/ROADMAP.md
|
|
@.planning/STATE.md
|
|
|
|
# Navigation and Auth References
|
|
@lib/core/router/app_router.dart
|
|
@lib/features/home/presentation/pages/home_page.dart
|
|
@lib/providers/auth_provider.dart
|
|
@lib/main.dart
|
|
</context>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Update Router with Household Routes</name>
|
|
<files>lib/core/router/app_router.dart</files>
|
|
<action>
|
|
Add household routes to existing router structure:
|
|
|
|
1. Import household pages:
|
|
```dart
|
|
import '../features/household/presentation/pages/household_list_page.dart';
|
|
import '../features/household/presentation/pages/create_household_page.dart';
|
|
import '../features/household/presentation/pages/join_household_page.dart';
|
|
import '../features/household/providers/household_provider.dart';
|
|
import '../providers/auth_provider.dart';
|
|
import '../features/household/domain/entities/household_entity.dart';
|
|
```
|
|
|
|
2. Add household route definitions:
|
|
```dart
|
|
// Household routes
|
|
static const householdList = '/households';
|
|
static const createHousehold = '/households/create';
|
|
static const joinHousehold = '/households/join';
|
|
```
|
|
|
|
3. Update router with household routes:
|
|
```dart
|
|
GoRouter router({required String initialLocation}) {
|
|
return GoRouter(
|
|
initialLocation: initialLocation,
|
|
redirect: (context, state) {
|
|
final auth = ref.read(authProvider);
|
|
|
|
// Handle unauthenticated users
|
|
if (auth.user == null) {
|
|
final isAuthRoute = state.location.startsWith('/auth') ||
|
|
state.location == '/';
|
|
if (!isAuthRoute) {
|
|
return '/auth/login';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Authenticated user flow
|
|
final user = auth.user!;
|
|
|
|
// Check if user has households
|
|
final householdsAsync = ref.read(householdProvider);
|
|
final households = householdsAsync.maybeWhen(
|
|
data: (households) => households,
|
|
orElse: () => null,
|
|
);
|
|
|
|
// If households not loaded yet, don't redirect
|
|
if (households == null) return null;
|
|
|
|
// If no households, go to household list to create/join
|
|
if (households.isEmpty) {
|
|
final isHouseholdRoute = state.location.startsWith('/households');
|
|
if (!isHouseholdRoute) {
|
|
return HouseholdListRoute.householdList;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Has households - check if current household is set
|
|
final currentHousehold = ref.read(currentHouseholdProvider);
|
|
if (currentHousehold == null) {
|
|
// Auto-select first household
|
|
ref.read(currentHouseholdProvider.notifier).state = households.first;
|
|
}
|
|
|
|
// Handle specific redirects
|
|
if (state.location == '/' || state.location == '/auth/login') {
|
|
return HomePage.route;
|
|
}
|
|
|
|
return null;
|
|
},
|
|
routes: [
|
|
// Existing auth routes...
|
|
|
|
// Household routes
|
|
GoRoute(
|
|
path: HouseholdListRoute.householdList,
|
|
name: 'household-list',
|
|
builder: (context, state) => const HouseholdListPage(),
|
|
),
|
|
GoRoute(
|
|
path: HouseholdListRoute.createHousehold,
|
|
name: 'create-household',
|
|
builder: (context, state) => const CreateHouseholdPage(),
|
|
),
|
|
GoRoute(
|
|
path: HouseholdListRoute.joinHousehold,
|
|
name: 'join-household',
|
|
builder: (context, state) => const JoinHouseholdPage(),
|
|
),
|
|
|
|
// Home route (existing)
|
|
GoRoute(
|
|
path: HomePage.route,
|
|
name: 'home',
|
|
builder: (context, state) => const HomePage(),
|
|
),
|
|
],
|
|
errorBuilder: (context, state) => Scaffold(
|
|
body: Center(
|
|
child: Text('Page not found: ${state.location}'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
```
|
|
|
|
4. Add household navigation helpers:
|
|
```dart
|
|
class HouseholdListRoute {
|
|
static const householdList = '/households';
|
|
static const createHousehold = '/households/create';
|
|
static const joinHousehold = '/households/join';
|
|
|
|
static void goToList(BuildContext context) {
|
|
GoRouter.of(context).push(householdList);
|
|
}
|
|
|
|
static void goToCreate(BuildContext context) {
|
|
GoRouter.of(context).push(createHousehold);
|
|
}
|
|
|
|
static void goToJoin(BuildContext context) {
|
|
GoRouter.of(context).push(joinHousehold);
|
|
}
|
|
}
|
|
```
|
|
|
|
Update auth redirect logic to handle household selection.
|
|
Include proper route guards for authenticated users with no households.
|
|
</action>
|
|
<verify>flutter analyze lib/core/router/app_router.dart passes</verify>
|
|
<done>Router includes household routes with proper authentication and household state guards</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Update Auth Provider with Household Integration</name>
|
|
<files>lib/providers/auth_provider.dart</files>
|
|
<action>
|
|
Add household loading to auth state changes:
|
|
|
|
1. Import household provider:
|
|
```dart
|
|
import '../features/household/providers/household_provider.dart';
|
|
import '../features/household/providers/household_provider.dart' as household;
|
|
```
|
|
|
|
2. Add household loading to auth state changes:
|
|
```dart
|
|
class AuthProvider extends StateNotifier<AsyncValue<AuthState>> {
|
|
final AuthRepository _repository;
|
|
|
|
AuthProvider(this._repository) : super(const AsyncValue.loading()) {
|
|
_initialize();
|
|
}
|
|
|
|
Future<void> _initialize() async {
|
|
state = const AsyncValue.loading();
|
|
try {
|
|
final currentUser = _repository.currentUser;
|
|
if (currentUser == null) {
|
|
state = const AsyncValue.data(AuthState.unauthenticated());
|
|
return;
|
|
}
|
|
|
|
// User is authenticated, get their session
|
|
final session = await _repository.getCurrentSession();
|
|
if (session == null) {
|
|
state = const AsyncValue.data(AuthState.unauthenticated());
|
|
return;
|
|
}
|
|
|
|
state = AsyncValue.data(AuthState.authenticated(currentUser));
|
|
|
|
// Load user households after successful auth
|
|
_loadUserHouseholds(currentUser.id);
|
|
|
|
} catch (e, stack) {
|
|
state = AsyncValue.error(e, stack);
|
|
}
|
|
}
|
|
|
|
Future<void> signIn(String email, String password) async {
|
|
state = const AsyncValue.loading();
|
|
try {
|
|
final user = await _repository.signIn(email, password);
|
|
state = AsyncValue.data(AuthState.authenticated(user));
|
|
|
|
// Load households after successful sign in
|
|
_loadUserHouseholds(user.id);
|
|
|
|
} catch (e, stack) {
|
|
state = AsyncValue.error(e, stack);
|
|
}
|
|
}
|
|
|
|
Future<void> signUp(String email, String password) async {
|
|
state = const AsyncValue.loading();
|
|
try {
|
|
final user = await _repository.signUp(email, password);
|
|
state = AsyncValue.data(AuthState.authenticated(user));
|
|
|
|
// Load households after successful sign up
|
|
_loadUserHouseholds(user.id);
|
|
|
|
} catch (e, stack) {
|
|
state = AsyncValue.error(e, stack);
|
|
}
|
|
}
|
|
|
|
Future<void> signOut() async {
|
|
try {
|
|
await _repository.signOut();
|
|
state = const AsyncValue.data(AuthState.unauthenticated());
|
|
|
|
// Clear household state
|
|
household.clearHouseholds();
|
|
ref.read(currentHouseholdProvider.notifier).state = null;
|
|
|
|
} catch (e, stack) {
|
|
state = AsyncValue.error(e, stack);
|
|
}
|
|
}
|
|
|
|
Future<void> _loadUserHouseholds(String userId) async {
|
|
try {
|
|
// This will trigger the household provider to load
|
|
final container = ProviderScope.containerOf(context!);
|
|
final householdNotifier = container.read(household.householdProvider.notifier);
|
|
await householdNotifier.loadUserHouseholds(userId);
|
|
} catch (e) {
|
|
// Household loading failure shouldn't break auth flow
|
|
// The household provider will handle its own error states
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
3. Update auth state to include household info:
|
|
```dart
|
|
class AuthState {
|
|
final User? user;
|
|
final bool isAuthenticated;
|
|
final bool hasHouseholds;
|
|
|
|
const AuthState({
|
|
this.user,
|
|
required this.isAuthenticated,
|
|
this.hasHouseholds = false,
|
|
});
|
|
|
|
factory AuthState.unauthenticated() => const AuthState(
|
|
isAuthenticated: false,
|
|
);
|
|
|
|
factory AuthState.authenticated(User user, {bool hasHouseholds = false}) => AuthState(
|
|
user: user,
|
|
isAuthenticated: true,
|
|
hasHouseholds: hasHouseholds,
|
|
);
|
|
|
|
AuthState copyWith({
|
|
User? user,
|
|
bool? isAuthenticated,
|
|
bool? hasHouseholds,
|
|
}) {
|
|
return AuthState(
|
|
user: user ?? this.user,
|
|
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
|
hasHouseholds: hasHouseholds ?? this.hasHouseholds,
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
Ensure household state is synchronized with authentication changes.
|
|
Include proper cleanup on sign out.
|
|
</action>
|
|
<verify>flutter analyze lib/providers/auth_provider.dart passes</verify>
|
|
<done>Auth provider integrates household loading with authentication state changes</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Update Home Page with Household Context</name>
|
|
<files>lib/features/home/presentation/pages/home_page.dart</files>
|
|
<action>
|
|
Add household context and navigation to home page:
|
|
|
|
1. Add imports:
|
|
```dart
|
|
import '../../household/providers/household_provider.dart';
|
|
import '../../household/presentation/pages/household_list_page.dart';
|
|
import '../widgets/household_switcher.dart'; // We'll create this widget
|
|
```
|
|
|
|
2. Add household context to home page:
|
|
```dart
|
|
class HomePage extends ConsumerWidget {
|
|
const HomePage({Key? key}) : super(key: key);
|
|
|
|
static const String route = '/home';
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final user = ref.watch(authProvider).user;
|
|
final currentHousehold = ref.watch(currentHouseholdProvider);
|
|
final householdsAsync = ref.watch(householdProvider);
|
|
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Row(
|
|
children: [
|
|
if (currentHousehold != null) ...[
|
|
CircleAvatar(
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
radius: 16,
|
|
child: Text(
|
|
currentHousehold.name.isNotEmpty
|
|
? currentHousehold.name[0].toUpperCase()
|
|
: 'H',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
currentHousehold.name,
|
|
style: Theme.of(context).textTheme.titleMedium,
|
|
),
|
|
Text(
|
|
'${currentHousehold.members.length} members',
|
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () => _showHouseholdSwitcher(context),
|
|
icon: const Icon(Icons.keyboard_arrow_down),
|
|
tooltip: 'Switch Household',
|
|
),
|
|
] else ...[
|
|
const Text('Sage'),
|
|
],
|
|
],
|
|
),
|
|
actions: [
|
|
if (user != null)
|
|
PopupMenuButton<String>(
|
|
onSelected: (value) {
|
|
switch (value) {
|
|
case 'households':
|
|
HouseholdListRoute.goToList(context);
|
|
break;
|
|
case 'logout':
|
|
_showLogoutConfirmation(context);
|
|
break;
|
|
}
|
|
},
|
|
itemBuilder: (context) => [
|
|
const PopupMenuItem(
|
|
value: 'households',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.home),
|
|
SizedBox(width: 8),
|
|
Text('Manage Households'),
|
|
],
|
|
),
|
|
),
|
|
const PopupMenuItem(
|
|
value: 'logout',
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.logout),
|
|
SizedBox(width: 8),
|
|
Text('Log Out'),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
body: householdsAsync.when(
|
|
loading: () => const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
error: (error, stack) => Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.error,
|
|
size: 64,
|
|
color: Theme.of(context).colorScheme.error,
|
|
),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
'Failed to load households',
|
|
style: Theme.of(context).textTheme.titleLarge,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
error.toString(),
|
|
style: Theme.of(context).textTheme.bodyMedium,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 24),
|
|
ElevatedButton(
|
|
onPressed: () => Navigator.of(context).pushNamed('/households'),
|
|
child: const Text('Manage Households'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
data: (households) {
|
|
if (households.isEmpty) {
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.home_outlined,
|
|
size: 96,
|
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.3),
|
|
),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'No Household Selected',
|
|
style: Theme.of(context).textTheme.headlineSmall,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Create or join a household to start managing your inventory',
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 32),
|
|
ElevatedButton.icon(
|
|
onPressed: () => HouseholdListRoute.goToList(context),
|
|
icon: const Icon(Icons.home),
|
|
label: const Text('Manage Households'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
if (currentHousehold == null) {
|
|
return const Center(
|
|
child: CircularProgressIndicator(),
|
|
);
|
|
}
|
|
|
|
// Normal home page content with inventory
|
|
return _buildInventoryContent(context, ref, currentHousehold);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildInventoryContent(BuildContext context, WidgetRef ref, HouseholdEntity household) {
|
|
// This would be the normal inventory/home content
|
|
// For now, show placeholder
|
|
return Center(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.inventory_2_outlined,
|
|
size: 96,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
const SizedBox(height: 24),
|
|
Text(
|
|
'Welcome to ${household.name}!',
|
|
style: Theme.of(context).textTheme.headlineSmall,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
'Your inventory will appear here once you start adding items',
|
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
|
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 32),
|
|
const Text(
|
|
'Phase 3 will bring barcode scanning and item management',
|
|
style: TextStyle(fontStyle: FontStyle.italic),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showHouseholdSwitcher(BuildContext context) {
|
|
// This would show a bottom sheet or dialog to switch households
|
|
// For now, navigate to household list
|
|
HouseholdListRoute.goToList(context);
|
|
}
|
|
|
|
void _showLogoutConfirmation(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) => AlertDialog(
|
|
title: const Text('Log Out'),
|
|
content: const Text('Are you sure you want to log out?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.of(context).pop(),
|
|
child: const Text('Cancel'),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
ref.read(authProvider.notifier).signOut();
|
|
},
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: Theme.of(context).colorScheme.error,
|
|
),
|
|
child: const Text('Log Out'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
```
|
|
|
|
Add household context display and switching functionality.
|
|
Include proper loading and error states.
|
|
Add navigation to household management.
|
|
</action>
|
|
<verify>flutter analyze lib/features/home/presentation/pages/home_page.dart passes</verify>
|
|
<done>Home page includes household context with navigation to household management</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Update Main Provider Registration</name>
|
|
<files>lib/main.dart</files>
|
|
<action>
|
|
Register household providers in main.dart:
|
|
|
|
1. Add household provider imports:
|
|
```dart
|
|
import 'features/household/providers/household_provider.dart';
|
|
```
|
|
|
|
2. Add household providers to ProviderScope overrides:
|
|
```dart
|
|
ProviderScope(
|
|
overrides: [
|
|
// Auth providers (existing)
|
|
authRepositoryProvider.overrideWithValue(AuthRepositoryImpl(client)),
|
|
authProvider.overrideWithValue(AuthProvider(authRepository)),
|
|
|
|
// Household providers
|
|
householdRemoteDatasourceProvider.overrideWithValue(HouseholdRemoteDatasource(client)),
|
|
householdRepositoryProvider.overrideWithValue(HouseholdRepositoryImpl(householdRemoteDatasource)),
|
|
],
|
|
child: MyApp(),
|
|
)
|
|
```
|
|
|
|
Ensure all household providers are properly initialized with dependencies.
|
|
</action>
|
|
<verify>flutter analyze lib/main.dart passes</verify>
|
|
<done>Main app includes household provider registration with proper dependencies</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<verification>
|
|
1. Router includes household routes with proper authentication and household state guards
|
|
2. Auth provider integrates household loading with authentication state changes
|
|
3. Home page displays current household context with navigation to household management
|
|
4. Household state is properly initialized and synchronized across the app
|
|
5. Navigation flow handles all household states seamlessly
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
Household functionality is fully integrated into app navigation and authentication flow with seamless user experience ready for production.
|
|
</success_criteria>
|
|
|
|
<output>
|
|
After completion, create `.planning/phases/02-household-creation/02-06-SUMMARY.md` with integration completion summary
|
|
</output> |