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:
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