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:
@@ -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
|
||||||
|
|||||||
266
.planning/phases/02-household-creation/02-01-PLAN.md
Normal file
266
.planning/phases/02-household-creation/02-01-PLAN.md
Normal 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>
|
||||||
433
.planning/phases/02-household-creation/02-02-PLAN.md
Normal file
433
.planning/phases/02-household-creation/02-02-PLAN.md
Normal 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>
|
||||||
367
.planning/phases/02-household-creation/02-03-PLAN.md
Normal file
367
.planning/phases/02-household-creation/02-03-PLAN.md
Normal 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>
|
||||||
590
.planning/phases/02-household-creation/02-04-PLAN.md
Normal file
590
.planning/phases/02-household-creation/02-04-PLAN.md
Normal 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>
|
||||||
679
.planning/phases/02-household-creation/02-05-PLAN.md
Normal file
679
.planning/phases/02-household-creation/02-05-PLAN.md
Normal 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>
|
||||||
676
.planning/phases/02-household-creation/02-06-PLAN.md
Normal file
676
.planning/phases/02-household-creation/02-06-PLAN.md
Normal 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>
|
||||||
Reference in New Issue
Block a user