docs(02): create phase 2 household creation plans

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
This commit is contained in:
Dani B
2026-01-28 15:50:09 -05:00
parent 80c59f1a5f
commit e20858f608
7 changed files with 3023 additions and 0 deletions

View File

@@ -0,0 +1,679 @@
---
phase: 02-household-creation
plan: 05
type: execute
wave: 3
depends_on: [02-04]
files_modified:
- lib/features/household/presentation/pages/join_household_page.dart
- lib/features/household/presentation/pages/household_list_page.dart
autonomous: true
must_haves:
truths:
- "Users can join households using 8-character invite codes with validation"
- "Household list page shows all user households with clear actions"
- "Join flow provides immediate feedback on invalid/expired codes"
- "Household management integrates with navigation and authentication state"
- "All operations handle loading states and errors gracefully"
artifacts:
- path: "lib/features/household/presentation/pages/join_household_page.dart"
provides: "Household joining interface with invite code validation"
contains: "JoinHouseholdPage", "invite code input", "validation feedback"
- path: "lib/features/household/presentation/pages/household_list_page.dart"
provides: "Household overview and management hub"
contains: "HouseholdListPage", "household cards", "create/join actions"
key_links:
- from: "lib/features/household/presentation/pages/join_household_page.dart"
to: "lib/features/household/providers/household_provider.dart"
via: "household joining use case"
pattern: "ref.read.*householdProvider.*joinHousehold"
- from: "lib/features/household/presentation/pages/household_list_page.dart"
to: "lib/features/household/presentation/widgets/household_card.dart"
via: "household display"
pattern: "HouseholdCard.*onTap"
---
<objective>
Complete household management UI with join household flow and household list hub with navigation integration.
Purpose: Provide complete household management experience where users can join existing households and manage all their households from one central location.
Output: Full household management UI ready for navigation integration and user testing.
</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
# UI Pattern References
@lib/features/authentication/presentation/pages/signup_page.dart
@lib/features/authentication/presentation/pages/login_page.dart
@lib/features/authentication/presentation/widgets/auth_button.dart
@lib/features/household/presentation/widgets/household_card.dart
</context>
<tasks>
<task type="auto">
<name>Create Join Household Page</name>
<files>lib/features/household/presentation/pages/join_household_page.dart</files>
<action>
Create join household page with invite code validation:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/services.dart';
import '../../providers/household_provider.dart';
import '../../domain/exceptions/household_exceptions.dart';
import '../../domain/entities/household_entity.dart';
class JoinHouseholdPage extends ConsumerStatefulWidget {
const JoinHouseholdPage({Key? key}) : super(key: key);
@override
ConsumerState<JoinHouseholdPage> createState() => _JoinHouseholdPageState();
}
class _JoinHouseholdPageState extends ConsumerState<JoinHouseholdPage> {
final _formKey = GlobalKey<FormState>();
final _codeController = TextEditingController();
bool _isLoading = false;
String? _error;
HouseholdEntity? _joinedHousehold;
@override
void dispose() {
_codeController.dispose();
super.dispose();
}
Future<void> _joinHousehold() async {
if (!_formKey.currentState!.validate()) return;
setState(() {
_isLoading = true;
_error = null;
});
try {
final user = ref.read(authProvider).user;
if (user == null) throw Exception('User not authenticated');
final household = await ref.read(householdProvider.notifier)
.joinHousehold(_codeController.text.trim(), user.id);
setState(() {
_joinedHousehold = household;
_isLoading = false;
});
// Auto-close after successful join
Future.delayed(const Duration(seconds: 2), () {
if (mounted) {
Navigator.of(context).pop(household);
}
});
} catch (e) {
setState(() {
_error = _getErrorMessage(e);
_isLoading = false;
});
}
}
String _getErrorMessage(dynamic error) {
if (error is HouseholdValidationException) {
return error.message;
} else if (error is HouseholdOperationException) {
return error.message;
} else if (error is InviteCodeException) {
return error.message;
}
return 'Failed to join household. Please check the invite code and try again.';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Join Household'),
backgroundColor: Theme.of(context).colorScheme.surface,
foregroundColor: Theme.of(context).colorScheme.onSurface,
elevation: 0,
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 32),
Icon(
Icons.group_add,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
'Join a Household',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Enter the 8-character invite code from your household member',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface.withOpacity(0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
TextFormField(
controller: _codeController,
decoration: InputDecoration(
labelText: 'Invite Code',
hintText: 'ABCD1234',
prefixIcon: const Icon(Icons.key),
helperText: '8 characters, case-insensitive',
),
textCapitalization: TextCapitalization.characters,
textInputAction: TextInputAction.done,
inputFormatters: [
UpperCaseTextFormatter(),
FilteringTextInputFormatter.allow(RegExp(r'[A-Z0-9]')),
LengthLimitingTextInputFormatter(8),
],
onChanged: (value) {
// Auto-format and move to next field after 8 chars
if (value.length == 8 && !_isLoading) {
_formKey.currentState?.validate();
}
},
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter an invite code';
}
if (value.trim().length != 8) {
return 'Invite code must be 8 characters';
}
return null;
},
onFieldSubmitted: (_) => _joinHousehold(),
),
const SizedBox(height: 24),
if (_error != null)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.error,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(width: 8),
Expanded(
child: Text(
_error!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
),
),
),
],
),
),
if (_joinedHousehold != null)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.check_circle,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Successfully joined!',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
Text(
'You are now a member of ${_joinedHousehold!.name}',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
),
),
],
),
),
if (_joinedHousehold == null) ...[
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _joinHousehold,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Join Household'),
),
const SizedBox(height: 16),
OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Cancel'),
),
],
],
),
),
),
),
);
}
}
// Helper formatter for uppercase input
class UpperCaseTextFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue oldValue,
TextEditingValue newValue,
) {
return TextEditingValue(
text: newValue.text.toUpperCase(),
selection: newValue.selection,
);
}
}
```
Include auto-formatting, validation, and clear success/error feedback.
Follow authentication page patterns for consistency.
</action>
<verify>flutter analyze lib/features/household/presentation/pages/join_household_page.dart passes</verify>
<done>Join household page provides invite code validation with immediate feedback</done>
</task>
<task type="auto">
<name>Create Household List Page</name>
<files>lib/features/household/presentation/pages/household_list_page.dart</files>
<action>
Create household list page as management hub:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../widgets/household_card.dart';
import '../widgets/invite_code_dialog.dart';
import '../../providers/household_provider.dart';
import '../../domain/entities/household_entity.dart';
import '../pages/create_household_page.dart';
import '../pages/join_household_page.dart';
class HouseholdListPage extends ConsumerStatefulWidget {
const HouseholdListPage({Key? key}) : super(key: key);
@override
ConsumerState<HouseholdListPage> createState() => _HouseholdListPageState();
}
class _HouseholdListPageState extends ConsumerState<HouseholdListPage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadHouseholds();
});
}
Future<void> _loadHouseholds() async {
final user = ref.read(authProvider).user;
if (user != null) {
await ref.read(householdProvider.notifier).loadUserHouseholds(user.id);
}
}
Future<void> _showCreateHousehold() async {
final result = await Navigator.of(context).push<HouseholdEntity>(
MaterialPageRoute(
builder: (context) => const CreateHouseholdPage(),
),
);
if (result != null) {
// Household was created, show success message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Welcome to ${result.name}!'),
backgroundColor: Colors.green,
),
);
}
}
}
Future<void> _showJoinHousehold() async {
final result = await Navigator.of(context).push<HouseholdEntity>(
MaterialPageRoute(
builder: (context) => const JoinHouseholdPage(),
),
);
if (result != null) {
// Household was joined, show success message
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('You joined ${result.name}!'),
backgroundColor: Colors.green,
),
);
}
}
}
Future<void> _showInviteDialog(HouseholdEntity household) async {
await showDialog(
context: context,
builder: (context) => InviteCodeDialog(
householdId: household.id,
householdName: household.name,
),
);
}
Future<void> _handleHouseholdTap(HouseholdEntity household) async {
// Set as current household and navigate to home/inventory
ref.read(currentHouseholdProvider.notifier).state = household;
if (mounted) {
// Navigate to inventory/home page
Navigator.of(context).pushReplacementNamed('/home');
}
}
@override
Widget build(BuildContext context) {
final user = ref.watch(authProvider).user;
final householdsAsync = ref.watch(householdProvider);
final currentHousehold = ref.watch(currentHouseholdProvider);
return Scaffold(
appBar: AppBar(
title: const Text('My Households'),
backgroundColor: Theme.of(context).colorScheme.surface,
foregroundColor: Theme.of(context).colorScheme.onSurface,
elevation: 0,
actions: [
IconButton(
onPressed: _loadHouseholds,
icon: const Icon(Icons.refresh),
tooltip: 'Refresh',
),
],
),
body: RefreshIndicator(
onRefresh: _loadHouseholds,
child: 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: _loadHouseholds,
child: const Text('Try Again'),
),
],
),
),
data: (households) {
if (households.isEmpty) {
return _buildEmptyState(context);
}
return Column(
children: [
if (currentHousehold != null)
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(
Icons.home,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Current Household',
style: Theme.of(context).textTheme.labelMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
),
),
Text(
currentHousehold.name,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
),
),
IconButton(
onPressed: () => _handleHouseholdTap(currentHousehold),
icon: const Icon(Icons.arrow_forward),
),
],
),
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.only(bottom: 100),
itemCount: households.length,
itemBuilder: (context, index) {
final household = households[index];
return HouseholdCard(
household: household,
onTap: () => _handleHouseholdTap(household),
onGenerateInvite: household.canInviteMembers()
? () => _showInviteDialog(household)
: null,
onLeave: household.members.length > 1
? () => _showLeaveConfirmation(household)
: null,
showActions: true,
);
},
),
),
],
);
},
),
),
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton.extended(
onPressed: _showCreateHousehold,
icon: const Icon(Icons.add),
label: const Text('Create'),
heroTag: 'create',
),
const SizedBox(height: 12),
FloatingActionButton.extended(
onPressed: _showJoinHousehold,
icon: const Icon(Icons.group_add),
label: const Text('Join'),
heroTag: 'join',
backgroundColor: Theme.of(context).colorScheme.secondary,
),
],
),
);
}
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
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 Households Yet',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
'Create your first household or join an existing one to start sharing 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: _showCreateHousehold,
icon: const Icon(Icons.add),
label: const Text('Create Household'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
const SizedBox(height: 12),
OutlinedButton.icon(
onPressed: _showJoinHousehold,
icon: const Icon(Icons.group_add),
label: const Text('Join Household'),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
),
],
),
),
);
}
Future<void> _showLeaveConfirmation(HouseholdEntity household) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Leave Household'),
content: Text('Are you sure you want to leave ${household.name}?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Leave'),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
),
],
),
);
if (confirmed == true) {
// Handle leaving household
// This would be implemented in the repository/provider
}
}
}
```
Include empty states, loading indicators, error handling, and refresh functionality.
Provide clear navigation to household creation, joining, and inventory management.
</action>
<verify>flutter analyze lib/features/household/presentation/pages/household_list_page.dart passes</verify>
<done>Household list page provides complete management hub with navigation integration</done>
</task>
</tasks>
<verification>
1. Join household page provides invite code validation with auto-formatting and immediate feedback
2. Household list page shows all user households with clear management actions
3. Navigation between create, join, and list flows works seamlessly
4. Empty states and error handling provide clear user guidance
5. All components follow existing authentication UI patterns for consistency
</verification>
<success_criteria>
Household management UI is complete with join flow, list hub, and navigation integration ready for user testing.
</success_criteria>
<output>
After completion, create `.planning/phases/02-household-creation/02-05-SUMMARY.md` with household UI completion summary
</output>