From e20858f608608ea9dda0d69aa7ecc7103756ac4b Mon Sep 17 00:00:00 2001 From: Dani B Date: Wed, 28 Jan 2026 15:50:09 -0500 Subject: [PATCH] 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 --- .planning/ROADMAP.md | 12 + .../02-household-creation/02-01-PLAN.md | 266 +++++++ .../02-household-creation/02-02-PLAN.md | 433 +++++++++++ .../02-household-creation/02-03-PLAN.md | 367 ++++++++++ .../02-household-creation/02-04-PLAN.md | 590 +++++++++++++++ .../02-household-creation/02-05-PLAN.md | 679 ++++++++++++++++++ .../02-household-creation/02-06-PLAN.md | 676 +++++++++++++++++ 7 files changed, 3023 insertions(+) create mode 100644 .planning/phases/02-household-creation/02-01-PLAN.md create mode 100644 .planning/phases/02-household-creation/02-02-PLAN.md create mode 100644 .planning/phases/02-household-creation/02-03-PLAN.md create mode 100644 .planning/phases/02-household-creation/02-04-PLAN.md create mode 100644 .planning/phases/02-household-creation/02-05-PLAN.md create mode 100644 .planning/phases/02-household-creation/02-06-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f177ffb..c8092c4 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -60,6 +60,18 @@ Plans: **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 diff --git a/.planning/phases/02-household-creation/02-01-PLAN.md b/.planning/phases/02-household-creation/02-01-PLAN.md new file mode 100644 index 0000000..392fa9d --- /dev/null +++ b/.planning/phases/02-household-creation/02-01-PLAN.md @@ -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" +--- + + +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. + + + +@~/.opencode/get-shit-done/workflows/execute-plan.md +@~/.opencode/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Create Household Data Models + lib/features/household/domain/models/household_models.dart + + 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. + + flutter analyze lib/features/household/domain/models/household_models.dart passes + Household data models compile with proper JSON serialization and validation + + + + Create Household Entity Models + lib/features/household/domain/entities/household_entity.dart + + Create entity models separating business logic from data persistence: + + 1. HouseholdEntity with: + - Core properties (same as model but without JSON methods) + - List 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. + + flutter analyze lib/features/household/domain/entities/household_entity.dart passes + Household entities compile with business logic methods and proper separation from data models + + + + Create Household Repository Interface + lib/features/household/domain/repositories/household_repository.dart + + Create abstract repository defining household operations: + + abstract class HouseholdRepository { + Future createHousehold(String name, String userId); + Future> getUserHouseholds(String userId); + Future getHouseholdById(String householdId); + Future generateInviteCode(String householdId, String createdBy); + Future joinHousehold(String inviteCode, String userId); + Future leaveHousehold(String householdId, String userId); + Future removeMember(String householdId, String memberUserId); + Future updateMemberRole(String householdId, String memberUserId, HouseholdRole role); + Future> getHouseholdMembers(String householdId); + Future getActiveInviteCode(String householdId); + Future revokeInviteCode(String inviteCodeId); + } + + Include Household enum: + enum HouseholdRole { owner, editor, viewer } + + Follow auth repository interface patterns. + + flutter analyze lib/features/household/domain/repositories/household_repository.dart passes + Household repository interface defines all required operations with proper typing + + + + Create Household Remote Datasource + lib/features/household/data/datasources/household_remote_datasource.dart + + Create Supabase datasource implementing low-level data operations: + + class HouseholdRemoteDatasource { + final SupabaseClient client; + + HouseholdRemoteDatasource(this.client); + + // Core CRUD operations + Future> createHousehold(Map data); + Future>> getUserHouseholds(String userId); + Future?> getHouseholdById(String householdId); + Future> generateInviteCode(Map data); + Future> joinHousehold(String inviteCode, String userId); + Future leaveHousehold(String householdId, String userId); + + // Member operations + Future>> getHouseholdMembers(String householdId); + Future removeMember(String householdId, String memberUserId); + Future updateMemberRole(String householdId, String memberUserId, String role); + + // Invite operations + Future?> getActiveInviteCode(String householdId); + Future revokeInviteCode(String inviteCodeId); + } + + Use Supabase table operations: .from('households'), .from('household_members'), .from('invite_codes'). + Include error handling for Supabase exceptions. + + flutter analyze lib/features/household/data/datasources/household_remote_datasource.dart passes + Household remote datasource provides low-level Supabase operations with proper error handling + + + + Create Household Repository Implementation + lib/features/household/data/repositories/household_repository_impl.dart + + Create repository implementation using datasource and models: + + class HouseholdRepositoryImpl implements HouseholdRepository { + final HouseholdRemoteDatasource datasource; + + HouseholdRepositoryImpl(this.datasource); + + @override + Future createHousehold(String name, String userId) async { + // Create household record + // Add owner as member + // Return populated entity + } + + @override + Future 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 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. + + flutter analyze lib/features/household/data/repositories/household_repository_impl.dart passes + Household repository implementation provides business logic and data mapping with validation + + + + + +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 + + + +Household data layer is complete with models, repository interface, and Supabase implementation ready for UI integration and invite code generation. + + + +After completion, create `.planning/phases/02-household-creation/02-01-SUMMARY.md` with household data layer implementation summary + \ No newline at end of file diff --git a/.planning/phases/02-household-creation/02-02-PLAN.md b/.planning/phases/02-household-creation/02-02-PLAN.md new file mode 100644 index 0000000..73d15b6 --- /dev/null +++ b/.planning/phases/02-household-creation/02-02-PLAN.md @@ -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" +--- + + +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. + + + +@~/.opencode/get-shit-done/workflows/execute-plan.md +@~/.opencode/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Create Household Tables Migration + supabase/migrations/001_create_household_tables.sql + + 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. + + supabase db push --dry-run shows valid SQL syntax + Household tables created with proper relationships and constraints + + + + Create Household RLS Policies + supabase/migrations/002_household_rls_policies.sql + + 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. + + supabase db push --dry-run shows valid RLS policies + Row-Level Security policies enforce household data isolation with proper role-based access + + + + Create Household Performance Indexes + supabase/migrations/003_household_indexes.sql + + 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. + + supabase db push --dry-run shows valid index creation + Performance indexes optimize household queries and support real-time synchronization + + + + Create Household Database Functions + supabase/migrations/004_household_functions.sql + + 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. + + supabase db push --dry-run shows valid function creation + Database functions provide efficient household operations with proper security + + + + + +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 + + + +Supabase database schema is complete with household tables, security policies, indexes, and functions ready for application integration. + + + +After completion, create `.planning/phases/02-household-creation/02-02-SUMMARY.md` with database schema implementation summary + \ No newline at end of file diff --git a/.planning/phases/02-household-creation/02-03-PLAN.md b/.planning/phases/02-household-creation/02-03-PLAN.md new file mode 100644 index 0000000..f2fa84c --- /dev/null +++ b/.planning/phases/02-household-creation/02-03-PLAN.md @@ -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" + - 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" + 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" +--- + + +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. + + + +@~/.opencode/get-shit-done/workflows/execute-plan.md +@~/.opencode/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Create Household Use Cases + + 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 + + + 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 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 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 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> 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. + + flutter analyze lib/features/household/domain/usecases/*.dart passes + Household use cases encapsulate business logic with proper validation + + + + Create Household Provider + lib/features/household/providers/household_provider.dart + + Create Riverpod provider for household state management: + ```dart + import 'package:flutter_riverpod/flutter_riverpod.dart'; + + // Use case providers + final createHouseholdUseCaseProvider = Provider((ref) { + final repository = ref.watch(householdRepositoryProvider); + return CreateHouseholdUseCase(repository); + }); + + final generateInviteCodeUseCaseProvider = Provider((ref) { + final repository = ref.watch(householdRepositoryProvider); + return GenerateInviteCodeUseCase(repository); + }); + + final joinHouseholdUseCaseProvider = Provider((ref) { + final repository = ref.watch(householdRepositoryProvider); + return JoinHouseholdUseCase(repository); + }); + + final getUserHouseholdsUseCaseProvider = Provider((ref) { + final repository = ref.watch(householdRepositoryProvider); + return GetUserHouseholdsUseCase(repository); + }); + + // Main household provider + class HouseholdProvider extends StateNotifier>> { + 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 _initialize() async { + state = const AsyncValue.loading(); + try { + // Will be called when auth state is available + } catch (e, stack) { + state = AsyncValue.error(e, stack); + } + } + + Future 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 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 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 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>>((ref) { + return HouseholdProvider( + ref.read(getUserHouseholdsUseCaseProvider), + ref.read(createHouseholdUseCaseProvider), + ref.read(joinHouseholdUseCaseProvider), + ref.read(generateInviteCodeUseCaseProvider), + ); + }); + + // Current selected household provider + final currentHouseholdProvider = StateProvider((ref) => null); + + // Repository provider (to be added to main providers file) + final householdRepositoryProvider = Provider((ref) { + final datasource = ref.read(householdRemoteDatasourceProvider); + return HouseholdRepositoryImpl(datasource); + }); + + final householdRemoteDatasourceProvider = Provider((ref) { + final client = ref.read(supabaseClientProvider); + return HouseholdRemoteDatasource(client); + }); + ``` + + Follow auth provider patterns for consistency. + Include proper error handling and state transitions. + + flutter analyze lib/features/household/providers/household_provider.dart passes + Household provider provides reactive state management with proper integration + + + + Create Household Exception Classes + lib/features/household/domain/exceptions/household_exceptions.dart + + 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. + + flutter analyze lib/features/household/domain/exceptions/household_exceptions.dart passes + Household exception classes provide clear error categorization + + + + + +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 + + + +Household state management is complete with use cases, provider, and exception handling ready for UI integration. + + + +After completion, create `.planning/phases/02-household-creation/02-03-SUMMARY.md` with state management implementation summary + \ No newline at end of file diff --git a/.planning/phases/02-household-creation/02-04-PLAN.md b/.planning/phases/02-household-creation/02-04-PLAN.md new file mode 100644 index 0000000..f3268cc --- /dev/null +++ b/.planning/phases/02-household-creation/02-04-PLAN.md @@ -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" +--- + + +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. + + + +@~/.opencode/get-shit-done/workflows/execute-plan.md +@~/.opencode/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Create Household Card Widget + lib/features/household/presentation/widgets/household_card.dart + + 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( + 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). + + flutter analyze lib/features/household/presentation/widgets/household_card.dart passes + Household card component displays household information with appropriate actions + + + + Create Invite Code Dialog + lib/features/household/presentation/widgets/invite_code_dialog.dart + + 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 createState() => _InviteCodeDialogState(); + } + + class _InviteCodeDialogState extends ConsumerState { + InviteCodeModel? _inviteCode; + bool _isLoading = false; + String? _error; + + @override + void initState() { + super.initState(); + _generateInviteCode(); + } + + Future _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 _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 _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. + + flutter analyze lib/features/household/presentation/widgets/invite_code_dialog.dart passes + Invite code dialog provides code generation, display, and sharing functionality + + + + Create Create Household Page + lib/features/household/presentation/pages/create_household_page.dart + + 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 createState() => _CreateHouseholdPageState(); + } + + class _CreateHouseholdPageState extends ConsumerState { + final _formKey = GlobalKey(); + final _nameController = TextEditingController(); + bool _isLoading = false; + String? _error; + + @override + void dispose() { + _nameController.dispose(); + super.dispose(); + } + + Future _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. + + flutter analyze lib/features/household/presentation/pages/create_household_page.dart passes + Create household page provides form validation and user feedback + + + + + +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 + + + +Household management UI components are complete with proper validation, error handling, and user feedback ready for integration. + + + +After completion, create `.planning/phases/02-household-creation/02-04-SUMMARY.md` with household UI components implementation summary + \ No newline at end of file diff --git a/.planning/phases/02-household-creation/02-05-PLAN.md b/.planning/phases/02-household-creation/02-05-PLAN.md new file mode 100644 index 0000000..531c832 --- /dev/null +++ b/.planning/phases/02-household-creation/02-05-PLAN.md @@ -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" +--- + + +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. + + + +@~/.opencode/get-shit-done/workflows/execute-plan.md +@~/.opencode/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Create Join Household Page + lib/features/household/presentation/pages/join_household_page.dart + + 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 createState() => _JoinHouseholdPageState(); + } + + class _JoinHouseholdPageState extends ConsumerState { + final _formKey = GlobalKey(); + final _codeController = TextEditingController(); + bool _isLoading = false; + String? _error; + HouseholdEntity? _joinedHousehold; + + @override + void dispose() { + _codeController.dispose(); + super.dispose(); + } + + Future _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. + + flutter analyze lib/features/household/presentation/pages/join_household_page.dart passes + Join household page provides invite code validation with immediate feedback + + + + Create Household List Page + lib/features/household/presentation/pages/household_list_page.dart + + 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 createState() => _HouseholdListPageState(); + } + + class _HouseholdListPageState extends ConsumerState { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _loadHouseholds(); + }); + } + + Future _loadHouseholds() async { + final user = ref.read(authProvider).user; + if (user != null) { + await ref.read(householdProvider.notifier).loadUserHouseholds(user.id); + } + } + + Future _showCreateHousehold() async { + final result = await Navigator.of(context).push( + 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 _showJoinHousehold() async { + final result = await Navigator.of(context).push( + 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 _showInviteDialog(HouseholdEntity household) async { + await showDialog( + context: context, + builder: (context) => InviteCodeDialog( + householdId: household.id, + householdName: household.name, + ), + ); + } + + Future _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 _showLeaveConfirmation(HouseholdEntity household) async { + final confirmed = await showDialog( + 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. + + flutter analyze lib/features/household/presentation/pages/household_list_page.dart passes + Household list page provides complete management hub with navigation integration + + + + + +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 + + + +Household management UI is complete with join flow, list hub, and navigation integration ready for user testing. + + + +After completion, create `.planning/phases/02-household-creation/02-05-SUMMARY.md` with household UI completion summary + \ No newline at end of file diff --git a/.planning/phases/02-household-creation/02-06-PLAN.md b/.planning/phases/02-household-creation/02-06-PLAN.md new file mode 100644 index 0000000..8da3858 --- /dev/null +++ b/.planning/phases/02-household-creation/02-06-PLAN.md @@ -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" +--- + + +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. + + + +@~/.opencode/get-shit-done/workflows/execute-plan.md +@~/.opencode/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Update Router with Household Routes + lib/core/router/app_router.dart + + 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. + + flutter analyze lib/core/router/app_router.dart passes + Router includes household routes with proper authentication and household state guards + + + + Update Auth Provider with Household Integration + lib/providers/auth_provider.dart + + 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> { + final AuthRepository _repository; + + AuthProvider(this._repository) : super(const AsyncValue.loading()) { + _initialize(); + } + + Future _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 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 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 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 _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. + + flutter analyze lib/providers/auth_provider.dart passes + Auth provider integrates household loading with authentication state changes + + + + Update Home Page with Household Context + lib/features/home/presentation/pages/home_page.dart + + 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( + 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. + + flutter analyze lib/features/home/presentation/pages/home_page.dart passes + Home page includes household context with navigation to household management + + + + Update Main Provider Registration + lib/main.dart + + 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. + + flutter analyze lib/main.dart passes + Main app includes household provider registration with proper dependencies + + + + + +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 + + + +Household functionality is fully integrated into app navigation and authentication flow with seamless user experience ready for production. + + + +After completion, create `.planning/phases/02-household-creation/02-06-SUMMARY.md` with integration completion summary + \ No newline at end of file