diff --git a/.gitignore b/.gitignore index d39914f..e38d1c3 100644 --- a/.gitignore +++ b/.gitignore @@ -44,5 +44,5 @@ app.*.map.json /android/app/profile /android/app/release -TPB.md -supabase_migration.sql +/TPB.md +/supabase_migration.sql diff --git a/TPB_APP_CHECKLIST.md b/TPB_APP_CHECKLIST.md new file mode 100644 index 0000000..90ee757 --- /dev/null +++ b/TPB_APP_CHECKLIST.md @@ -0,0 +1,58 @@ +# TPB App Checklist (Flutter Scope) + +This checklist tracks only the Flutter app implementation against `TPB.md`. +Backend infrastructure (RLS, triggers, storage bucket settings, SQL policies) is intentionally out of scope here. + +## 1) Authentication & Onboarding + +- [x] Forgot-password entry from login +- [x] Password-recovery dialog on auth recovery event +- [x] Ensure default collection on first login +- [x] Add user as owner/member when creating collection + +## 2) Lightning Add (Scanner & Active Collection) + +- [x] Persist and restore active collection +- [x] Scanner supports active collection selection +- [x] Lookup scanned `hw_id` in `global_cars` +- [x] Found branch uses bottom sheet + add action +- [x] New discovery branch captures Name/Series/Year in bottom sheet +- [x] Insert into `hotwheels` with active collection +- [x] Insert validation vote flow in app (`car_votes`) +- [x] Continuous scanner loop / fast next-scan UX + +## 3) Private Photo Upload & Processing + +- [x] Car detail supports camera capture / update photo +- [x] Upload path format in app: `uid/entryid.jpg` +- [x] In-app compression/resizing target (1080px, <=500KB) +- [x] Save path into `hotwheels.user_image_url` + +## 4) Garage Display + +- [x] Query cars by active collection +- [x] Generate signed URLs for private images +- [x] Fallback to `assets/img/icon_bg_removed.png` when no image +- [x] Pagination / lazy loading +- [x] Multi-select move between collections +- [x] Single-car move action + +## 5) Sharing & Collaboration UX + +- [x] Invite member by email +- [x] Remove member flow +- [x] Leave collection flow (non-owner) +- [x] Owner guardrails in manage collection flow + +## 6) Community Validation & Reporting (App UX) + +- [x] Show verification status and confirmation count +- [x] Confirm catalog entry action from car detail +- [x] Report catalog issue action with reason + optional note +- [x] Prevent duplicate open report submission per user/car in app flow +- [x] My Reports screen with status display (open/reviewed/resolved/dismissed) + +## Current App-Only Verdict + +- App-side TPB implementation is complete for the defined Flutter workflows. +- Any further remaining TPB work should now be treated as either backend operations or new product scope. diff --git a/supabase_migration.sql b/supabase_migration.sql deleted file mode 100644 index 5265522..0000000 --- a/supabase_migration.sql +++ /dev/null @@ -1,235 +0,0 @@ --- ══════════════════════════════════════════════════════════════════════ --- HW Collector Hub – Multi-Collection Migration --- Run this in the Supabase SQL Editor (one-time). --- ══════════════════════════════════════════════════════════════════════ - --- 1) Collections table -CREATE TABLE IF NOT EXISTS collections ( - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - owner_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - created_at TIMESTAMPTZ DEFAULT now() -); - --- 2) Collection members (join table) -CREATE TABLE IF NOT EXISTS collection_members ( - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, - collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE, - user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - role TEXT NOT NULL DEFAULT 'member' - CHECK (role IN ('owner', 'member')), - created_at TIMESTAMPTZ DEFAULT now(), - UNIQUE(collection_id, user_id) -); - --- 3) Add collection_id column to hotwheels -ALTER TABLE hotwheels - ADD COLUMN IF NOT EXISTS collection_id UUID REFERENCES collections(id) ON DELETE CASCADE; - --- ── RLS: collections ────────────────────────────────────────────────── -ALTER TABLE collections ENABLE ROW LEVEL SECURITY; - --- Users can see collections they are a member of. -CREATE POLICY "Members can view collections" - ON collections FOR SELECT - USING ( - owner_id = auth.uid() - OR id IN ( - SELECT collection_id FROM collection_members - WHERE user_id = auth.uid() - ) - ); - --- Any authenticated user can create a collection. -CREATE POLICY "Authenticated users can create collections" - ON collections FOR INSERT - WITH CHECK (auth.uid() = owner_id); - --- Only the owner can update. -CREATE POLICY "Owner can update collection" - ON collections FOR UPDATE - USING (owner_id = auth.uid()); - --- Only the owner can delete. -CREATE POLICY "Owner can delete collection" - ON collections FOR DELETE - USING (owner_id = auth.uid()); - --- ── RLS: collection_members ────────────────────────────────────────── -ALTER TABLE collection_members ENABLE ROW LEVEL SECURITY; - --- Users can see their own memberships (avoids infinite recursion). -CREATE POLICY "Users can view own memberships" - ON collection_members FOR SELECT - USING (user_id = auth.uid()); - --- Owner can add members. -CREATE POLICY "Owner can add members" - ON collection_members FOR INSERT - WITH CHECK ( - collection_id IN ( - SELECT id FROM collections WHERE owner_id = auth.uid() - ) - ); - --- Owner can remove members. -CREATE POLICY "Owner can remove members" - ON collection_members FOR DELETE - USING ( - collection_id IN ( - SELECT id FROM collections WHERE owner_id = auth.uid() - ) - OR user_id = auth.uid() -- members can remove themselves - ); - --- ── RLS: hotwheels (update existing) ──────────────────────────────── --- Drop old policies first (they were user_id based). -DROP POLICY IF EXISTS "Enable read access for all users" ON hotwheels; -DROP POLICY IF EXISTS "Enable insert for authenticated users only" ON hotwheels; -DROP POLICY IF EXISTS "Enable update for users based on user_id" ON hotwheels; -DROP POLICY IF EXISTS "Enable delete for users based on user_id" ON hotwheels; - --- Members of a collection can see its cars. -CREATE POLICY "Collection members can view cars" - ON hotwheels FOR SELECT - USING ( - collection_id IN ( - SELECT collection_id FROM collection_members - WHERE user_id = auth.uid() - ) - ); - --- Members can add cars. -CREATE POLICY "Collection members can insert cars" - ON hotwheels FOR INSERT - WITH CHECK ( - collection_id IN ( - SELECT collection_id FROM collection_members - WHERE user_id = auth.uid() - ) - ); - --- Members can update cars in their collections. -CREATE POLICY "Collection members can update cars" - ON hotwheels FOR UPDATE - USING ( - collection_id IN ( - SELECT collection_id FROM collection_members - WHERE user_id = auth.uid() - ) - ); - --- Members can delete cars from their collections. -CREATE POLICY "Collection members can delete cars" - ON hotwheels FOR DELETE - USING ( - collection_id IN ( - SELECT collection_id FROM collection_members - WHERE user_id = auth.uid() - ) - ); - --- ══════════════════════════════════════════════════════════════════════ --- MIGRATION HELPER: Move existing cars into a default collection --- for each user that already has cars. --- ══════════════════════════════════════════════════════════════════════ -DO $$ -DECLARE - _user RECORD; - _coll UUID; -BEGIN - FOR _user IN - SELECT DISTINCT user_id FROM hotwheels WHERE collection_id IS NULL - LOOP - -- Create a default "My Collection" for this user. - INSERT INTO collections (name, owner_id) - VALUES ('My Collection', _user.user_id) - RETURNING id INTO _coll; - - -- Owner is also a member. - INSERT INTO collection_members (collection_id, user_id, role) - VALUES (_coll, _user.user_id, 'owner'); - - -- Assign all existing cars to this collection. - UPDATE hotwheels - SET collection_id = _coll - WHERE user_id = _user.user_id AND collection_id IS NULL; - END LOOP; -END $$; - --- After migration, make collection_id NOT NULL. -ALTER TABLE hotwheels ALTER COLUMN collection_id SET NOT NULL; - --- ══════════════════════════════════════════════════════════════════════ --- RPC: Look up a user's ID by email (for inviting) --- ══════════════════════════════════════════════════════════════════════ -CREATE OR REPLACE FUNCTION get_user_id_by_email(lookup_email TEXT) -RETURNS UUID -LANGUAGE sql -SECURITY DEFINER -- runs as postgres, can read auth.users -SET search_path = '' -AS $$ - SELECT id FROM auth.users - WHERE email = lower(lookup_email) - LIMIT 1; -$$; - --- ══════════════════════════════════════════════════════════════════════ --- RPC: Get collection members with their emails --- ══════════════════════════════════════════════════════════════════════ -CREATE OR REPLACE FUNCTION get_collection_members(p_collection_id UUID) -RETURNS TABLE( - id UUID, - user_id UUID, - email TEXT, - role TEXT, - created_at TIMESTAMPTZ -) -LANGUAGE sql -SECURITY DEFINER -SET search_path = '' -AS $$ - SELECT - cm.id, - cm.user_id, - u.email, - cm.role, - cm.created_at - FROM public.collection_members cm - JOIN auth.users u ON u.id = cm.user_id - WHERE cm.collection_id = p_collection_id - AND cm.collection_id IN ( - SELECT cm2.collection_id FROM public.collection_members cm2 - WHERE cm2.user_id = auth.uid() - ) - ORDER BY - CASE cm.role WHEN 'owner' THEN 0 ELSE 1 END, - cm.created_at; -$$; - --- ══════════════════════════════════════════════════════════════════════ --- Community reporting: car_reports --- ══════════════════════════════════════════════════════════════════════ -CREATE TABLE IF NOT EXISTS car_reports ( - id UUID DEFAULT gen_random_uuid() PRIMARY KEY, - hw_id VARCHAR NOT NULL REFERENCES global_cars(hw_id) ON DELETE CASCADE, - hotwheels_id BIGINT REFERENCES hotwheels(id) ON DELETE SET NULL, - reporter_user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - reason TEXT NOT NULL, - note TEXT, - status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'reviewed', 'resolved', 'dismissed')), - created_at TIMESTAMPTZ NOT NULL DEFAULT now() -); - -ALTER TABLE car_reports ENABLE ROW LEVEL SECURITY; - --- Reporters can create reports only for themselves. -CREATE POLICY "Users can create own car reports" - ON car_reports FOR INSERT - WITH CHECK (reporter_user_id = auth.uid()); - --- Reporters can see their own reports. -CREATE POLICY "Users can view own car reports" - ON car_reports FOR SELECT - USING (reporter_user_id = auth.uid());