# Volana Expo App UI 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:** Translate the HTML prototype (`prototype/Volana.dc.html`) into the Expo app (`app/`) — all pages, mock data, theming and responsive (mobile + web) layout — per the approved spec at `docs/superpowers/specs/2026-07-02-expo-app-ui-design.md`. **Architecture:** Bottom-tab + stack navigation (Expo Router) on mobile, with a persistent top nav bar replacing the tab bar on wide/web viewports (`md:` breakpoint, ~768px). Two React Context providers (`WalletProvider`, `ModalProvider`) hold mock wallet state and the globally-triggered Wallet-Connect/Checkout `BottomSheet` overlays. All product/resolver/order data is static, taken verbatim from the prototype's embedded arrays. Styling uses HeroUI Native components with Tailwind/Uniwind `className`, with only color tokens overridden in `global.css` (radius is left at the HeroUI default, which already matches the prototype's 8–16px scale). **Tech Stack:** Expo SDK 57, Expo Router 57 (typed routes), React 19, React Native 0.86, HeroUI Native 1.0.5, Uniwind (Tailwind v4 for RN), `@expo/vector-icons` (Ionicons). **Note on verification steps:** This project has no test runner configured (`package.json` only has `lint`/`typecheck`/`format` scripts) and this feature is presentational/mock-data-only per the approved spec (no business logic to unit-test). So instead of TDD red/green steps, every task's verification step is `yarn typecheck` (and `yarn lint` at milestones), plus manual visual checks at the point a screen first becomes viewable. All commands below assume the working directory is `app/` (i.e. `/mnt/store/develop/projects/projd/volana/app`). --- ## File Overview ``` app/src/global.css — theme color overrides (Task 1) app/src/data/mock.ts — Product/Resolver/Order types + mock data (Task 2) app/src/lib/status.ts — status/availability → label+color helpers (Task 3) app/src/state/wallet.tsx — WalletProvider/useWallet (Task 4) app/src/state/modals.tsx — ModalProvider/useModals (Task 5) app/src/app/_layout.tsx — providers, TopNav, global sheets (Tasks 6 & 15) app/src/components/StatusChip.tsx — Task 7 app/src/components/ListingCard.tsx — Task 8 app/src/components/ResolverCard.tsx — Task 9 app/src/components/OrderRow.tsx — Task 10 app/src/components/WalletConnectSheet.tsx — Task 11 app/src/components/CheckoutSheet.tsx — Task 12 app/src/components/AppHeader.tsx — Task 13 app/src/components/TopNav.tsx — Task 14 app/src/app/(tabs)/index.tsx — Home screen (Task 16) app/src/app/(tabs)/browse.tsx — Browse screen, replaces explore.tsx (Task 17) app/src/app/(tabs)/orders.tsx — Orders list screen (Task 18) app/src/app/(tabs)/_layout.tsx — tab bar, responsive hide (Task 19) app/src/app/listing/[id].tsx — Listing detail screen (Task 20) app/src/app/orders/[id].tsx — Order detail screen (Task 21) ``` Task 22 is a final typecheck/lint/manual-walkthrough pass. --- ### Task 1: Theme color tokens **Files:** - Modify: `app/src/global.css` - [ ] **Step 1: Add prototype color overrides** Replace the full contents of `app/src/global.css` with: ```css @import "tailwindcss"; @import "uniwind"; @import "heroui-native/styles"; /* End-user path: heroui-native lives in the project's node_modules, one level up from this src/global.css file. */ @source '../node_modules/heroui-native/lib'; @layer theme { @variant light { --background: #f7f6fb; --surface: #ffffff; --surface-secondary: #f0eef8; --accent: #7a34d4; --accent-foreground: #ffffff; --success: #059669; --warning: #d97706; --danger: #dc2626; } @variant dark { --background: #0e0e1c; --surface: #171730; --surface-secondary: #1e1e3c; --accent: #b060ff; --accent-foreground: #ffffff; --success: #14f195; --warning: #f5a623; --danger: #ff4d4f; } } ``` These are the exact hex values from `prototype/Volana.dc.html`'s `:root`/`[data-theme="light"]` CSS variables (`--c-bg`, `--c-surf`, `--c-surf2`, `--c-a`, `--c-g`, `--c-y`, `--c-r`). `--radius` is intentionally left untouched — HeroUI's default base radius is 8px, which already produces `radius-lg`=8px/`radius-xl`=12px/`radius-2xl`=16px, matching the prototype's 8–16px card/button radii closely enough that overriding it isn't needed. - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors (CSS changes don't affect TS, this just confirms the baseline is still healthy before further changes). - [ ] **Step 3: Commit** ```bash git add app/src/global.css git commit -m "theme: apply Volana prototype accent/status/surface colors" ``` --- ### Task 2: Mock data **Files:** - Create: `app/src/data/mock.ts` - [ ] **Step 1: Create the mock data module** ```ts export type Currency = "SOL" | "USDC"; export interface Product { id: number; name: string; desc: string; price: string; priceNum: number; cur: Currency; usd: string; qty: number; seller: string; bg: string; emoji: string; alt: Currency[]; cat: string; } export interface Resolver { id: number; name: string; desc: string; fee: string; type: "Human" | "DAO" | "Automated"; wins: number; total: number; } export type OrderStatus = "AwaitingConfirm" | "Active" | "Complete" | "Cancelled" | "Disputed"; export interface Order { id: number; lid: number; name: string; amt: string; status: OrderStatus; date: string; seller: string; emoji: string; } export const PRODUCTS: Product[] = [ { id: 0, name: 'MacBook Pro 14" M3 Pro', desc: "Factory sealed. 18GB Unified Memory, 512GB SSD, Space Black. International keyboard layout.", price: "2.4 SOL", priceNum: 2.4, cur: "SOL", usd: "$288", qty: 5, seller: "Gh9Z…mF8k", bg: "#0a1828", emoji: "💻", alt: ["USDC"], cat: "Electronics" }, { id: 1, name: "Adobe Photoshop Lifetime Key", desc: "One-time activation code. Non-subscription. Valid for 2 devices on Windows and macOS.", price: "45 USDC", priceNum: 45, cur: "USDC", usd: "$45", qty: 12, seller: "Bx3K…7pRq", bg: "#001230", emoji: "🎨", alt: ["SOL"], cat: "Software" }, { id: 2, name: 'Air Jordan 1 Retro "Shadow" US 10', desc: "Deadstock. Black/Particle Grey/White. Original box, tissue paper and receipt. Zero wears.", price: "180 USDC", priceNum: 180, cur: "USDC", usd: "$180", qty: 1, seller: "Qm4R…nL2w", bg: "#180800", emoji: "👟", alt: [], cat: "Fashion" }, { id: 3, name: "Custom 65% Mechanical Keyboard", desc: "Lubed Gateron Yellow Pro V2 switches. POM plate, anodized aluminum case. USB-C cable included.", price: "0.9 SOL", priceNum: 0.9, cur: "SOL", usd: "$108", qty: 3, seller: "Kx7P…2mJc", bg: "#0a1810", emoji: "⌨️", alt: ["USDC"], cat: "Electronics" }, { id: 4, name: "Midjourney Pro Plan (1 Year)", desc: "Full-year activation code. 30 fast GPU hours/month, unlimited relaxed, private and stealth mode.", price: "120 USDC", priceNum: 120, cur: "USDC", usd: "$120", qty: 8, seller: "Pp1L…sY9v", bg: "#120820", emoji: "🖼️", alt: [], cat: "Software" }, { id: 5, name: "Sony WH-1000XM5 Headphones", desc: "Brand new, factory sealed. Midnight Black. 30-hour battery. Industry-leading noise cancellation.", price: "1.8 SOL", priceNum: 1.8, cur: "SOL", usd: "$216", qty: 0, seller: "Fx2T…oK5r", bg: "#0a0a14", emoji: "🎧", alt: [], cat: "Electronics" }, { id: 6, name: 'iPad Pro 12.9" M2 (256GB Wi-Fi)', desc: "Silver. Opened for 2 hours of testing only. AppleCare+ until June 2026. Original box included.", price: "3.2 SOL", priceNum: 3.2, cur: "SOL", usd: "$384", qty: 2, seller: "Rv8W…eC1x", bg: "#081420", emoji: "📱", alt: ["USDC"], cat: "Electronics" }, { id: 7, name: "Figma Professional (1 Year)", desc: "Team seat. Unlimited projects, 180-day version history, dev mode and advanced prototyping.", price: "60 USDC", priceNum: 60, cur: "USDC", usd: "$60", qty: 20, seller: "Mn6S…hB3z", bg: "#180a00", emoji: "✏️", alt: ["SOL"], cat: "Software" }, { id: 8, name: "Supreme Box Logo Hoodie — M", desc: "FW23 White. Unworn, original tags attached. Comes in original Supreme bag. Firm price.", price: "220 USDC", priceNum: 220, cur: "USDC", usd: "$220", qty: 1, seller: "Yx5A…dN7u", bg: "#180008", emoji: "🧥", alt: [], cat: "Fashion" }, { id: 9, name: "Nintendo Switch OLED (White)", desc: "Excellent condition. Original Joy-Con, dock, power adapter, HDMI cable. Screen is pristine.", price: "1.2 SOL", priceNum: 1.2, cur: "SOL", usd: "$144", qty: 4, seller: "Jc0M…pQ4y", bg: "#0e0d18", emoji: "🕹️", alt: ["USDC"], cat: "Electronics" }, { id: 10, name: "myshop.sol Domain Name", desc: "Premium 3-year .sol domain registration. Currently parked. Instant transfer to your wallet on purchase.", price: "0.5 SOL", priceNum: 0.5, cur: "SOL", usd: "$60", qty: 1, seller: "Zg3F…wR6t", bg: "#0e0820", emoji: "🌐", alt: [], cat: "Crypto" }, { id: 11, name: "Rolex Submariner 116610LN", desc: "Ref 116610LN, 2019. Full box and papers. Last serviced June 2024 by Rolex-authorized RSC.", price: "8.5 SOL", priceNum: 8.5, cur: "SOL", usd: "$1,020", qty: 1, seller: "Wh7N…iE2s", bg: "#081215", emoji: "⌚", alt: ["USDC"], cat: "Luxury" }, ]; export const RESOLVERS: Resolver[] = [ { id: 0, name: "ShieldArb", desc: "Human arbitrators with 24h response.", fee: "0.5%", type: "Human", wins: 94, total: 1247 }, { id: 1, name: "FairDAO", desc: "Community-governed DAO for digital goods.", fee: "1.0%", type: "DAO", wins: 89, total: 823 }, { id: 2, name: "QuickResolve", desc: "AI-assisted, resolves in under 2 hours.", fee: "1.5%", type: "Automated", wins: 96, total: 3102 }, ]; export const ORDERS: Order[] = [ { id: 0, lid: 0, name: 'MacBook Pro 14" M3 Pro', amt: "2.4 SOL", status: "AwaitingConfirm", date: "2 days ago", seller: "Gh9Z…mF8k", emoji: "💻" }, { id: 1, lid: 1, name: "Adobe Photoshop Lifetime Key", amt: "45 USDC", status: "Active", date: "5 days ago", seller: "Bx3K…7pRq", emoji: "🎨" }, { id: 2, lid: 7, name: "Figma Professional (1 Year)", amt: "60 USDC", status: "Complete", date: "12 days ago", seller: "Mn6S…hB3z", emoji: "✏️" }, ]; ``` This is a verbatim port of the `_L`, `_R`, `_O` arrays from `prototype/Volana.dc.html` (lines 835–859). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/data/mock.ts git commit -m "feat: add mock product/resolver/order data from prototype" ``` --- ### Task 3: Status helpers **Files:** - Create: `app/src/lib/status.ts` - [ ] **Step 1: Create the status/availability helper module** ```ts import type { OrderStatus } from "@/data/mock"; export type StatusColor = "default" | "accent" | "success" | "warning" | "danger"; export interface StatusInfo { label: string; color: StatusColor; } const ORDER_STATUS_INFO: Record = { AwaitingConfirm: { label: "Awaiting Confirm", color: "warning" }, Active: { label: "Active", color: "accent" }, Complete: { label: "Complete", color: "success" }, Cancelled: { label: "Cancelled", color: "default" }, Disputed: { label: "Disputed", color: "danger" }, }; export function orderStatusInfo(status: OrderStatus): StatusInfo { return ORDER_STATUS_INFO[status]; } export function availabilityInfo(qty: number): StatusInfo { if (qty === 0) return { label: "Out of Stock", color: "default" }; if (qty <= 3) return { label: `Only ${qty} left!`, color: "warning" }; return { label: `${qty} in stock`, color: "success" }; } ``` `StatusColor` intentionally matches HeroUI Native's `Chip` `color` prop union (`'default'|'accent'|'success'|'warning'|'danger'`) exactly, so results can be passed straight through to `Chip`/`StatusChip` without conversion. This ports the `_si`/`_av` helper functions from `prototype/Volana.dc.html` (lines 869–884). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/lib/status.ts git commit -m "feat: add order status and availability label/color helpers" ``` --- ### Task 4: WalletProvider **Files:** - Create: `app/src/state/wallet.tsx` - [ ] **Step 1: Create the mock wallet context** ```tsx import type { JSX, PropsWithChildren } from "react"; import { createContext, useCallback, useContext, useMemo, useState } from "react"; export type WalletName = "phantom" | "solflare" | "backpack"; const WALLET_ADDRESSES: Record = { phantom: "Gh9Z…k3mP", solflare: "Bx3K…7pRq", backpack: "Kx7P…2mJc", }; interface WalletContextValue { connected: boolean; address: string | null; connect: (wallet: WalletName) => void; disconnect: () => void; } const WalletContext = createContext(null); export function WalletProvider({ children }: PropsWithChildren): JSX.Element { const [address, setAddress] = useState(null); const connect = useCallback((wallet: WalletName) => { setAddress(WALLET_ADDRESSES[wallet]); }, []); const disconnect = useCallback(() => { setAddress(null); }, []); const value = useMemo( () => ({ connected: address !== null, address, connect, disconnect }), [address, connect, disconnect], ); return {children}; } export function useWallet(): WalletContextValue { const ctx = useContext(WalletContext); if (!ctx) throw new Error("useWallet must be used within a WalletProvider"); return ctx; } ``` Mirrors the prototype's mock `connectPhantom`/`connectSolflare`/`connectBackpack`/`disconnectWallet` handlers (lines 1062–1082), minus the toast side effect (that's wired at the call site in Task 11). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/state/wallet.tsx git commit -m "feat: add mock WalletProvider" ``` --- ### Task 5: ModalProvider **Files:** - Create: `app/src/state/modals.tsx` - [ ] **Step 1: Create the global modal/checkout context** ```tsx import type { JSX, PropsWithChildren } from "react"; import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react"; import { useWallet, type WalletName } from "@/state/wallet"; export type CheckoutStep = "review" | "pending" | "success"; interface ModalContextValue { isWalletSheetOpen: boolean; isCheckoutSheetOpen: boolean; checkoutListingId: number | null; checkoutStep: CheckoutStep; requestBuy: (listingId: number) => void; openWalletConnect: () => void; connectWallet: (wallet: WalletName) => void; closeWalletSheet: () => void; closeCheckoutSheet: () => void; confirmOrder: () => void; } const ModalContext = createContext(null); export function ModalProvider({ children }: PropsWithChildren): JSX.Element { const { connected, connect } = useWallet(); const [isWalletSheetOpen, setWalletSheetOpen] = useState(false); const [isCheckoutSheetOpen, setCheckoutSheetOpen] = useState(false); const [checkoutListingId, setCheckoutListingId] = useState(null); const [checkoutStep, setCheckoutStep] = useState("review"); const pendingListingId = useRef(null); const requestBuy = useCallback( (listingId: number) => { if (connected) { setCheckoutListingId(listingId); setCheckoutStep("review"); setCheckoutSheetOpen(true); } else { pendingListingId.current = listingId; setWalletSheetOpen(true); } }, [connected], ); const openWalletConnect = useCallback(() => { pendingListingId.current = null; setWalletSheetOpen(true); }, []); const connectWallet = useCallback( (wallet: WalletName) => { connect(wallet); setWalletSheetOpen(false); const listingId = pendingListingId.current; pendingListingId.current = null; if (listingId !== null) { setCheckoutListingId(listingId); setCheckoutStep("review"); setCheckoutSheetOpen(true); } }, [connect], ); const closeWalletSheet = useCallback(() => { setWalletSheetOpen(false); pendingListingId.current = null; }, []); const closeCheckoutSheet = useCallback(() => { setCheckoutSheetOpen(false); }, []); const confirmOrder = useCallback(() => { setCheckoutStep("pending"); setTimeout(() => setCheckoutStep("success"), 2200); }, []); const value = useMemo( () => ({ isWalletSheetOpen, isCheckoutSheetOpen, checkoutListingId, checkoutStep, requestBuy, openWalletConnect, connectWallet, closeWalletSheet, closeCheckoutSheet, confirmOrder, }), [ isWalletSheetOpen, isCheckoutSheetOpen, checkoutListingId, checkoutStep, requestBuy, openWalletConnect, connectWallet, closeWalletSheet, closeCheckoutSheet, confirmOrder, ], ); return {children}; } export function useModals(): ModalContextValue { const ctx = useContext(ModalContext); if (!ctx) throw new Error("useModals must be used within a ModalProvider"); return ctx; } ``` `requestBuy` mirrors the prototype's `clickBuy`/`onBuyNow` logic (open wallet modal first if not connected, remembering the listing via `pendingId`, else go straight to checkout). `confirmOrder` mirrors `confirmOrder` (lines 1055–1058): flips to `pending`, then `success` after 2.2s. - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/state/modals.tsx git commit -m "feat: add ModalProvider for wallet-connect and checkout sheets" ``` --- ### Task 6: Wire Wallet and Modal providers into the root layout **Files:** - Modify: `app/src/app/_layout.tsx` - [ ] **Step 1: Wrap the app in WalletProvider and ModalProvider** Replace the full contents of `app/src/app/_layout.tsx` with: ```tsx import type { JSX } from "react"; import { Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { HeroUINativeProvider } from "heroui-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import "../global.css"; import { WalletProvider } from "@/state/wallet"; import { ModalProvider } from "@/state/modals"; export default function RootLayout(): JSX.Element { return ( ); } ``` - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. Run: `yarn start` (or `yarn start --web`), open the app. Expected: app boots and shows the existing Home tab exactly as before (providers don't change behavior yet — nothing consumes them until later tasks). Stop the dev server after confirming. - [ ] **Step 3: Commit** ```bash git add app/src/app/_layout.tsx git commit -m "feat: wire WalletProvider and ModalProvider into root layout" ``` --- ### Task 7: StatusChip component **Files:** - Create: `app/src/components/StatusChip.tsx` - [ ] **Step 1: Create the component** ```tsx import type { JSX } from "react"; import { Chip } from "heroui-native"; import type { StatusColor } from "@/lib/status"; interface StatusChipProps { label: string; color: StatusColor; } export function StatusChip({ label, color }: StatusChipProps): JSX.Element { return ( {label} ); } ``` `variant="soft"` gives the tinted-background-plus-matching-text look the prototype uses for all its badges (e.g. `background:var(--c-gd);border:1px solid var(--c-g);color:var(--c-g)`). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/components/StatusChip.tsx git commit -m "feat: add StatusChip component" ``` --- ### Task 8: ListingCard component **Files:** - Create: `app/src/components/ListingCard.tsx` - [ ] **Step 1: Create the component** ```tsx import type { GestureResponderEvent } from "react-native"; import type { JSX } from "react"; import { Pressable, View } from "react-native"; import { useRouter } from "expo-router"; import { Button, Separator, Surface, Typography } from "heroui-native"; import type { Product } from "@/data/mock"; import { availabilityInfo } from "@/lib/status"; import { StatusChip } from "@/components/StatusChip"; import { useModals } from "@/state/modals"; interface ListingCardProps { product: Product; variant?: "list" | "grid"; } export function ListingCard({ product, variant = "grid" }: ListingCardProps): JSX.Element { const router = useRouter(); const { requestBuy } = useModals(); const availability = availabilityInfo(product.qty); const isOut = product.qty === 0; const openListing = () => router.push(`/listing/${product.id}`); const handleBuyPress = (event: GestureResponderEvent) => { event.stopPropagation(); requestBuy(product.id); }; if (variant === "list") { return ( {product.emoji} {product.priceNum} {product.cur} {product.name} {product.desc} ); } return ( {product.emoji} {product.name} {product.seller} {product.price} ); } ``` `grid` variant carries its own responsive width (`w-1/2 md:w-1/3 lg:w-1/4`) so callers can just render a `flex-row flex-wrap` container and drop cards in — no CSS Grid needed (React Native doesn't support `display: grid`, even on the web build). `list` variant reproduces the prototype's image/price/name+desc/buy row (lines 353–392) using `Separator` for the vertical divider lines instead of manual border classes. - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/components/ListingCard.tsx git commit -m "feat: add ListingCard component (list and grid variants)" ``` --- ### Task 9: ResolverCard component **Files:** - Create: `app/src/components/ResolverCard.tsx` - [ ] **Step 1: Create the component** ```tsx import type { JSX } from "react"; import { Pressable, View } from "react-native"; import { Typography } from "heroui-native"; import type { Resolver } from "@/data/mock"; interface ResolverCardProps { resolver: Resolver; isSelected: boolean; onSelect: () => void; } export function ResolverCard({ resolver, isSelected, onSelect }: ResolverCardProps): JSX.Element { return ( {resolver.name} {resolver.fee} {resolver.type} · {resolver.wins}% buyer-favorable ); } ``` Ports the prototype's selectable resolver rows (lines 514–522 / `_fb` selected-state styling). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/components/ResolverCard.tsx git commit -m "feat: add ResolverCard component" ``` --- ### Task 10: OrderRow component **Files:** - Create: `app/src/components/OrderRow.tsx` - [ ] **Step 1: Create the component** ```tsx import type { JSX } from "react"; import { Pressable, View } from "react-native"; import { useRouter } from "expo-router"; import { Typography } from "heroui-native"; import type { Order } from "@/data/mock"; import { orderStatusInfo } from "@/lib/status"; import { StatusChip } from "@/components/StatusChip"; interface OrderRowProps { order: Order; } export function OrderRow({ order }: OrderRowProps): JSX.Element { const router = useRouter(); const status = orderStatusInfo(order.status); return ( router.push(`/orders/${order.id}`)} className="flex-row items-center gap-3.5 rounded-xl border border-border bg-surface p-3.5" > {order.emoji} {order.name} {order.seller} {order.amt} {order.date} ); } ``` Ports the prototype's order list row (lines 556–570). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/components/OrderRow.tsx git commit -m "feat: add OrderRow component" ``` --- ### Task 11: WalletConnectSheet component **Files:** - Create: `app/src/components/WalletConnectSheet.tsx` - [ ] **Step 1: Create the component** ```tsx import type { JSX } from "react"; import { View } from "react-native"; import { BottomSheet, Button, Typography, useToast } from "heroui-native"; import { useModals } from "@/state/modals"; import type { WalletName } from "@/state/wallet"; const WALLETS: { id: WalletName; label: string; description: string; emoji: string }[] = [ { id: "phantom", label: "Phantom", description: "Most popular Solana wallet", emoji: "👻" }, { id: "solflare", label: "Solflare", description: "Built for power users", emoji: "🌊" }, { id: "backpack", label: "Backpack", description: "xNFT & multi-chain wallet", emoji: "🎒" }, ]; export function WalletConnectSheet(): JSX.Element { const { isWalletSheetOpen, closeWalletSheet, connectWallet } = useModals(); const { toast } = useToast(); const handleConnect = (wallet: WalletName, label: string) => { connectWallet(wallet); toast.show({ variant: "success", label: `${label} connected!` }); }; return ( { if (!open) closeWalletSheet(); }} > Connect Wallet Choose your Solana wallet {WALLETS.map((wallet) => ( ))} ); } ``` This is rendered once at the root (Task 15) and driven entirely by `ModalProvider`'s `isWalletSheetOpen`/`closeWalletSheet`/`connectWallet` — no `BottomSheet.Trigger` needed, matching the documented fully-controlled `isOpen`/`onOpenChange` pattern. Ports the prototype's wallet modal (lines 656–698) and `connectPhantom`/`connectSolflare`/`connectBackpack` toasts (lines 1062–1082). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/components/WalletConnectSheet.tsx git commit -m "feat: add WalletConnectSheet component" ``` --- ### Task 12: CheckoutSheet component **Files:** - Create: `app/src/components/CheckoutSheet.tsx` - [ ] **Step 1: Create the component** ```tsx import type { JSX } from "react"; import { useMemo, useState } from "react"; import { View } from "react-native"; import { useRouter } from "expo-router"; import { BottomSheet, Button, Spinner, Typography, useThemeColor } from "heroui-native"; import { PRODUCTS, RESOLVERS } from "@/data/mock"; import { useModals } from "@/state/modals"; import { ResolverCard } from "@/components/ResolverCard"; export function CheckoutSheet(): JSX.Element { const router = useRouter(); const accentColor = useThemeColor("accent"); const { isCheckoutSheetOpen, checkoutListingId, checkoutStep, closeCheckoutSheet, confirmOrder } = useModals(); const [resolverId, setResolverId] = useState(0); const listing = useMemo( () => PRODUCTS.find((product) => product.id === checkoutListingId) ?? PRODUCTS[0], [checkoutListingId], ); const handleViewOrders = () => { closeCheckoutSheet(); router.push("/orders"); }; const handleContinueShopping = () => { closeCheckoutSheet(); router.push("/browse"); }; return ( { if (!open) closeCheckoutSheet(); }} > {checkoutStep === "review" && ( Place Order {listing.emoji} {listing.name} {listing.seller} Payment {listing.price} ≈ {listing.usd} USD Dispute Protection {RESOLVERS.map((item) => ( setResolverId(item.id)} /> ))} )} {checkoutStep === "pending" && ( Confirming on Solana… {"Approve the transaction in your wallet.\nThis takes a few seconds."} )} {checkoutStep === "success" && ( Order Placed! {`${listing.price} is now held in secure escrow.\nThe seller has been notified.`} )} ); } ``` `confirmOrder` (from `ModalProvider`) drives the `review → pending → success` transition with the same 2.2s delay as the prototype (lines 1055–1058). The resolver picker reuses `ResolverCard` (Task 9). `isCloseOnPress={checkoutStep !== "pending"}` stops the user from dismissing the sheet mid-"transaction". - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/components/CheckoutSheet.tsx git commit -m "feat: add CheckoutSheet component (review/pending/success)" ``` --- ### Task 13: AppHeader component **Files:** - Create: `app/src/components/AppHeader.tsx` - [ ] **Step 1: Create the component** ```tsx import type { JSX } from "react"; import { Pressable, View } from "react-native"; import { useRouter } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { Typography, useThemeColor } from "heroui-native"; interface AppHeaderProps { title: string; } export function AppHeader({ title }: AppHeaderProps): JSX.Element { const router = useRouter(); const foreground = useThemeColor("foreground"); return ( router.back()} hitSlop={8} className="p-1"> {title} ); } ``` Used only on the mobile-pushed Listing Detail and Order Detail screens (Tasks 20–21), which set `headerShown: false` at the root Stack level and render this instead. It's hidden at `md:` because `TopNav` (Task 14) is visible there and provides navigation instead. - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/components/AppHeader.tsx git commit -m "feat: add AppHeader component for mobile stack screens" ``` --- ### Task 14: TopNav component **Files:** - Create: `app/src/components/TopNav.tsx` - [ ] **Step 1: Create the component** ```tsx import type { JSX } from "react"; import { useState } from "react"; import { Pressable, View } from "react-native"; import { useRouter } from "expo-router"; import { Ionicons } from "@expo/vector-icons"; import { Button, SearchField, Typography, useThemeColor } from "heroui-native"; import { useWallet } from "@/state/wallet"; import { useModals } from "@/state/modals"; export function TopNav(): JSX.Element { const router = useRouter(); const accentForeground = useThemeColor("accent-foreground"); const { connected, address, disconnect } = useWallet(); const { openWalletConnect } = useModals(); const [search, setSearch] = useState(""); return ( router.push("/")} className="flex-row items-center gap-2"> Volana router.push("/browse")} /> {connected ? ( ) : ( )} ); } ``` `hidden md:flex` keeps this invisible on mobile widths. The search field navigates to `/browse` on submit — it intentionally does not thread the query into Browse's own search state (Browse has its own independent search field, Task 17); wiring cross-screen search state isn't needed for a mock-data app and would add scope without a corresponding prototype requirement beyond "navigate to browse". - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add app/src/components/TopNav.tsx git commit -m "feat: add TopNav component for wide/web layouts" ``` --- ### Task 15: Wire TopNav and global sheets into the root layout **Files:** - Modify: `app/src/app/_layout.tsx` - [ ] **Step 1: Render TopNav above the Stack, and the two sheets alongside it** Replace the full contents of `app/src/app/_layout.tsx` with: ```tsx import type { JSX } from "react"; import { Stack } from "expo-router"; import { StatusBar } from "expo-status-bar"; import { HeroUINativeProvider } from "heroui-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { View } from "react-native"; import "../global.css"; import { WalletProvider } from "@/state/wallet"; import { ModalProvider } from "@/state/modals"; import { TopNav } from "@/components/TopNav"; import { WalletConnectSheet } from "@/components/WalletConnectSheet"; import { CheckoutSheet } from "@/components/CheckoutSheet"; export default function RootLayout(): JSX.Element { return ( ); } ``` `TopNav` sits above the `Stack` inside a `flex-1` wrapper, so it persists across every route pushed onto that Stack (tab screens and, later, `listing/[id]`/`orders/[id]`). The two sheets are rendered once, outside the Stack, and are entirely state-driven by `ModalProvider` — any screen can call `useModals().requestBuy(...)` etc. to open them. - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. Run: `yarn start --web`, resize the browser window wide (≥768px) and narrow (<768px). Expected: at narrow width, no top bar is visible (bottom tab bar still shows, from the existing (tabs) layout). At wide width, the Volana top bar (logo, search field, Browse/My Orders buttons, Connect Wallet button) appears above the content. Clicking "Connect Wallet" opens a bottom sheet listing Phantom/Solflare/Backpack; clicking one closes it and the nav button now shows the mock address. Stop the dev server after confirming. - [ ] **Step 3: Commit** ```bash git add app/src/app/_layout.tsx git commit -m "feat: render TopNav and global wallet/checkout sheets in root layout" ``` --- ### Task 16: Home screen **Files:** - Modify: `app/src/app/(tabs)/index.tsx` - [ ] **Step 1: Replace the placeholder Home screen** Replace the full contents of `app/src/app/(tabs)/index.tsx` with: ```tsx import type { JSX } from "react"; import { ScrollView, View } from "react-native"; import { useRouter } from "expo-router"; import { Button, Typography } from "heroui-native"; import { PRODUCTS } from "@/data/mock"; import { ListingCard } from "@/components/ListingCard"; export default function HomeTab(): JSX.Element { const router = useRouter(); const featured = PRODUCTS.filter((product) => product.qty > 0).slice(0, 4); return ( {"The marketplace\n"} nobody controls. Buy and sell freely. Payments are final — no chargebacks, no frozen accounts. Featured listings {featured.map((product) => ( ))} ); } ``` This is the "Minimal" home-tab scope agreed in the spec: short hero + featured listings only (no stats/"why Volana"/"how it works" sections, no footer). Ports the prototype's hero headline (lines 125–126) and featured listings grid (lines 262–276), using the same "first 4 in-stock products" selection (prototype `featuredProducts`, lines 1099–1103). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. Run: `yarn start --web`, open the Home tab. Expected: purple-accented headline "The marketplace / nobody controls.", subline, "Browse Listings →" button, and a 2-column (narrow) / 3–4-column (wide) grid of 4 featured product cards with emoji thumbnails, names, and prices. Tapping a card navigates to a (still-placeholder-behaving, since Task 20 isn't done yet — it will 404 or show a default Expo Router "unmatched route" screen) listing route — that's expected at this point. Stop the dev server after confirming the Home tab itself renders correctly. - [ ] **Step 3: Commit** ```bash git add "app/src/app/(tabs)/index.tsx" git commit -m "feat: implement Home screen (hero + featured listings)" ``` --- ### Task 17: Browse screen **Files:** - Delete: `app/src/app/(tabs)/explore.tsx` - Create: `app/src/app/(tabs)/browse.tsx` - [ ] **Step 1: Remove the placeholder Explore screen** ```bash git rm "app/src/app/(tabs)/explore.tsx" ``` - [ ] **Step 2: Create the Browse screen** ```tsx import type { JSX } from "react"; import { useMemo, useState } from "react"; import { Pressable, ScrollView, View } from "react-native"; import { Ionicons } from "@expo/vector-icons"; import { Chip, SearchField, Switch, Typography, useThemeColor } from "heroui-native"; import type { Currency, Product } from "@/data/mock"; import { PRODUCTS } from "@/data/mock"; import { ListingCard } from "@/components/ListingCard"; type CurrencyFilter = "all" | Currency; type SortMode = "newest" | "priceLow" | "priceHigh"; type ViewMode = "list" | "grid"; export default function BrowseTab(): JSX.Element { const [search, setSearch] = useState(""); const [currencyFilter, setCurrencyFilter] = useState("all"); const [sortMode, setSortMode] = useState("newest"); const [inStockOnly, setInStockOnly] = useState(true); const [viewMode, setViewMode] = useState("list"); const mutedColor = useThemeColor("muted"); const accentForeground = useThemeColor("accent-foreground"); const products = useMemo(() => { let result = [...PRODUCTS]; if (inStockOnly) result = result.filter((product) => product.qty > 0); if (currencyFilter !== "all") result = result.filter((product) => product.cur === currencyFilter); if (search.trim()) { const query = search.trim().toLowerCase(); result = result.filter( (product) => product.name.toLowerCase().includes(query) || product.desc.toLowerCase().includes(query), ); } if (sortMode === "priceLow") result.sort((a, b) => a.priceNum - b.priceNum); else if (sortMode === "priceHigh") result.sort((a, b) => b.priceNum - a.priceNum); return result; }, [search, currencyFilter, sortMode, inStockOnly]); return ( Currency setCurrencyFilter("all")} > All setCurrencyFilter("SOL")} > SOL setCurrencyFilter("USDC")} > USDC Sort setSortMode("newest")} > Newest setSortMode("priceLow")} > Price ↑ setSortMode("priceHigh")} > Price ↓ In stock only setViewMode("list")} className={`h-7 w-8 items-center justify-center rounded-md ${ viewMode === "list" ? "bg-accent" : "" }`} > setViewMode("grid")} className={`h-7 w-8 items-center justify-center rounded-md ${ viewMode === "grid" ? "bg-accent" : "" }`} > {viewMode === "list" ? ( {products.map((product) => ( ))} ) : ( {products.map((product) => ( ))} )} ); } ``` Ports the prototype's filter bar (lines 322–347: currency chips, sort chips, in-stock switch, list/grid toggle) and its filtering/sorting logic (`renderVals`, lines 898–914). Both view modes render via a plain `ScrollView` + `.map()` rather than `FlatList`, since `ListingCard`'s `grid` variant already carries responsive width classes that are incompatible with `FlatList`'s fixed `numColumns` — and at 12 mock items, `FlatList`'s virtualization has no practical benefit anyway. - [ ] **Step 3: Verify** Run: `yarn typecheck` Expected: passes with no errors (this also confirms the `explore.tsx` deletion didn't break any other import — nothing else references it). Run: `yarn start --web`, navigate to the Browse tab (bottom tab bar narrow, or "Browse" link in TopNav wide). Expected: search field, Currency (All/SOL/USDC) and Sort (Newest/Price↑/Price↓) chips that visibly highlight the active one, "In stock only" switch (defaults on — Sony headphones with qty 0 should be hidden), list/grid toggle that switches layout. Typing in the search field filters the list live. Stop the dev server after confirming. - [ ] **Step 4: Commit** ```bash git add "app/src/app/(tabs)/browse.tsx" git commit -m "feat: implement Browse screen (search, filters, list/grid view)" ``` --- ### Task 18: Orders screen **Files:** - Create: `app/src/app/(tabs)/orders.tsx` - [ ] **Step 1: Create the Orders list screen** ```tsx import type { JSX } from "react"; import { ScrollView, View } from "react-native"; import { Typography } from "heroui-native"; import { ORDERS } from "@/data/mock"; import { OrderRow } from "@/components/OrderRow"; export default function OrdersTab(): JSX.Element { return ( My Orders {ORDERS.map((order) => ( ))} ); } ``` Ports the prototype's My Orders page (lines 546–575), using the static `ORDERS` mock array (per the spec's "no dynamically added orders" decision — the prototype itself doesn't add purchased items to this list either). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. - [ ] **Step 3: Commit** ```bash git add "app/src/app/(tabs)/orders.tsx" git commit -m "feat: implement Orders list screen" ``` --- ### Task 19: Responsive tab bar with Browse and Orders tabs **Files:** - Modify: `app/src/app/(tabs)/_layout.tsx` - [ ] **Step 1: Register the Browse and Orders tabs, hide the tab bar at md: width** Replace the full contents of `app/src/app/(tabs)/_layout.tsx` with: ```tsx import { Ionicons } from "@expo/vector-icons"; import { Tabs } from "expo-router"; import type { ComponentProps, JSX } from "react"; import type { ColorValue } from "react-native"; import { useWindowDimensions } from "react-native"; type IoniconName = ComponentProps["name"]; function TabIcon({ name, color }: { name: IoniconName; color: ColorValue }): JSX.Element { return ; } const WIDE_BREAKPOINT = 768; export default function TabsLayout(): JSX.Element { const { width } = useWindowDimensions(); const isWide = width >= WIDE_BREAKPOINT; return ( , }} /> , }} /> , }} /> ); } ``` `useWindowDimensions` is used (rather than a Tailwind `md:` class) because `tabBarStyle` is a React Navigation option evaluated in JS, not a style on a component we can attach a `className` to directly. It re-renders on resize, so resizing a browser window across the breakpoint live-toggles the tab bar. - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. Run: `yarn start --web`. Expected: bottom tab bar shows Home/Browse/Orders at narrow width; resizing the window to ≥768px hides the tab bar (TopNav's Browse/My Orders links take over). Stop the dev server after confirming. - [ ] **Step 3: Commit** ```bash git add "app/src/app/(tabs)/_layout.tsx" git commit -m "feat: add Browse/Orders tabs, hide tab bar on wide viewports" ``` --- ### Task 20: Listing Detail screen **Files:** - Create: `app/src/app/listing/[id].tsx` - [ ] **Step 1: Create the Listing Detail screen** ```tsx import type { JSX } from "react"; import type { ViewStyle } from "react-native"; import { useMemo, useState } from "react"; import { Platform, ScrollView, View } from "react-native"; import { useLocalSearchParams } from "expo-router"; import { Button, Chip, Separator, Typography, useToast } from "heroui-native"; import type { Currency } from "@/data/mock"; import { PRODUCTS, RESOLVERS } from "@/data/mock"; import { availabilityInfo } from "@/lib/status"; import { AppHeader } from "@/components/AppHeader"; import { StatusChip } from "@/components/StatusChip"; import { ResolverCard } from "@/components/ResolverCard"; import { ListingCard } from "@/components/ListingCard"; import { useModals } from "@/state/modals"; // react-native-web supports CSS `position: sticky`, but React Native's own // ViewStyle type doesn't include it — cast is required to use it web-only. const stickyBoxStyle: ViewStyle | undefined = Platform.OS === "web" ? ({ position: "sticky", top: 84 } as ViewStyle) : undefined; export default function ListingDetailScreen(): JSX.Element { const { id } = useLocalSearchParams<{ id: string }>(); const { toast } = useToast(); const { requestBuy } = useModals(); const [resolverId, setResolverId] = useState(0); const listing = useMemo( () => PRODUCTS.find((product) => product.id === Number(id)) ?? PRODUCTS[0], [id], ); const [currency, setCurrency] = useState(listing.cur); const availability = availabilityInfo(listing.qty); const isOut = listing.qty === 0; const relatedProducts = PRODUCTS.filter((p) => p.id !== listing.id && p.qty > 0).slice(0, 6); return ( {listing.emoji} {listing.name} {listing.desc} Verified on Solana {listing.seller} About this listing Category {listing.cat} Available {listing.qty} units Currency {listing.cur} Listing ID {`#${listing.id}${listing.id}${listing.id}${listing.id}`} {relatedProducts.length > 0 && ( Customers also bought {relatedProducts.map((product) => ( ))} )} {listing.price} ≈ {listing.usd} USD {listing.alt.length > 0 && ( <> Pay with setCurrency(listing.cur)} > {listing.cur} setCurrency(listing.alt[0])} > {listing.alt[0]} )} Dispute Protection {RESOLVERS.map((resolver) => ( setResolverId(resolver.id)} /> ))} Fee only charged if dispute is adjudicated. ✓ Final Payment 🛡 Dispute Cover ⬡ On-chain Funds held in secure escrow until you confirm receipt. ); } ``` Ports the prototype's listing detail page (lines 424–543): image, title/description, verified-seller row with copy button (shows a toast instead of touching a real clipboard — no on-chain/real wallet functionality exists to copy, per the spec's non-goals), info grid, related products, and the buy panel (price, currency switch, resolver picker, Buy Now, trust badges). `requestBuy` (from `ModalProvider`, Task 5) reproduces the prototype's "open wallet modal if disconnected, else open checkout" branch. - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. Run: `yarn start --web`, go to Browse, tap any product card. Expected: listing detail renders with image placeholder, title/description, verified-seller row (Copy button shows a toast), info grid, buy panel with price/resolver picker/Buy Now button, trust badge chips. At narrow width the layout is a single stacked column with a back button (`AppHeader`) at the top; at ≥768px it's two columns with the buy panel on the right. Tapping "Buy Now →" while disconnected opens the Wallet Connect sheet; connecting a wallet then opens the Checkout sheet automatically with this listing pre-filled. Stop the dev server after confirming. - [ ] **Step 3: Commit** ```bash git add "app/src/app/listing/[id].tsx" git commit -m "feat: implement Listing Detail screen" ``` --- ### Task 21: Order Detail screen **Files:** - Create: `app/src/app/orders/[id].tsx` - [ ] **Step 1: Create the Order Detail screen** ```tsx import type { JSX } from "react"; import { useMemo, useState } from "react"; import { ScrollView, View } from "react-native"; import { useLocalSearchParams, useRouter } from "expo-router"; import { Button, Typography, useToast } from "heroui-native"; import { ORDERS } from "@/data/mock"; import { orderStatusInfo } from "@/lib/status"; import { AppHeader } from "@/components/AppHeader"; import { StatusChip } from "@/components/StatusChip"; const STEP_LABELS = ["Payment\nHeld", "Seller\nConfirms", "You Confirm\nReceipt"] as const; export default function OrderDetailScreen(): JSX.Element { const { id } = useLocalSearchParams<{ id: string }>(); const router = useRouter(); const { toast } = useToast(); const order = useMemo(() => ORDERS.find((item) => item.id === Number(id)) ?? ORDERS[0], [id]); const [status, setStatus] = useState(order.status); const statusInfo = orderStatusInfo(status); const step2Done = status !== "AwaitingConfirm"; const step3Done = status === "Complete"; const activeStepIndex = step3Done ? 2 : step2Done ? 1 : 0; const canCancel = status === "AwaitingConfirm"; const canConfirm = status === "Active"; const handleCancel = () => { setStatus("Cancelled"); toast.show({ variant: "success", label: "Order cancelled. Full refund initiated." }); }; const handleConfirmReceipt = () => { setStatus("Complete"); toast.show({ variant: "success", label: "Receipt confirmed! Seller has been paid." }); }; return ( Order placed {order.date} {order.name} {order.seller} {order.amt} Order Progress {STEP_LABELS.map((label, index) => { const done = index < activeStepIndex || (index === activeStepIndex && step3Done); const active = index === activeStepIndex && !step3Done; return ( {done ? "✓" : String(index + 1)} {label} ); })} {order.amt} {" is locked in a secure on-chain escrow. Funds are released to the seller only after you "} confirm receipt — no third party can touch them. {canCancel && ( )} {canConfirm && ( )} ); } ``` Ports the prototype's order detail page (lines 577–651): header, 3-step progress stepper with the same `done`/`active` color logic as `_si`/the inline `od` derivation (lines 918–943), escrow info box, and status-conditional actions (`Cancel Order` only when `AwaitingConfirm`, `Confirm Receipt` only when `Active`, `View Listing` always). Status changes are local `useState` (mirrors the prototype, which also only mutates its in-memory state and never persists — see spec's "orders stay static" decision for the *list*; here on the *detail* screen the single order's status is allowed to change locally so Cancel/Confirm visibly do something, exactly like the prototype's `cancelOrder`/`confirmReceipt` handlers). - [ ] **Step 2: Verify** Run: `yarn typecheck` Expected: passes with no errors. Run: `yarn start --web`, go to Orders, tap the "MacBook Pro" order (status `AwaitingConfirm`). Expected: header with name/seller/amount/status chip, 3-step progress bar (step 1 green checkmark, step 2 highlighted, step 3 grey), escrow info text, "Cancel Order (Full Refund)" and "View Listing →" buttons (no "Confirm Receipt" button, since status is `AwaitingConfirm` not `Active`). Tapping "Cancel Order" shows a toast and flips the status chip/stepper to Cancelled. Stop the dev server after confirming. - [ ] **Step 3: Commit** ```bash git add "app/src/app/orders/[id].tsx" git commit -m "feat: implement Order Detail screen" ``` --- ### Task 22: Final verification pass **Files:** none (verification only) - [ ] **Step 1: Full typecheck and lint** Run: `yarn typecheck` Expected: passes with no errors. Run: `yarn lint` Expected: passes with no errors (fix any issues found before proceeding — do not disable rules to silence them). - [ ] **Step 2: Manual walkthrough — mobile width** Run: `yarn start --web`, keep the browser window under 768px wide (or open the Expo Go / simulator build if available). Walk through and confirm: - Bottom tab bar shows Home / Browse / Orders with icons. - Home: hero headline in the accent purple, "Browse Listings →" button navigates to Browse, featured grid shows 4 cards. - Browse: search filters live, currency/sort chips toggle correctly, "In stock only" switch hides the 0-qty Sony headphones when on, list/grid toggle switches layout. - Tapping a product card opens Listing Detail with a back button (top-left) that returns to Browse. - "Buy Now →" while disconnected opens Wallet Connect → picking a wallet closes it and opens Checkout automatically on the same listing. - Checkout: Confirm & Place Order → shows a spinner for ~2s → success screen → "View My Orders" navigates to the Orders tab. - Orders tab lists 3 mock orders with correct status chips; tapping one opens Order Detail with the correct progress stepper and status-appropriate action buttons. - Order Detail: Cancel/Confirm actions show a toast and update the status chip/stepper immediately. - [ ] **Step 3: Manual walkthrough — wide/web width** Resize the browser window to ≥768px (or use a wide simulator/tablet). Walk through and confirm: - Bottom tab bar is gone; a top nav bar (logo, search field, Browse, My Orders, wallet button) is visible and stays visible on every screen, including Listing Detail and Order Detail. - "Browse" / "My Orders" nav buttons navigate correctly; wallet button opens Wallet Connect / shows the connected mock address once connected. - Home hero is centered, featured grid shows more columns than at mobile width. - Browse grid shows more columns; list view rows are wider. - Listing Detail shows a two-column layout (content left, buy panel right); the buy panel visually stays in place while scrolling the left column (sticky). - No mobile-only `AppHeader` back bar is visible on Listing/Order Detail (TopNav is used for navigation instead). - [ ] **Step 4: Stop the dev server** No commit for this task — it's verification-only. If any issue was found and fixed during the walkthrough, amend the relevant earlier task's commit history is *not* required; just create one more commit with the fix: ```bash git add -A git commit -m "fix: address issues found during manual UI walkthrough" ``` (Only run this if fixes were actually needed — skip entirely if the walkthrough passed cleanly.) --- ## Spec Coverage Check | Spec section | Covered by | |---|---| | Theming (accent/status/surface colors, radius left default) | Task 1 | | Mock data (products/resolvers/orders) | Task 2 | | State (WalletProvider, ModalProvider incl. pending-listing-id flow, checkout step machine) | Tasks 4–5 | | Navigation & Screens file layout | Tasks 6, 15–21 | | Responsive shell (TopNav vs bottom tabs, `md:` breakpoint) | Tasks 14, 15, 19 | | Grid layouts via flex-wrap, not CSS grid | Task 8 (ListingCard), used in Tasks 16, 17, 20 | | Listing-detail two-column + sticky buy box on web | Task 20 | | Home (minimal scope) | Task 16 | | Browse (search/filter/sort/view toggle) | Task 17 | | Listing Detail (info grid, related products, resolver/currency selection, trust badges) | Task 20 | | Orders list | Task 18 | | Order Detail (stepper, status actions) | Task 21 | | Shared components (StatusChip, ListingCard, ResolverCard, OrderRow, WalletConnectSheet, CheckoutSheet, AppHeader, TopNav) | Tasks 7–14 | | Verification approach (no test runner → typecheck/lint + manual walkthrough) | Every task's Verify step + Task 22 |