From 37836ec8a5a8172eb717da9d56f2ea3d86b3d453 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Sun, 24 May 2026 04:17:38 -0600 Subject: [PATCH] Remove outdated shop implementation plan. Shop work is complete; drop the superseded planning doc. --- .../plans/2026-05-24-shop-implementation.md | 512 ------------------ 1 file changed, 512 deletions(-) delete mode 100644 docs/superpowers/plans/2026-05-24-shop-implementation.md diff --git a/docs/superpowers/plans/2026-05-24-shop-implementation.md b/docs/superpowers/plans/2026-05-24-shop-implementation.md deleted file mode 100644 index bede5c5..0000000 --- a/docs/superpowers/plans/2026-05-24-shop-implementation.md +++ /dev/null @@ -1,512 +0,0 @@ -# Shop Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Fix the existing shop end-to-end so `/shop` shows catalog items, users can buy with coins earned from habits, equip purchases, and see changes on the pet/room. - -**Architecture:** Primary fix is an idempotent migration that upserts `shop_items` (empty shop today). Harden buy/equip with Postgres RPCs that atomically deduct coins and update ownership/equip state. Client [`lib/shop-storage.ts`](../../lib/shop-storage.ts) calls RPCs; UI auto-equips after purchase. - -**Tech Stack:** Next.js App Router, Supabase Postgres + RLS, existing shop UI in [`components/shop/shop-content.tsx`](../../components/shop/shop-content.tsx) - -**Brainstorming decisions:** -- Scope: fix existing shop (not rebuild) -- Blocker: empty shop (`shop_items` not populated in prod) -- Catalog fix: migration upsert (not client fallback) - ---- - -## File map - -| File | Responsibility | -|------|----------------| -| `supabase/migrations/20260525100000_shop_catalog_upsert.sql` | Idempotent catalog rows + legacy cleanup | -| `supabase/migrations/20260525110000_shop_purchase_and_equip.sql` | Atomic purchase/equip RPCs | -| `supabase/seed.sql` | Stay in sync with catalog migration | -| `lib/shop-storage.ts` | Call RPCs for buy/equip; keep reads client-side | -| `components/shop/shop-content.tsx` | Auto-equip after buy, surface RPC errors | - ---- - -### Task 1: Shop catalog migration (fixes empty shop) - -**Files:** -- Create: `supabase/migrations/20260525100000_shop_catalog_upsert.sql` -- Modify: `supabase/seed.sql` - -- [ ] **Step 1: Create catalog migration** - -```sql --- Idempotent shop catalog. Fixes empty shop when seed did not run. - -delete from public.user_items -where item_id in ('hat_blue', 'glasses_round', 'room_sunset', 'room-space', 'room-neon'); - -delete from public.shop_items -where id in ( - 'hat_blue', - 'glasses_round', - 'room_sunset', - 'room-space', - 'room-neon', - 'pants-1', - 'pants-2', - 'shoes-1', - 'shoes-2', - 'torso-1', - 'torso-2', - 'eyes-1', - 'eyes-2', - 'head-1', - 'head-2' -); - -insert into public.shop_items (id, name, type, price, image_path) -values - ('pants-3', 'Pants 3', 'pants', 30, '/character/pants/pants-3.png'), - ('shoes-3', 'Shoes 3', 'shoes', 30, '/character/shoes/shoes-3.png'), - ('torso-3', 'Torso 3', 'torso', 35, '/character/torso/torso-3.png'), - ('torso-4', 'Torso 4', 'torso', 40, '/character/torso/torso-4.png'), - ('head-3', 'Head 3', 'head', 40, '/character/head/head-3.png'), - ('head-4', 'Head 4', 'head', 45, '/character/head/head-4.png'), - ('head-5', 'Head 5', 'head', 50, '/character/head/head-5.png'), - ('head-6', 'Head 6', 'head', 55, '/character/head/head-6.png'), - ('head-7', 'Head 7', 'head', 60, '/character/head/head-7.png'), - ('room-night', 'Starlit Night', 'room', 40, ''), - ('room-forest', 'Mystic Forest', 'room', 50, '') -on conflict (id) do update -set - name = excluded.name, - type = excluded.type, - price = excluded.price, - image_path = excluded.image_path; -``` - -- [ ] **Step 2: Align `supabase/seed.sql`** - -Replace the delete/insert block in `supabase/seed.sql` with the same catalog SQL as Step 1 (keep file identical to migration body so local `db reset` and prod deploy stay aligned). - -- [ ] **Step 3: Verify migration applies locally** - -Run: `npx supabase db reset` (if local Supabase running) OR push to linked project: `npx supabase db push --linked` - -Run: `npx supabase db execute --linked --sql "select count(*) from shop_items;"` - -Expected: `11` rows - -- [ ] **Step 4: Commit** - -```bash -git add supabase/migrations/20260525100000_shop_catalog_upsert.sql supabase/seed.sql -git commit -m "fix: upsert shop catalog via migration so shop is never empty" -``` - ---- - -### Task 2: Postgres RPCs for atomic purchase and equip - -**Files:** -- Create: `supabase/migrations/20260525110000_shop_purchase_and_equip.sql` - -- [ ] **Step 1: Create RPC migration** - -```sql --- Atomic shop purchase and equip operations. - -create or replace function public.purchase_shop_item(p_item_id text) -returns jsonb -language plpgsql -security definer -set search_path = public -as $$ -declare - v_user_id uuid := auth.uid(); - v_item public.shop_items%rowtype; - v_coins int; -begin - if v_user_id is null then - raise exception 'You must be signed in to purchase items.'; - end if; - - select * into v_item from public.shop_items where id = p_item_id; - if not found then - raise exception 'That shop item is no longer available.'; - end if; - - if exists ( - select 1 from public.user_items - where user_id = v_user_id and item_id = p_item_id - ) then - raise exception 'You already own this item.'; - end if; - - select coins into v_coins - from public.avatar_state - where user_id = v_user_id - for update; - - if not found then - insert into public.avatar_state (user_id, coins) values (v_user_id, 0); - v_coins := 0; - end if; - - if v_coins < v_item.price then - raise exception 'Not enough points for this purchase.'; - end if; - - update public.avatar_state - set coins = v_coins - v_item.price, updated_at = now() - where user_id = v_user_id; - - insert into public.user_items (user_id, item_id) - values (v_user_id, p_item_id); - - return jsonb_build_object( - 'item_id', v_item.id, - 'name', v_item.name, - 'type', v_item.type, - 'price', v_item.price, - 'image_path', v_item.image_path, - 'coins', v_coins - v_item.price - ); -end; -$$; - -create or replace function public.equip_shop_item(p_item_id text) -returns jsonb -language plpgsql -security definer -set search_path = public -as $$ -declare - v_user_id uuid := auth.uid(); - v_item public.shop_items%rowtype; - v_equipped text[]; - v_layer text; -begin - if v_user_id is null then - raise exception 'You must be signed in to equip items.'; - end if; - - if not exists ( - select 1 from public.user_items - where user_id = v_user_id and item_id = p_item_id - ) then - raise exception 'Purchase this item before equipping it.'; - end if; - - select * into v_item from public.shop_items where id = p_item_id; - if not found then - raise exception 'Could not find that shop item.'; - end if; - - if v_item.type = 'room' then - update public.avatar_state - set room_background = p_item_id, updated_at = now() - where user_id = v_user_id; - - return jsonb_build_object('equipped_room', p_item_id); - end if; - - select equipped_items into v_equipped - from public.avatar_state - where user_id = v_user_id - for update; - - v_equipped := coalesce(v_equipped, '{}'); - - -- Remove any equipped item on the same layer - v_equipped := array( - select e from unnest(v_equipped) as e - where e = p_item_id - or not (e like v_item.type || '-%') - ); - - v_equipped := array_append(v_equipped, p_item_id); - - v_layer := v_item.type; - - update public.avatar_state - set - equipped_items = v_equipped, - equipped_item = p_item_id, - head_style = case when v_layer = 'head' then p_item_id else head_style end, - torso_style = case when v_layer = 'torso' then p_item_id else torso_style end, - pants_style = case when v_layer = 'pants' then p_item_id else pants_style end, - shoe_style = case when v_layer = 'shoes' then p_item_id else shoe_style end, - eye_type = case when v_layer = 'eyes' then p_item_id else eye_type end, - updated_at = now() - where user_id = v_user_id; - - return jsonb_build_object('equipped_items', to_jsonb(v_equipped)); -end; -$$; - -create or replace function public.unequip_shop_item(p_item_id text) -returns jsonb -language plpgsql -security definer -set search_path = public -as $$ -declare - v_user_id uuid := auth.uid(); - v_item public.shop_items%rowtype; - v_equipped text[]; - v_layer text; -begin - if v_user_id is null then - raise exception 'You must be signed in to unequip items.'; - end if; - - select * into v_item from public.shop_items where id = p_item_id; - if not found then - raise exception 'Could not find that shop item.'; - end if; - - if v_item.type = 'room' then - raise exception 'Room backgrounds cannot be unequipped.'; - end if; - - select equipped_items into v_equipped - from public.avatar_state - where user_id = v_user_id - for update; - - v_equipped := coalesce(v_equipped, '{}'); - v_equipped := array(select e from unnest(v_equipped) as e where e <> p_item_id); - v_layer := v_item.type; - - update public.avatar_state - set - equipped_items = v_equipped, - equipped_item = coalesce(v_equipped[1], 'none'), - head_style = case when v_layer = 'head' then 'none' else head_style end, - torso_style = case when v_layer = 'torso' then 'none' else torso_style end, - pants_style = case when v_layer = 'pants' then 'none' else pants_style end, - shoe_style = case when v_layer = 'shoes' then 'none' else shoe_style end, - eye_type = case when v_layer = 'eyes' then 'none' else eye_type end, - updated_at = now() - where user_id = v_user_id; - - return jsonb_build_object('equipped_items', to_jsonb(v_equipped)); -end; -$$; - -revoke all on function public.purchase_shop_item(text) from public; -revoke all on function public.equip_shop_item(text) from public; -revoke all on function public.unequip_shop_item(text) from public; - -grant execute on function public.purchase_shop_item(text) to authenticated; -grant execute on function public.equip_shop_item(text) to authenticated; -grant execute on function public.unequip_shop_item(text) to authenticated; -``` - -- [ ] **Step 2: Apply migration** - -Run: `npx supabase db push --linked` (or `db reset` locally) - -Expected: functions created without error - -- [ ] **Step 3: Commit** - -```bash -git add supabase/migrations/20260525110000_shop_purchase_and_equip.sql -git commit -m "feat: add atomic shop purchase and equip RPCs" -``` - ---- - -### Task 3: Wire client storage to RPCs - -**Files:** -- Modify: `lib/shop-storage.ts` - -- [ ] **Step 1: Replace `purchaseShopItem`** - -Remove `adjustCoins` import usage from purchase path. Replace function body: - -```typescript -export async function purchaseShopItem(itemId: string) { - const userId = await getAuthenticatedUserId(); - - if (!userId) { - throw new Error("You must be signed in to purchase items."); - } - - const supabase = createClient(); - const { data, error } = await supabase.rpc("purchase_shop_item", { - p_item_id: itemId, - }); - - if (error) { - throw new Error(error.message); - } - - notifyHabitPetDataUpdated(); - - return { - id: data.item_id as string, - name: data.name as string, - type: data.type as ShopItemRecord["type"], - price: data.price as number, - image_path: data.image_path as string, - }; -} -``` - -- [ ] **Step 2: Replace `equipShopItem`** - -```typescript -export async function equipShopItem(itemId: string) { - const userId = await getAuthenticatedUserId(); - - if (!userId) { - throw new Error("You must be signed in to equip items."); - } - - const supabase = createClient(); - const { error } = await supabase.rpc("equip_shop_item", { - p_item_id: itemId, - }); - - if (error) { - throw new Error(error.message); - } - - notifyHabitPetDataUpdated(); - return getEquippedItems(); -} -``` - -- [ ] **Step 3: Replace `unequipShopItem`** - -```typescript -export async function unequipShopItem(itemId: string) { - const userId = await getAuthenticatedUserId(); - - if (!userId) { - throw new Error("You must be signed in to unequip items."); - } - - const supabase = createClient(); - const { error } = await supabase.rpc("unequip_shop_item", { - p_item_id: itemId, - }); - - if (error) { - throw new Error(error.message); - } - - notifyHabitPetDataUpdated(); - return getEquippedItems(); -} -``` - -- [ ] **Step 4: Remove unused import** - -Remove `adjustCoins` from the import at top of `lib/shop-storage.ts` if no longer used. Keep `getCoins` for `getShopInventory`. - -- [ ] **Step 5: Run lint** - -Run: `npm run lint` - -Expected: no errors - -- [ ] **Step 6: Commit** - -```bash -git add lib/shop-storage.ts -git commit -m "refactor: use Supabase RPCs for shop purchase and equip" -``` - ---- - -### Task 4: Shop UX — auto-equip after purchase - -**Files:** -- Modify: `components/shop/shop-content.tsx` - -- [ ] **Step 1: Auto-equip in `handlePurchase`** - -After successful purchase, call equip: - -```typescript -const handlePurchase = async (itemId: string) => { - setPendingItemId(itemId); - - try { - const item = await purchaseShopItem(itemId); - await equipShopItem(itemId); - toast(`Purchased and equipped ${item.name}.`, "success"); - await refreshShop(); - } catch (purchaseError) { - toast( - purchaseError instanceof Error - ? purchaseError.message - : "Purchase failed.", - "error", - ); - } finally { - setPendingItemId(null); - } -}; -``` - -- [ ] **Step 2: Run lint and build** - -Run: `npm run lint && npm run build` - -Expected: pass - -- [ ] **Step 3: Commit** - -```bash -git add components/shop/shop-content.tsx -git commit -m "feat: auto-equip shop items after purchase" -``` - ---- - -### Task 5: End-to-end verification - -- [ ] **Step 1: Local smoke test** - -1. Start app: `npm run dev` -2. Sign in, complete onboarding if needed -3. Visit `/shop` — expect ~11 items in grouped sections (not empty message) -4. Complete one habit on `/avatar` or `/habits` — shop balance should show 50 points -5. Buy `shoes-3` (30 pts) — balance 20, item shows Equipped -6. Visit `/avatar` — pet shows shoes-3 sprite -7. Visit `/customize` — shoes-3 selectable (owned) -8. Try buying same item again — error toast "already own" -9. Buy with 20 pts an item costing 40 — error "Not enough points", balance unchanged - -- [ ] **Step 2: Push and verify deploy** - -```bash -git push origin main -``` - -Watch GitHub Actions Deploy workflow. Expected: success, `supabase db push --include-seed` applies new migrations. - -- [ ] **Step 3: Production smoke test** - -Visit https://cursor-hackathon-flame.vercel.app/shop — catalog visible, buy/equip flow works. - ---- - -## Success criteria - -| Check | Expected | -|-------|----------| -| `/shop` loads items | 11 catalog rows, grouped by layer + rooms | -| Earn coins | +50 per habit complete | -| Buy | Deducts coins, adds `user_items` row | -| Equip | Updates pet sprite / room on `/avatar` | -| Customize | Owned variants unlocked | -| Errors | Clear messages for insufficient points / already owned | - -## Out of scope - -- Coin rewards from daily quiz -- Next.js API route layer (RPCs sufficient) -- New shop item types beyond existing layer styles + rooms