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

@@ -60,6 +60,18 @@ Plans:
**Dependencies:** Phase 1 (AUTH required to create households) **Dependencies:** Phase 1 (AUTH required to create households)
**Plans:** 6 plans in 4 waves
Plans:
- [ ] 02-01-PLAN.md — Household data models, repository pattern, and Supabase integration
- [ ] 02-02-PLAN.md — Database schema with RLS policies and household isolation
- [ ] 02-03-PLAN.md — Household state management and use cases with Riverpod
- [ ] 02-04-PLAN.md — Household UI components (cards, invite dialogs, creation page)
- [ ] 02-05-PLAN.md — Household management UI (join flow, list hub, navigation)
- [ ] 02-06-PLAN.md — Navigation integration and auth state synchronization
**Status:** Pending - Ready for execution
--- ---
### Phase 3: Barcode Scanning & Product Lookup ### Phase 3: Barcode Scanning & Product Lookup

View File

@@ -0,0 +1,266 @@
---
phase: 02-household-creation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- lib/features/household/domain/models/household_models.dart
- lib/features/household/data/datasources/household_remote_datasource.dart
- lib/features/household/data/repositories/household_repository_impl.dart
- lib/features/household/domain/repositories/household_repository.dart
- lib/features/household/domain/entities/household_entity.dart
autonomous: true
must_haves:
truths:
- "Household data model supports creation, membership, and invite code functionality"
- "Repository interface defines household operations without implementation details"
- "Remote datasource handles Supabase household table operations"
- "Entity model separates business logic from data layer"
artifacts:
- path: "lib/features/household/domain/models/household_models.dart"
provides: "Household, HouseholdMember, InviteCode data models"
contains: "class Household", "class HouseholdMember", "class InviteCode"
- path: "lib/features/household/domain/repositories/household_repository.dart"
provides: "HouseholdRepository interface"
contains: "abstract class HouseholdRepository", "createHousehold", "joinHousehold", "generateInviteCode"
- path: "lib/features/household/data/repositories/household_repository_impl.dart"
provides: "Supabase implementation of household operations"
contains: "class HouseholdRepositoryImpl", "implements HouseholdRepository"
- path: "lib/features/household/data/datasources/household_remote_datasource.dart"
provides: "Supabase client wrapper for household operations"
contains: "class HouseholdRemoteDatasource", "SupabaseClient"
- path: "lib/features/household/domain/entities/household_entity.dart"
provides: "Business logic entities for household operations"
contains: "class HouseholdEntity", "class HouseholdMemberEntity"
key_links:
- from: "lib/features/household/data/repositories/household_repository_impl.dart"
to: "lib/features/household/data/datasources/household_remote_datasource.dart"
via: "constructor injection"
pattern: "HouseholdRemoteDatasource.*datasource"
- from: "lib/features/household/data/repositories/household_repository_impl.dart"
to: "lib/features/household/domain/models/household_models.dart"
via: "model mapping"
pattern: "Household.*from.*HouseholdModel"
---
<objective>
Create the foundation for household management with proper data models, repository pattern, and Supabase integration.
Purpose: Establish clean architecture patterns for household operations that support multi-tenant isolation and real-time sync requirements.
Output: Complete data layer with models, repository interface, and Supabase datasource ready for household operations.
</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
# Phase 1 Architecture References
@lib/features/authentication/data/repositories/auth_repository_impl.dart
@lib/features/authentication/domain/repositories/auth_repository.dart
@lib/features/authentication/domain/models/user_models.dart
@lib/features/authentication/domain/entities/user_entity.dart
</context>
<tasks>
<task type="auto">
<name>Create Household Data Models</name>
<files>lib/features/household/domain/models/household_models.dart</files>
<action>
Create household data models following auth model patterns:
1. HouseholdModel with:
- id (UUID)
- name (String, required)
- createdAt (DateTime)
- updatedAt (DateTime)
- createdBy (UUID, references user)
2. HouseholdMemberModel with:
- id (UUID)
- householdId (UUID)
- userId (UUID)
- role (enum: owner, editor, viewer)
- joinedAt (DateTime)
3. InviteCodeModel with:
- id (UUID)
- householdId (UUID)
- code (String, 8 chars, unique)
- createdAt (DateTime)
- expiresAt (DateTime, 30 days from creation)
- createdBy (UUID)
- usedBy (UUID?, nullable)
- usedAt (DateTime?, nullable)
Include fromJson/toJson methods, copyWith methods, and proper null safety.
Reference auth model patterns for consistency.
</action>
<verify>flutter analyze lib/features/household/domain/models/household_models.dart passes</verify>
<done>Household data models compile with proper JSON serialization and validation</done>
</task>
<task type="auto">
<name>Create Household Entity Models</name>
<files>lib/features/household/domain/entities/household_entity.dart</files>
<action>
Create entity models separating business logic from data persistence:
1. HouseholdEntity with:
- Core properties (same as model but without JSON methods)
- List<HouseholdMemberEntity> members
- InviteCode? currentInvite (if user is owner)
- bool isCurrentUserOwner
2. HouseholdMemberEntity with:
- id, householdId, userId, role, joinedAt
- User? user (optional populated user data)
- bool isCurrentUser
Include business logic methods:
- canInviteMembers() (owner/editor only)
- canRemoveMember() (owner only, can't remove self if last owner)
- getRoleDisplayName()
Follow auth entity patterns for consistency.
</action>
<verify>flutter analyze lib/features/household/domain/entities/household_entity.dart passes</verify>
<done>Household entities compile with business logic methods and proper separation from data models</done>
</task>
<task type="auto">
<name>Create Household Repository Interface</name>
<files>lib/features/household/domain/repositories/household_repository.dart</files>
<action>
Create abstract repository defining household operations:
abstract class HouseholdRepository {
Future<HouseholdEntity> createHousehold(String name, String userId);
Future<List<HouseholdEntity>> getUserHouseholds(String userId);
Future<HouseholdEntity?> getHouseholdById(String householdId);
Future<InviteCodeModel> generateInviteCode(String householdId, String createdBy);
Future<HouseholdEntity> joinHousehold(String inviteCode, String userId);
Future<void> leaveHousehold(String householdId, String userId);
Future<void> removeMember(String householdId, String memberUserId);
Future<void> updateMemberRole(String householdId, String memberUserId, HouseholdRole role);
Future<List<HouseholdMemberEntity>> getHouseholdMembers(String householdId);
Future<InviteCodeModel?> getActiveInviteCode(String householdId);
Future<void> revokeInviteCode(String inviteCodeId);
}
Include Household enum:
enum HouseholdRole { owner, editor, viewer }
Follow auth repository interface patterns.
</action>
<verify>flutter analyze lib/features/household/domain/repositories/household_repository.dart passes</verify>
<done>Household repository interface defines all required operations with proper typing</done>
</task>
<task type="auto">
<name>Create Household Remote Datasource</name>
<files>lib/features/household/data/datasources/household_remote_datasource.dart</files>
<action>
Create Supabase datasource implementing low-level data operations:
class HouseholdRemoteDatasource {
final SupabaseClient client;
HouseholdRemoteDatasource(this.client);
// Core CRUD operations
Future<Map<String, dynamic>> createHousehold(Map<String, dynamic> data);
Future<List<Map<String, dynamic>>> getUserHouseholds(String userId);
Future<Map<String, dynamic>?> getHouseholdById(String householdId);
Future<Map<String, dynamic>> generateInviteCode(Map<String, dynamic> data);
Future<Map<String, dynamic>> joinHousehold(String inviteCode, String userId);
Future<void> leaveHousehold(String householdId, String userId);
// Member operations
Future<List<Map<String, dynamic>>> getHouseholdMembers(String householdId);
Future<void> removeMember(String householdId, String memberUserId);
Future<void> updateMemberRole(String householdId, String memberUserId, String role);
// Invite operations
Future<Map<String, dynamic>?> getActiveInviteCode(String householdId);
Future<void> revokeInviteCode(String inviteCodeId);
}
Use Supabase table operations: .from('households'), .from('household_members'), .from('invite_codes').
Include error handling for Supabase exceptions.
</action>
<verify>flutter analyze lib/features/household/data/datasources/household_remote_datasource.dart passes</verify>
<done>Household remote datasource provides low-level Supabase operations with proper error handling</done>
</task>
<task type="auto">
<name>Create Household Repository Implementation</name>
<files>lib/features/household/data/repositories/household_repository_impl.dart</files>
<action>
Create repository implementation using datasource and models:
class HouseholdRepositoryImpl implements HouseholdRepository {
final HouseholdRemoteDatasource datasource;
HouseholdRepositoryImpl(this.datasource);
@override
Future<HouseholdEntity> createHousehold(String name, String userId) async {
// Create household record
// Add owner as member
// Return populated entity
}
@override
Future<InviteCodeModel> generateInviteCode(String householdId, String createdBy) async {
// Generate 8-character unique code
// Check for collisions
// Set expiry to 30 days
// Revoke any existing codes
}
@override
Future<HouseholdEntity> joinHousehold(String inviteCode, String userId) async {
// Validate invite code
// Check expiry
// Add member with 'editor' role
// Mark invite as used
// Return household entity
}
// Implement all other interface methods...
}
Include invite code generation logic (8 chars, alphanumeric, collision checking).
Handle business rules (only owners can generate invites, can't leave if last owner, etc.).
Map between models and entities with proper validation.
</action>
<verify>flutter analyze lib/features/household/data/repositories/household_repository_impl.dart passes</verify>
<done>Household repository implementation provides business logic and data mapping with validation</done>
</task>
</tasks>
<verification>
1. All models compile with proper JSON serialization and null safety
2. Repository interface defines all required operations from SHARE-01 through SHARE-05
3. Remote datasource uses Supabase client for household table operations
4. Repository implementation includes business rules for invite codes and member management
5. Data layer follows clean architecture with proper separation of concerns
</verification>
<success_criteria>
Household data layer is complete with models, repository interface, and Supabase implementation ready for UI integration and invite code generation.
</success_criteria>
<output>
After completion, create `.planning/phases/02-household-creation/02-01-SUMMARY.md` with household data layer implementation summary
</output>

View File

@@ -0,0 +1,433 @@
---
phase: 02-household-creation
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- lib/features/household/data/datasources/household_remote_datasource.dart
- lib/features/household/domain/repositories/household_repository.dart
- lib/features/household/data/repositories/household_repository_impl.dart
- lib/features/household/domain/models/household_models.dart
- lib/features/household/domain/entities/household_entity.dart
autonomous: true
must_haves:
truths:
- "Supabase database schema supports households, members, and invite codes with proper relationships"
- "Row-Level Security policies isolate household data at database layer"
- "Database constraints prevent duplicate members and invalid invite codes"
- "Realtime subscriptions enable household member sync"
artifacts:
- path: "supabase/migrations/001_create_household_tables.sql"
provides: "Database schema for household functionality"
contains: "CREATE TABLE households", "CREATE TABLE household_members", "CREATE TABLE invite_codes"
- path: "supabase/migrations/002_household_rls_policies.sql"
provides: "Row-Level Security policies for household data isolation"
contains: "CREATE POLICY", "ROW LEVEL SECURITY"
- path: "supabase/migrations/003_household_indexes.sql"
provides: "Performance indexes for household queries"
contains: "CREATE INDEX", "household_id", "user_id"
key_links:
- from: "lib/features/household/data/repositories/household_repository_impl.dart"
to: "supabase/migrations/001_create_household_tables.sql"
via: "table operations"
pattern: "from.*households"
- from: "supabase/migrations/002_household_rls_policies.sql"
to: "supabase/migrations/001_create_household_tables.sql"
via: "security policies"
pattern: "POLICY.*FOR.*households"
---
<objective>
Create Supabase database schema with proper household isolation, member relationships, and invite code management.
Purpose: Establish database foundation that enforces multi-tenant security and supports real-time household synchronization requirements.
Output: Complete database schema with tables, RLS policies, and indexes for household operations.
</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
@.planning/research/ARCHITECTURE.md
# Database Architecture References
@lib/features/authentication/data/repositories/auth_repository_impl.dart
</context>
<tasks>
<task type="auto">
<name>Create Household Tables Migration</name>
<files>supabase/migrations/001_create_household_tables.sql</files>
<action>
Create database schema for household functionality:
1. households table:
```sql
CREATE TABLE households (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
name TEXT NOT NULL CHECK (length(name) >= 1 AND length(name) <= 100),
created_by UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT now() NOT NULL
);
CREATE TRIGGER households_updated_at
BEFORE UPDATE ON households
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
```
2. household_members table:
```sql
CREATE TABLE household_members (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
household_id UUID NOT NULL REFERENCES households(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
role TEXT NOT NULL CHECK (role IN ('owner', 'editor', 'viewer')),
joined_at TIMESTAMPTZ DEFAULT now() NOT NULL,
UNIQUE(household_id, user_id)
);
```
3. invite_codes table:
```sql
CREATE TABLE invite_codes (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
household_id UUID NOT NULL REFERENCES households(id) ON DELETE CASCADE,
code TEXT NOT NULL UNIQUE CHECK (length(code) = 8),
created_by UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ DEFAULT now() NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
used_by UUID REFERENCES auth.users(id) ON DELETE SET NULL,
used_at TIMESTAMPTZ,
CONSTRAINT valid_invite_code CHECK (
(used_by IS NULL AND used_at IS NULL) OR
(used_by IS NOT NULL AND used_at IS NOT NULL)
)
);
```
Follow Supabase migration patterns and include proper constraints.
</action>
<verify>supabase db push --dry-run shows valid SQL syntax</verify>
<done>Household tables created with proper relationships and constraints</done>
</task>
<task type="auto">
<name>Create Household RLS Policies</name>
<files>supabase/migrations/002_household_rls_policies.sql</files>
<action>
Create Row-Level Security policies for household data isolation:
1. Enable RLS on all household tables:
```sql
ALTER TABLE households ENABLE ROW LEVEL SECURITY;
ALTER TABLE household_members ENABLE ROW LEVEL SECURITY;
ALTER TABLE invite_codes ENABLE ROW LEVEL SECURITY;
```
2. households table policies:
```sql
-- Users can view households they belong to
CREATE POLICY "Users can view their households" ON households
FOR SELECT USING (
id IN (
SELECT household_id FROM household_members
WHERE user_id = auth.uid()
)
);
-- Users can insert households (will be owner via member insertion)
CREATE POLICY "Users can create households" ON households
FOR INSERT WITH CHECK (created_by = auth.uid());
-- Only household owners can update households
CREATE POLICY "Owners can update households" ON households
FOR UPDATE USING (
id IN (
SELECT household_id FROM household_members
WHERE user_id = auth.uid() AND role = 'owner'
)
);
```
3. household_members table policies:
```sql
-- Users can view members of their households
CREATE POLICY "Users can view household members" ON household_members
FOR SELECT USING (
household_id IN (
SELECT household_id FROM household_members
WHERE user_id = auth.uid()
)
);
-- Users can insert themselves as members (via join flow)
CREATE POLICY "Users can join households" ON household_members
FOR INSERT WITH CHECK (user_id = auth.uid());
-- Only owners can manage member roles and remove members
CREATE POLICY "Owners can manage members" ON household_members
FOR UPDATE USING (
household_id IN (
SELECT household_id FROM household_members
WHERE user_id = auth.uid() AND role = 'owner'
)
);
-- Only owners can remove members (except themselves if last owner handled in app logic)
CREATE POLICY "Owners can remove members" ON household_members
FOR DELETE USING (
household_id IN (
SELECT household_id FROM household_members
WHERE user_id = auth.uid() AND role = 'owner'
)
);
```
4. invite_codes table policies:
```sql
-- Users can view invite codes for their households
CREATE POLICY "Users can view household invite codes" ON invite_codes
FOR SELECT USING (
household_id IN (
SELECT household_id FROM household_members
WHERE user_id = auth.uid()
)
);
-- Only owners can create invite codes
CREATE POLICY "Owners can create invite codes" ON invite_codes
FOR INSERT WITH CHECK (
created_by = auth.uid() AND
household_id IN (
SELECT household_id FROM household_members
WHERE user_id = auth.uid() AND role = 'owner'
)
);
-- Only owners can revoke invite codes
CREATE POLICY "Owners can revoke invite codes" ON invite_codes
FOR DELETE USING (
created_by = auth.uid()
);
```
Ensure policies enforce multi-tenant isolation at database layer.
</action>
<verify>supabase db push --dry-run shows valid RLS policies</verify>
<done>Row-Level Security policies enforce household data isolation with proper role-based access</done>
</task>
<task type="auto">
<name>Create Household Performance Indexes</name>
<files>supabase/migrations/003_household_indexes.sql</files>
<action>
Create performance indexes for household operations:
1. households table indexes:
```sql
-- For finding households by creator
CREATE INDEX idx_households_created_by ON households(created_by);
-- For household sorting by creation date
CREATE INDEX idx_households_created_at ON households(created_at);
```
2. household_members table indexes:
```sql
-- For finding user's households (primary query)
CREATE INDEX idx_household_members_user_id ON household_members(user_id);
-- For finding household members (primary query)
CREATE INDEX idx_household_members_household_id ON household_members(household_id);
-- Composite index for member role queries
CREATE INDEX idx_household_members_household_role ON household_members(household_id, role);
-- For checking if user is member of household
CREATE UNIQUE INDEX idx_household_members_unique ON household_members(household_id, user_id);
```
3. invite_codes table indexes:
```sql
-- For invite code lookup (primary query)
CREATE UNIQUE INDEX idx_invite_codes_code ON invite_codes(code);
-- For finding household's active invites
CREATE INDEX idx_invite_codes_household_id ON invite_codes(household_id);
-- For cleaning expired invites
CREATE INDEX idx_invite_codes_expires_at ON invite_codes(expires_at);
-- For finding unused invites
CREATE INDEX idx_invite_codes_unused ON invite_codes(used_by) WHERE used_by IS NULL;
```
4. Realtime function for household changes:
```sql
-- Function to broadcast household member changes
CREATE OR REPLACE FUNCTION broadcast_household_change()
RETURNS TRIGGER AS $$
BEGIN
-- This will be used by Supabase Realtime
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Trigger for household member changes
CREATE TRIGGER household_members_broadcast
AFTER INSERT OR UPDATE OR DELETE ON household_members
FOR EACH ROW EXECUTE FUNCTION broadcast_household_change();
```
Include indexes that support the most common household queries and real-time sync.
</action>
<verify>supabase db push --dry-run shows valid index creation</verify>
<done>Performance indexes optimize household queries and support real-time synchronization</done>
</task>
<task type="auto">
<name>Create Household Database Functions</name>
<files>supabase/migrations/004_household_functions.sql</files>
<action>
Create database functions for household operations:
1. Generate unique invite code function:
```sql
CREATE OR REPLACE FUNCTION generate_invite_code()
RETURNS TEXT AS $$
DECLARE
new_code TEXT;
code_exists BOOLEAN;
BEGIN
LOOP
-- Generate 8-character alphanumeric code
new_code := upper(substring(encode(gen_random_bytes(4), 'hex'), 1, 8));
-- Check for collision
SELECT EXISTS(SELECT 1 FROM invite_codes WHERE code = new_code AND used_by IS NULL) INTO code_exists;
EXIT WHEN NOT code_exists;
END LOOP;
RETURN new_code;
END;
$$ LANGUAGE plpgsql;
```
2. Get user's households with member info:
```sql
CREATE OR REPLACE FUNCTION get_user_households(user_uuid UUID)
RETURNS TABLE (
household_id UUID,
household_name TEXT,
user_role TEXT,
member_count BIGINT,
created_at TIMESTAMPTZ
) AS $$
BEGIN
RETURN QUERY
SELECT
h.id,
h.name,
hm.role,
(SELECT COUNT(*) FROM household_members WHERE household_id = h.id),
h.created_at
FROM households h
JOIN household_members hm ON h.id = hm.household_id
WHERE hm.user_id = user_uuid
ORDER BY h.created_at DESC;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```
3. Validate and consume invite code:
```sql
CREATE OR REPLACE FUNCTION join_household_by_code(
invite_code TEXT,
user_uuid UUID
)
RETURNS TABLE (
success BOOLEAN,
household_id UUID,
household_name TEXT,
error_message TEXT
) AS $$
DECLARE
invite_record RECORD;
household_record RECORD;
is_already_member BOOLEAN;
BEGIN
-- Find valid invite code
SELECT ic.*, h.name as household_name INTO invite_record
FROM invite_codes ic
JOIN households h ON ic.household_id = h.id
WHERE ic.code = upper(invite_code)
AND ic.used_by IS NULL
AND ic.expires_at > now();
IF NOT FOUND THEN
RETURN QUERY SELECT false, NULL::UUID, NULL::TEXT, 'Invalid or expired invite code'::TEXT;
RETURN;
END IF;
-- Check if already member
SELECT EXISTS(
SELECT 1 FROM household_members
WHERE household_id = invite_record.household_id AND user_id = user_uuid
) INTO is_already_member;
IF is_already_member THEN
RETURN QUERY SELECT false, NULL::UUID, NULL::TEXT, 'Already a member of this household'::TEXT;
RETURN;
END IF;
-- Add as member with editor role
INSERT INTO household_members (household_id, user_id, role)
VALUES (invite_record.household_id, user_uuid, 'editor');
-- Mark invite as used
UPDATE invite_codes
SET used_by = user_uuid, used_at = now()
WHERE id = invite_record.id;
-- Get household info
SELECT id, name INTO household_record
FROM households WHERE id = invite_record.household_id;
RETURN QUERY SELECT true, household_record.id, household_record.name, NULL::TEXT;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```
Include proper SECURITY DEFINER settings and error handling.
</action>
<verify>supabase db push --dry-run shows valid function creation</verify>
<done>Database functions provide efficient household operations with proper security</done>
</task>
</tasks>
<verification>
1. Database schema supports all household operations from SHARE-01 through SHARE-05
2. RLS policies enforce multi-tenant isolation at database layer
3. Performance indexes optimize common household queries
4. Database functions provide efficient invite code generation and validation
5. Realtime triggers support household member synchronization
</verification>
<success_criteria>
Supabase database schema is complete with household tables, security policies, indexes, and functions ready for application integration.
</success_criteria>
<output>
After completion, create `.planning/phases/02-household-creation/02-02-SUMMARY.md` with database schema implementation summary
</output>

View File

@@ -0,0 +1,367 @@
---
phase: 02-household-creation
plan: 03
type: execute
wave: 2
depends_on: [02-01]
files_modified:
- lib/features/household/providers/household_provider.dart
- lib/features/household/domain/usecases/create_household_usecase.dart
- lib/features/household/domain/usecases/generate_invite_code_usecase.dart
- lib/features/household/domain/usecases/join_household_usecase.dart
- lib/features/household/domain/usecases/get_user_households_usecase.dart
autonomous: true
must_haves:
truths:
- "Household state management provides reactive updates across the app"
- "Use cases encapsulate business logic for household operations"
- "Provider integrates with existing authentication state"
- "Household operations handle loading states and errors properly"
artifacts:
- path: "lib/features/household/providers/household_provider.dart"
provides: "Global household state management"
contains: "class HouseholdProvider", "Riverpod", "AsyncValue"
- path: "lib/features/household/domain/usecases/create_household_usecase.dart"
provides: "Household creation business logic"
contains: "class CreateHouseholdUseCase", "Future<HouseholdEntity>"
- path: "lib/features/household/domain/usecases/join_household_usecase.dart"
provides: "Household joining logic with validation"
contains: "class JoinHouseholdUseCase", "invite code validation"
- path: "lib/features/household/domain/usecases/get_user_households_usecase.dart"
provides: "User household retrieval logic"
contains: "class GetUserHouseholdsUseCase", "List<HouseholdEntity>"
key_links:
- from: "lib/features/household/providers/household_provider.dart"
to: "lib/features/household/domain/usecases/create_household_usecase.dart"
via: "dependency injection"
pattern: "CreateHouseholdUseCase.*createHouseholdUseCase"
- from: "lib/features/household/providers/household_provider.dart"
to: "lib/providers/auth_provider.dart"
via: "authentication state"
pattern: "ref.watch.*authProvider"
---
<objective>
Implement household state management and business logic use cases following clean architecture patterns.
Purpose: Provide reactive household state that integrates with authentication and handles all household operations with proper error handling.
Output: Complete state management layer with use cases and Riverpod provider ready for UI integration.
</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
# State Management References
@lib/providers/auth_provider.dart
@lib/features/authentication/data/repositories/auth_repository_impl.dart
</context>
<tasks>
<task type="auto">
<name>Create Household Use Cases</name>
<files>
lib/features/household/domain/usecases/create_household_usecase.dart
lib/features/household/domain/usecases/generate_invite_code_usecase.dart
lib/features/household/domain/usecases/join_household_usecase.dart
lib/features/household/domain/usecases/get_user_households_usecase.dart
</files>
<action>
Create use cases for household operations following clean architecture:
1. CreateHouseholdUseCase (create_household_usecase.dart):
```dart
class CreateHouseholdUseCase {
final HouseholdRepository repository;
CreateHouseholdUseCase(this.repository);
Future<HouseholdEntity> call(String name, String userId) async {
if (name.trim().isEmpty) {
throw HouseholdValidationException('Household name cannot be empty');
}
if (name.length > 100) {
throw HouseholdValidationException('Household name too long (max 100 characters)');
}
return await repository.createHousehold(name.trim(), userId);
}
}
```
2. GenerateInviteCodeUseCase (generate_invite_code_usecase.dart):
```dart
class GenerateInviteCodeUseCase {
final HouseholdRepository repository;
GenerateInviteCodeUseCase(this.repository);
Future<InviteCodeModel> call(String householdId, String userId) async {
// Check if user is owner (will be validated in repository)
return await repository.generateInviteCode(householdId, userId);
}
}
```
3. JoinHouseholdUseCase (join_household_usecase.dart):
```dart
class JoinHouseholdUseCase {
final HouseholdRepository repository;
JoinHouseholdUseCase(this.repository);
Future<HouseholdEntity> call(String inviteCode, String userId) async {
if (inviteCode.trim().isEmpty) {
throw HouseholdValidationException('Invite code cannot be empty');
}
if (inviteCode.length != 8) {
throw HouseholdValidationException('Invite code must be 8 characters');
}
return await repository.joinHousehold(inviteCode.trim().toUpperCase(), userId);
}
}
```
4. GetUserHouseholdsUseCase (get_user_households_usecase.dart):
```dart
class GetUserHouseholdsUseCase {
final HouseholdRepository repository;
GetUserHouseholdsUseCase(this.repository);
Future<List<HouseholdEntity>> call(String userId) async {
return await repository.getUserHouseholds(userId);
}
}
```
Include proper validation and business logic in each use case.
Create custom exception classes for household operations.
</action>
<verify>flutter analyze lib/features/household/domain/usecases/*.dart passes</verify>
<done>Household use cases encapsulate business logic with proper validation</done>
</task>
<task type="auto">
<name>Create Household Provider</name>
<files>lib/features/household/providers/household_provider.dart</files>
<action>
Create Riverpod provider for household state management:
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Use case providers
final createHouseholdUseCaseProvider = Provider<CreateHouseholdUseCase>((ref) {
final repository = ref.watch(householdRepositoryProvider);
return CreateHouseholdUseCase(repository);
});
final generateInviteCodeUseCaseProvider = Provider<GenerateInviteCodeUseCase>((ref) {
final repository = ref.watch(householdRepositoryProvider);
return GenerateInviteCodeUseCase(repository);
});
final joinHouseholdUseCaseProvider = Provider<JoinHouseholdUseCase>((ref) {
final repository = ref.watch(householdRepositoryProvider);
return JoinHouseholdUseCase(repository);
});
final getUserHouseholdsUseCaseProvider = Provider<GetUserHouseholdsUseCase>((ref) {
final repository = ref.watch(householdRepositoryProvider);
return GetUserHouseholdsUseCase(repository);
});
// Main household provider
class HouseholdProvider extends StateNotifier<AsyncValue<List<HouseholdEntity>>> {
final GetUserHouseholdsUseCase _getUserHouseholdsUseCase;
final CreateHouseholdUseCase _createHouseholdUseCase;
final JoinHouseholdUseCase _joinHouseholdUseCase;
final GenerateInviteCodeUseCase _generateInviteCodeUseCase;
HouseholdProvider(
this._getUserHouseholdsUseCase,
this._createHouseholdUseCase,
this._joinHouseholdUseCase,
this._generateInviteCodeUseCase,
) : super(const AsyncValue.loading()) {
_initialize();
}
Future<void> _initialize() async {
state = const AsyncValue.loading();
try {
// Will be called when auth state is available
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
Future<void> loadUserHouseholds(String userId) async {
state = const AsyncValue.loading();
try {
final households = await _getUserHouseholdsUseCase(userId);
state = AsyncValue.data(households);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
Future<HouseholdEntity> createHousehold(String name, String userId) async {
try {
final household = await _createHouseholdUseCase(name, userId);
// Refresh the list
await loadUserHouseholds(userId);
return household;
} catch (e) {
throw HouseholdOperationException('Failed to create household: $e');
}
}
Future<HouseholdEntity> joinHousehold(String inviteCode, String userId) async {
try {
final household = await _joinHouseholdUseCase(inviteCode, userId);
// Refresh the list
await loadUserHouseholds(userId);
return household;
} catch (e) {
throw HouseholdOperationException('Failed to join household: $e');
}
}
Future<InviteCodeModel> generateInviteCode(String householdId, String userId) async {
try {
return await _generateInviteCodeUseCase(householdId, userId);
} catch (e) {
throw HouseholdOperationException('Failed to generate invite code: $e');
}
}
void clearHouseholds() {
state = const AsyncValue.data([]);
}
}
// Provider instance
final householdProvider = StateNotifierProvider<HouseholdProvider, AsyncValue<List<HouseholdEntity>>>((ref) {
return HouseholdProvider(
ref.read(getUserHouseholdsUseCaseProvider),
ref.read(createHouseholdUseCaseProvider),
ref.read(joinHouseholdUseCaseProvider),
ref.read(generateInviteCodeUseCaseProvider),
);
});
// Current selected household provider
final currentHouseholdProvider = StateProvider<HouseholdEntity?>((ref) => null);
// Repository provider (to be added to main providers file)
final householdRepositoryProvider = Provider<HouseholdRepository>((ref) {
final datasource = ref.read(householdRemoteDatasourceProvider);
return HouseholdRepositoryImpl(datasource);
});
final householdRemoteDatasourceProvider = Provider<HouseholdRemoteDatasource>((ref) {
final client = ref.read(supabaseClientProvider);
return HouseholdRemoteDatasource(client);
});
```
Follow auth provider patterns for consistency.
Include proper error handling and state transitions.
</action>
<verify>flutter analyze lib/features/household/providers/household_provider.dart passes</verify>
<done>Household provider provides reactive state management with proper integration</done>
</task>
<task type="auto">
<name>Create Household Exception Classes</name>
<files>lib/features/household/domain/exceptions/household_exceptions.dart</files>
<action>
Create custom exception classes for household operations:
```dart
// Base household exception
abstract class HouseholdException implements Exception {
final String message;
const HouseholdException(this.message);
@override
String toString() => 'HouseholdException: $message';
}
// Validation exceptions
class HouseholdValidationException extends HouseholdException {
const HouseholdValidationException(String message) : super(message);
@override
String toString() => 'HouseholdValidationException: $message';
}
// Operation exceptions
class HouseholdOperationException extends HouseholdException {
const HouseholdOperationException(String message) : super(message);
@override
String toString() => 'HouseholdOperationException: $message';
}
// Specific household exceptions
class HouseholdNotFoundException extends HouseholdException {
const HouseholdNotFoundException(String message) : super(message);
@override
String toString() => 'HouseholdNotFoundException: $message';
}
class InviteCodeException extends HouseholdException {
const InviteCodeException(String message) : super(message);
@override
String toString() => 'InviteCodeException: $message';
}
class HouseholdMembershipException extends HouseholdException {
const HouseholdMembershipException(String message) : super(message);
@override
String toString() => 'HouseholdMembershipException: $message';
}
```
Follow authentication exception patterns for consistency.
</action>
<verify>flutter analyze lib/features/household/domain/exceptions/household_exceptions.dart passes</verify>
<done>Household exception classes provide clear error categorization</done>
</task>
</tasks>
<verification>
1. Use cases encapsulate business logic with proper validation
2. Household provider provides reactive state management following Riverpod patterns
3. Exception classes provide clear error categorization
4. Integration with authentication state is properly structured
5. All household operations handle loading states and errors appropriately
</verification>
<success_criteria>
Household state management is complete with use cases, provider, and exception handling ready for UI integration.
</success_criteria>
<output>
After completion, create `.planning/phases/02-household-creation/02-03-SUMMARY.md` with state management implementation summary
</output>

View File

@@ -0,0 +1,590 @@
---
phase: 02-household-creation
plan: 04
type: execute
wave: 3
depends_on: [02-03, 02-01]
files_modified:
- lib/features/household/presentation/pages/create_household_page.dart
- lib/features/household/presentation/pages/join_household_page.dart
- lib/features/household/presentation/pages/household_list_page.dart
- lib/features/household/presentation/widgets/invite_code_dialog.dart
- lib/features/household/presentation/widgets/household_card.dart
autonomous: true
must_haves:
truths:
- "Users can create households with validation and feedback"
- "Users can join households using 8-character invite codes"
- "Household management UI provides clear navigation and state feedback"
- "Invite code generation works with proper security and expiration"
- "All household operations handle loading states and errors gracefully"
artifacts:
- path: "lib/features/household/presentation/pages/create_household_page.dart"
provides: "Household creation interface"
contains: "CreateHouseholdPage", "form validation", "create button"
- path: "lib/features/household/presentation/pages/join_household_page.dart"
provides: "Household joining interface with invite code input"
contains: "JoinHouseholdPage", "invite code field", "join button"
- path: "lib/features/household/presentation/pages/household_list_page.dart"
provides: "Household overview and management"
contains: "HouseholdListPage", "household cards", "create/join buttons"
- path: "lib/features/household/presentation/widgets/invite_code_dialog.dart"
provides: "Invite code generation and display"
contains: "InviteCodeDialog", "code display", "share functionality"
- path: "lib/features/household/presentation/widgets/household_card.dart"
provides: "Household display component"
contains: "HouseholdCard", "member count", "role display"
key_links:
- from: "lib/features/household/presentation/pages/create_household_page.dart"
to: "lib/features/household/providers/household_provider.dart"
via: "Riverpod consumer"
pattern: "ref.read.*householdProvider"
- from: "lib/features/household/presentation/pages/household_list_page.dart"
to: "lib/providers/auth_provider.dart"
via: "authentication state"
pattern: "ref.watch.*authProvider"
---
<objective>
Create household management UI components for creating, joining, and managing households with proper validation and user feedback.
Purpose: Provide intuitive interfaces for all household operations from SHARE-01 through SHARE-05 with consistent UX and error handling.
Output: Complete household management UI ready for integration with navigation system.
</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
</context>
<tasks>
<task type="auto">
<name>Create Household Card Widget</name>
<files>lib/features/household/presentation/widgets/household_card.dart</files>
<action>
Create household card component following auth UI patterns:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../domain/entities/household_entity.dart';
class HouseholdCard extends ConsumerWidget {
final HouseholdEntity household;
final VoidCallback? onTap;
final VoidCallback? onGenerateInvite;
final VoidCallback? onLeave;
final bool showActions;
const HouseholdCard({
Key? key,
required this.household,
this.onTap,
this.onGenerateInvite,
this.onLeave,
this.showActions = true,
}) : super(key: key);
@override
Widget build(BuildContext context, WidgetRef ref) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: CircleAvatar(
backgroundColor: Theme.of(context).colorScheme.primary,
child: Text(
household.name.isNotEmpty ? household.name[0].toUpperCase() : 'H',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
title: Text(
household.name,
style: Theme.of(context).textTheme.titleMedium,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text('${household.members.length} member${household.members.length != 1 ? 's' : ''}'),
if (household.currentInvite != null)
Text(
'Invite active',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
fontSize: 12,
),
),
],
),
trailing: showActions ? _buildActions(context) : null,
onTap: onTap,
),
);
}
Widget _buildActions(BuildContext context) {
return PopupMenuButton<String>(
onSelected: (value) {
switch (value) {
case 'invite':
onGenerateInvite?.call();
break;
case 'leave':
onLeave?.call();
break;
}
},
itemBuilder: (context) => [
if (household.isCurrentUserOwner || household.members.any(m => m.isCurrentUser && m.role.name != 'viewer'))
const PopupMenuItem(
value: 'invite',
child: Row(
children: [
Icon(Icons.person_add),
SizedBox(width: 8),
Text('Generate Invite'),
],
),
),
if (!household.isCurrentUserOwner || household.members.length > 1)
const PopupMenuItem(
value: 'leave',
child: Row(
children: [
Icon(Icons.exit_to_app),
SizedBox(width: 8),
Text('Leave Household'),
],
),
),
],
);
}
}
```
Follow Material Design card patterns and authentication UI consistency.
Include role-based action visibility (only owners/editors can generate invites).
</action>
<verify>flutter analyze lib/features/household/presentation/widgets/household_card.dart passes</verify>
<done>Household card component displays household information with appropriate actions</done>
</task>
<task type="auto">
<name>Create Invite Code Dialog</name>
<files>lib/features/household/presentation/widgets/invite_code_dialog.dart</files>
<action>
Create dialog for generating and sharing invite codes:
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter/services.dart';
import '../../domain/models/household_models.dart';
import '../../providers/household_provider.dart';
class InviteCodeDialog extends ConsumerStatefulWidget {
final String householdId;
final String householdName;
const InviteCodeDialog({
Key? key,
required this.householdId,
required this.householdName,
}) : super(key: key);
@override
ConsumerState<InviteCodeDialog> createState() => _InviteCodeDialogState();
}
class _InviteCodeDialogState extends ConsumerState<InviteCodeDialog> {
InviteCodeModel? _inviteCode;
bool _isLoading = false;
String? _error;
@override
void initState() {
super.initState();
_generateInviteCode();
}
Future<void> _generateInviteCode() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final user = ref.read(authProvider).user;
if (user == null) throw Exception('User not authenticated');
final inviteCode = await ref.read(householdProvider.notifier)
.generateInviteCode(widget.householdId, user.id);
setState(() {
_inviteCode = inviteCode;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Invite to ${widget.householdName}'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (_isLoading)
const Column(
children: [
CircularProgressIndicator(),
SizedBox(height: 16),
Text('Generating invite code...'),
],
)
else if (_error != null)
Column(
children: [
Icon(
Icons.error,
color: Theme.of(context).colorScheme.error,
size: 48,
),
const SizedBox(height: 16),
Text(
'Failed to generate invite code',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 8),
Text(_error!),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _generateInviteCode,
child: const Text('Try Again'),
),
],
)
else if (_inviteCode != null)
Column(
children: [
Text(
'Share this code with household members:',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceVariant,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Text(
_inviteCode!.code,
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
letterSpacing: 2,
),
),
const SizedBox(height: 8),
Text(
'Expires: ${DateFormat('MMM dd, yyyy').format(_inviteCode!.expiresAt)}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => _copyToClipboard(_inviteCode!.code),
icon: const Icon(Icons.copy),
label: const Text('Copy Code'),
),
),
const SizedBox(width: 8),
Expanded(
child: OutlinedButton.icon(
onPressed: () => _shareInvite(_inviteCode!.code),
icon: const Icon(Icons.share),
label: const Text('Share'),
),
),
],
),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Close'),
),
],
);
}
Future<void> _copyToClipboard(String code) async {
await Clipboard.setData(ClipboardData(text: code));
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invite code copied to clipboard')),
);
}
}
Future<void> _shareInvite(String code) async {
// Share functionality would be implemented here
await _copyToClipboard(code);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Invite code copied - paste to share')),
);
}
}
}
```
Include proper loading states, error handling, and copy-to-clipboard functionality.
Follow Material Design dialog patterns from authentication flows.
</action>
<verify>flutter analyze lib/features/household/presentation/widgets/invite_code_dialog.dart passes</verify>
<done>Invite code dialog provides code generation, display, and sharing functionality</done>
</task>
<task type="auto">
<name>Create Create Household Page</name>
<files>lib/features/household/presentation/pages/create_household_page.dart</files>
<action>
Create household creation page following authentication UI patterns:
```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';
class CreateHouseholdPage extends ConsumerStatefulWidget {
const CreateHouseholdPage({Key? key}) : super(key: key);
@override
ConsumerState<CreateHouseholdPage> createState() => _CreateHouseholdPageState();
}
class _CreateHouseholdPageState extends ConsumerState<CreateHouseholdPage> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
bool _isLoading = false;
String? _error;
@override
void dispose() {
_nameController.dispose();
super.dispose();
}
Future<void> _createHousehold() 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)
.createHousehold(_nameController.text.trim(), user.id);
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Household "${household.name}" created successfully!'),
backgroundColor: Colors.green,
),
);
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;
}
return 'Failed to create household. Please try again.';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Create 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.home,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
'Create a New Household',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Start tracking your shared inventory with family or roommates',
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: _nameController,
decoration: const InputDecoration(
labelText: 'Household Name',
hintText: 'e.g., The Smith Family',
prefixIcon: Icon(Icons.home),
),
textInputAction: TextInputAction.done,
autofillHints: const [AutofillHints.name],
validator: (value) {
if (value == null || value.trim().isEmpty) {
return 'Please enter a household name';
}
if (value.trim().length > 100) {
return 'Household name too long (max 100 characters)';
}
return null;
},
onFieldSubmitted: (_) => _createHousehold(),
),
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,
),
),
),
],
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _isLoading ? null : _createHousehold,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Create Household'),
),
const SizedBox(height: 16),
OutlinedButton(
onPressed: () => Navigator.of(context).pop(),
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Cancel'),
),
],
),
),
),
),
);
}
}
```
Follow authentication page patterns for consistency.
Include proper validation, loading states, and error handling.
</action>
<verify>flutter analyze lib/features/household/presentation/pages/create_household_page.dart passes</verify>
<done>Create household page provides form validation and user feedback</done>
</task>
</tasks>
<verification>
1. Household card component displays household information with role-based actions
2. Invite code dialog handles generation, display, copying, and sharing
3. Create household page follows authentication UI patterns with proper validation
4. All components handle loading states and errors appropriately
5. UI is consistent with existing authentication flows
</verification>
<success_criteria>
Household management UI components are complete with proper validation, error handling, and user feedback ready for integration.
</success_criteria>
<output>
After completion, create `.planning/phases/02-household-creation/02-04-SUMMARY.md` with household UI components implementation summary
</output>

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>

View File

@@ -0,0 +1,676 @@
---
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>