Skip to content

Pokemon Sealed Products — Client Wiring Implementation Plan (PR 3 of 3)

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: Let a merchant actually browse and quick-add Pokemon sealed products from the catalog UI — PR 2 (#365) built the backend capability (supportsSealed, plugin-dispatched routes, required game on every write endpoint); this PR is the client half PR 2's own review flagged as required before merging: thread game through the two client calls that are actually wired up, and widen the UI gates that currently hide Pokemon sealed products entirely.

Architecture: client/src/pages/catalog/CatalogSealedPage.jsx's sealedSupported/importSupported booleans are hardcoded per-game string checks (selectedGame === 'mtg', etc.) — the same pattern already used for Riftbound, just extended to include 'pokemon'. client/src/hooks/useSealedQuickAdd.js is the single choke point both the Browse and By-Release views funnel quick-add calls through, so accepting a game parameter there (instead of touching every call site) threads it end-to-end with one change. client/src/utils/api.js's sealedProductsApi.quickAdd/.quickAddBulk are the only two sealedProductsApi methods with any client consumer today (confirmed by grep — .list/.get/.getPricingConfig/.updatePricingConfig are unwired dead exports, out of scope, not touched).

Scope cut — "By Release" stays MTG-only. Its release picker fetches from the legacy GET /api/sets (MTG-only: SetModel-shaped filter, uppercase codes, data.cards/data.isOnlineOnly fields — a different endpoint from the multi-game GET /api/catalog/sets). Generalizing it needs a different fetch, a response-shape reconciliation, and verifying Pokemon sets report sealedProductCount the same way — real work, deferred to a future PR. This plan adds a new byReleaseSupported gate (selectedGame === 'mtg') so the "By Release" tab stays hidden for Pokemon while importSupported (the inline per-product quick-add in Browse) becomes true for it.

Tech Stack: React 18, Vitest + @testing-library/react v16 (has renderHook built in — no extra dependency needed), react-router-dom (MemoryRouter in tests).

Global Constraints

  • Client code and tests are both ESM (import/export); tests use Vitest + @testing-library/react/jest-dom, co-located as x.test.js/x.test.jsx.
  • Do not touch sealedProductsApi.list/.get/.getPricingConfig/.updatePricingConfig — confirmed zero client consumers today (grep -rn "sealedProductsApi\." client/src matches only useSealedQuickAdd.js's quickAdd/quickAddBulk calls). Wiring those up is a different, unscoped feature.
  • Do not touch SealedByRelease's internals beyond nothing — it stays permanently gated behind byReleaseSupported === (selectedGame === 'mtg'), so its existing hardcoded game: 'mtg' fetches are still always correct; no change needed inside that function.
  • No || 'mtg'-style defaultsgame is threaded explicitly from selectedGame (the route param), never assumed.
  • npm run test:client exits 0, no regressions, ≥70% coverage maintained on modified files.
  • Known environment issue, not a code concern: running npm install in this worktree with the system's stale npm 6.14.8 binary corrupts package-lock.json/client/package-lock.json (downgrades lockfileVersion). If a step needs a fresh install, run it and then git status/git restore any resulting lockfile diff before committing — never commit a corrupted lockfile.
  • UI-change rule: before claiming this done, start the dev server and exercise the actual feature in a browser (Task 4) — passing unit tests alone doesn't verify the feature works. The live Shopify quick-add submit call is out of reach in this environment (no credentials available, per PR 2's Task 10 notes) — Task 4 verifies everything reachable without it: gating, tab visibility, and real Pokemon sealed data loading in the Browse view.
  • Commit messages: one imperative sentence, sentence case, no trailing period, ending with the trailer Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>.
  • Before starting: this branch (claude/pokemon-sealed-client-wiring) is stacked on the still-open PR 2 branch (claude/pokemon-sealed-sync-enablement, PR #365) — not main. If PR 2 merges before this lands, rebase onto main before opening this PR.

Task 1: Thread game through the client's sealed quick-add API calls

Files:

  • Modify: client/src/utils/api.js

Interfaces:

  • Produces: sealedProductsApi.quickAdd(game, uuid, quantity, price)Promise (was (uuid, quantity, price)); sealedProductsApi.quickAddBulk(game, items)Promise (was (items), now merges game onto each item before sending, matching the server's quickAddBulkSchemaz.array(quickAddSchema) where each array item requires its own game). Consumed by Task 2.

There is no dedicated test file for api.js's thin wrapper functions in this codebase (confirmed: no client/src/utils/api.test.js exists) — this task's correctness is verified by Task 2's tests, which mock this module and assert on the exact arguments these functions are called with.

  • [ ] Step 1: Update the two function signatures

In client/src/utils/api.js, find:

javascript
// Sealed Products API
export const sealedProductsApi = {
  list: (params = {}) => api.get('/sealed-products', { params }),
  get: (id) => api.get(`/sealed-products/${id}`),
  quickAdd: (uuid, quantity, price) => api.post('/sealed-products/quick-add', { uuid, quantity, price }),
  quickAddBulk: (items) => api.post('/sealed-products/quick-add-bulk', { items }),
  getPricingConfig: () => api.get('/sealed-pricing-config'),
  updatePricingConfig: (config) => api.put('/sealed-pricing-config', config),
};

Replace with:

javascript
// Sealed Products API
export const sealedProductsApi = {
  list: (params = {}) => api.get('/sealed-products', { params }),
  get: (id) => api.get(`/sealed-products/${id}`),
  quickAdd: (game, uuid, quantity, price) => api.post('/sealed-products/quick-add', { game, uuid, quantity, price }),
  // Each item in the bulk array needs its own `game` — the server's
  // quickAddBulkSchema validates `items: z.array(quickAddSchema)`, and
  // quickAddSchema requires `game` per item, not once at the top level.
  quickAddBulk: (game, items) => api.post('/sealed-products/quick-add-bulk', {
    items: items.map((item) => ({ ...item, game })),
  }),
  getPricingConfig: () => api.get('/sealed-pricing-config'),
  updatePricingConfig: (config) => api.put('/sealed-pricing-config', config),
};
  • [ ] Step 2: Run the client test suite to confirm nothing broke

Run: npm run test:client Expected: all suites PASS. (This change alone doesn't break anything since useSealedQuickAdd.js, the only caller, is updated in Task 2 — but confirm the baseline stays green before moving on.)

  • [ ] Step 3: Commit
bash
git add client/src/utils/api.js
git commit -m "$(cat <<'EOF'
Thread game through the sealed quick-add API client functions

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EOF
)"

Task 2: useSealedQuickAdd accepts and threads game

Files:

  • Modify: client/src/hooks/useSealedQuickAdd.js
  • Modify: client/src/hooks/useSealedQuickAdd.test.js

Interfaces:

  • Consumes: sealedProductsApi.quickAdd(game, uuid, quantity, price) / .quickAddBulk(game, items) (Task 1).

  • Produces: useSealedQuickAdd(game) (was useSealedQuickAdd() — the hook's default export now takes one required argument). add/addBulk's own signatures are unchanged (add(product, rowKey), addBulk(products, rowKeyFor)) — game is captured from the hook's own parameter, not passed per-call. Consumed by Task 3 (CatalogSealedPage.jsx's two call sites).

  • [ ] Step 1: Write the failing tests

Add to client/src/hooks/useSealedQuickAdd.test.js. First, add these imports at the top of the file (after the existing import line):

javascript
import { renderHook, act } from '@testing-library/react';

vi.mock('../utils/api', () => ({
  sealedProductsApi: {
    quickAdd: vi.fn(),
    quickAddBulk: vi.fn(),
  },
}));

import { sealedProductsApi } from '../utils/api';
import useSealedQuickAdd from './useSealedQuickAdd';

Then add this new describe block at the end of the file:

javascript
describe('useSealedQuickAdd — game threading', () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it('passes the hook-level game argument through to quickAdd', async () => {
    sealedProductsApi.quickAdd.mockResolvedValue({ data: {} });
    const { result } = renderHook(() => useSealedQuickAdd('pokemon'));

    act(() => {
      result.current.setDraftField('uuid-1', { uuid: 'uuid-1' }, 'quantity', '2');
      result.current.setDraftField('uuid-1', { uuid: 'uuid-1' }, 'price', '10');
    });

    await act(async () => {
      await result.current.add({ uuid: 'uuid-1' }, 'uuid-1');
    });

    expect(sealedProductsApi.quickAdd).toHaveBeenCalledWith('pokemon', 'uuid-1', 2, 10);
  });

  it('passes the hook-level game argument through to quickAddBulk', async () => {
    sealedProductsApi.quickAddBulk.mockResolvedValue({
      data: { added: 1, updated: 0, failed: 0, results: [{ uuid: 'uuid-1', ok: true }] },
    });
    const { result } = renderHook(() => useSealedQuickAdd('mtg'));

    act(() => {
      result.current.setDraftField('uuid-1', { uuid: 'uuid-1' }, 'quantity', '3');
    });

    await act(async () => {
      await result.current.addBulk([{ uuid: 'uuid-1' }], (p) => p.uuid);
    });

    expect(sealedProductsApi.quickAddBulk).toHaveBeenCalledWith('mtg', [{ uuid: 'uuid-1', quantity: 3, price: 0 }]);
  });
});

Need to import beforeEach and vi in the test file's existing top-level import { describe, it, expect } from 'vitest'; — update it to:

javascript
import { describe, it, expect, vi, beforeEach } from 'vitest';
  • [ ] Step 2: Run test to verify it fails

Run: npx vitest run client/src/hooks/useSealedQuickAdd.test.js --config vitest.client.config.js Expected: FAIL — sealedProductsApi.quickAdd is called with ('uuid-1', 2, 10) (3 args, no game), not ('pokemon', 'uuid-1', 2, 10) — the assertion mismatch shows the current signature.

  • [ ] Step 3: Update the hook to accept and thread game

In client/src/hooks/useSealedQuickAdd.js, change the function signature:

javascript
function useSealedQuickAdd() {

to:

javascript
function useSealedQuickAdd(game) {

Update the add callback's API call and dependency array — find:

javascript
      setQuickAddState((prev) => ({ ...prev, [rowKey]: 'adding' }));
      try {
        const { data } = await sealedProductsApi.quickAdd(product.uuid, quantity, price);

replace with:

javascript
      setQuickAddState((prev) => ({ ...prev, [rowKey]: 'adding' }));
      try {
        const { data } = await sealedProductsApi.quickAdd(game, product.uuid, quantity, price);

and its closing dependency array — find:

javascript
    },
    [drafts]
  );

  // Bulk variant for the By Release "Sync N" button:

replace with:

javascript
    },
    [drafts, game]
  );

  // Bulk variant for the By Release "Sync N" button:

Update the addBulk callback's API call — find:

javascript
      rowKeys.forEach((rowKey) => setQuickAddState((prev) => ({ ...prev, [rowKey]: 'adding' })));
      try {
        const { data } = await sealedProductsApi.quickAddBulk(items);

replace with:

javascript
      rowKeys.forEach((rowKey) => setQuickAddState((prev) => ({ ...prev, [rowKey]: 'adding' })));
      try {
        const { data } = await sealedProductsApi.quickAddBulk(game, items);

and its closing dependency array — find the addBulk callback's final lines:

javascript
        throw error;
      }
    },
    [drafts]
  );

  const reset = useCallback(() => {

replace with:

javascript
        throw error;
      }
    },
    [drafts, game]
  );

  const reset = useCallback(() => {
  • [ ] Step 4: Run test to verify it passes

Run: npx vitest run client/src/hooks/useSealedQuickAdd.test.js --config vitest.client.config.js Expected: PASS (all tests, including the two new game-threading ones)

  • [ ] Step 5: Run the full client test suite

Run: npm run test:client Expected: all suites PASS. CatalogSealedPage.test.jsx is expected to FAIL at this point — it still calls useSealedQuickAdd() indirectly via the unmodified CatalogSealedPage.jsx, which Task 3 fixes. If it fails here, that's the expected, correct signal that Task 3 is needed next — not a regression to chase down in this task.

  • [ ] Step 6: Commit
bash
git add client/src/hooks/useSealedQuickAdd.js client/src/hooks/useSealedQuickAdd.test.js
git commit -m "$(cat <<'EOF'
Accept and thread game through useSealedQuickAdd

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EOF
)"

Task 3: Widen CatalogSealedPage.jsx's gates to support Pokemon

Files:

  • Modify: client/src/pages/catalog/CatalogSealedPage.jsx
  • Modify: client/src/pages/catalog/CatalogSealedPage.test.jsx

Interfaces:

  • Consumes: useSealedQuickAdd(game) (Task 2).

  • Produces: sealedSupported now includes 'pokemon'; importSupported now includes 'pokemon'; new byReleaseSupported (selectedGame === 'mtg' only) gates the "By Release" tab's visibility, decoupled from importSupported.

  • [ ] Step 1: Write the failing tests

In client/src/pages/catalog/CatalogSealedPage.test.jsx, replace the existing "shows the not-yet-available message" test — find:

javascript
  it('shows the not-yet-available message for a game without sealed support', async () => {
    renderAt('pokemon');
    await waitFor(() => expect(screen.getByText(/not yet available for this game/i)).toBeInTheDocument());
  });

replace with (Pokemon is now supported, so this needs a game that genuinely isn't registered yet to still exercise the fallback branch):

javascript
  it('shows the not-yet-available message for a game without sealed support', async () => {
    renderAt('sorcery');
    await waitFor(() => expect(screen.getByText(/not yet available for this game/i)).toBeInTheDocument());
  });

Then add this new test immediately after the existing 'does not offer import/by-release controls for a catalog-only game (riftbound)' test (before the closing }); of the describe block):

javascript
  it('offers quick-add for Pokemon (import now supported) without the By Release tab', async () => {
    renderAt('pokemon');
    await waitFor(() => {
      expect(api.get).toHaveBeenCalledWith('/catalog/sealed', expect.objectContaining({ params: expect.objectContaining({ game: 'pokemon' }) }));
    });
    expect(screen.queryByRole('button', { name: /Browse & Import/i })).not.toBeInTheDocument();
    expect(screen.queryByRole('button', { name: /By Release/i })).not.toBeInTheDocument();
    await waitFor(() => {
      expect(screen.getByText(/Use the \+ on any product to set a price and add it to your inventory/i)).toBeInTheDocument();
    });
  });
  • [ ] Step 2: Run test to verify it fails

Run: npx vitest run client/src/pages/catalog/CatalogSealedPage.test.jsx --config vitest.client.config.js Expected: FAIL — the new Pokemon test finds the "not yet available" message instead of quick-add UI (Pokemon isn't in sealedSupported/importSupported yet); the renamed 'sorcery' test currently passes already (any unregistered game already falls through to the message) but re-run everything together to confirm the new test is the one failing.

  • [ ] Step 3: Widen the gates and thread game into the quick-add hook

In client/src/pages/catalog/CatalogSealedPage.jsx, find:

javascript
function CatalogSealedView({ selectedGame }) {
  // SEALED import + review only exists for MTG: the /sealed-products/* endpoints read from
  // mtg_sets (SetModel). Riftbound sealed is browse-only (singles sync IS supported —
  // see MULTI_GAME_ARCHITECTURE.md sealed requirements for what game-generic sealed needs).
  const sealedSupported = selectedGame === 'mtg' || selectedGame === 'riftbound';
  const importSupported = selectedGame === 'mtg';
  const [section, setSection] = useState('browse'); // 'browse' | 'byRelease'

replace with:

javascript
function CatalogSealedView({ selectedGame }) {
  // SEALED browse + import now works for both MTG and Pokemon — both have the
  // full plugin SEALED INTERFACE (see MULTI_GAME_ARCHITECTURE.md "Sealed
  // Products"). Riftbound sealed stays browse-only: singles sync IS supported
  // for Riftbound, but sealed import is a separate future effort (no plugin
  // SEALED INTERFACE for Riftbound yet).
  const sealedSupported = selectedGame === 'mtg' || selectedGame === 'riftbound' || selectedGame === 'pokemon';
  const importSupported = selectedGame === 'mtg' || selectedGame === 'pokemon';
  // "By Release" fetches its release picker from the legacy MTG-only
  // GET /api/sets endpoint (SetModel-shaped) — not the multi-game
  // /api/catalog/sets. Generalizing it to other games is a separate,
  // unscoped effort, so it stays MTG-only even though import itself now
  // supports Pokemon too.
  const byReleaseSupported = selectedGame === 'mtg';
  const [section, setSection] = useState('browse'); // 'browse' | 'byRelease'

Find:

javascript
  return (
    <div className="space-y-4 sm:space-y-6 max-w-5xl mx-auto">
      {importSupported && (
        <Card className="p-3 sm:p-4">
          <div className="flex gap-2 flex-wrap">
            <Button
              variant={section === 'browse' ? 'default' : 'outline'}
              onClick={() => setSection('browse')}
              className="flex-1 sm:flex-none min-w-[120px]"
            >
              Browse &amp; Import
            </Button>
            <Button
              variant={section === 'byRelease' ? 'default' : 'outline'}
              onClick={() => setSection('byRelease')}
              className="flex-1 sm:flex-none min-w-[120px]"
            >
              By Release
            </Button>
          </div>
        </Card>
      )}

      {section === 'byRelease' && importSupported ? (
        <SealedByRelease />
      ) : (
        <SealedBrowse selectedGame={selectedGame} quickAddEnabled={importSupported} />
      )}
    </div>
  );
}

replace with:

javascript
  return (
    <div className="space-y-4 sm:space-y-6 max-w-5xl mx-auto">
      {byReleaseSupported && (
        <Card className="p-3 sm:p-4">
          <div className="flex gap-2 flex-wrap">
            <Button
              variant={section === 'browse' ? 'default' : 'outline'}
              onClick={() => setSection('browse')}
              className="flex-1 sm:flex-none min-w-[120px]"
            >
              Browse &amp; Import
            </Button>
            <Button
              variant={section === 'byRelease' ? 'default' : 'outline'}
              onClick={() => setSection('byRelease')}
              className="flex-1 sm:flex-none min-w-[120px]"
            >
              By Release
            </Button>
          </div>
        </Card>
      )}

      {section === 'byRelease' && byReleaseSupported ? (
        <SealedByRelease />
      ) : (
        <SealedBrowse selectedGame={selectedGame} quickAddEnabled={importSupported} />
      )}
    </div>
  );
}

Finally, in the SealedBrowse function, find:

javascript
function SealedBrowse({ selectedGame, quickAddEnabled }) {
  const [sealedProducts, setSealedProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [pagination, setPagination] = useState({ page: 1, pages: 1, total: 0 });
  const [viewMode, setViewMode] = useState('grid');
  const [searchValue, setSearchValue] = useState('');
  const [setFilter, setSetFilter] = useState('');
  const [categoryFilter, setCategoryFilter] = useState('');
  const [subtypeFilter, setSubtypeFilter] = useState('');
  const [sortBy, setSortBy] = useState('releaseDate'); // releaseDate, name

  const quickAdd = useSealedQuickAdd();

replace with:

javascript
function SealedBrowse({ selectedGame, quickAddEnabled }) {
  const [sealedProducts, setSealedProducts] = useState([]);
  const [loading, setLoading] = useState(true);
  const [pagination, setPagination] = useState({ page: 1, pages: 1, total: 0 });
  const [viewMode, setViewMode] = useState('grid');
  const [searchValue, setSearchValue] = useState('');
  const [setFilter, setSetFilter] = useState('');
  const [categoryFilter, setCategoryFilter] = useState('');
  const [subtypeFilter, setSubtypeFilter] = useState('');
  const [sortBy, setSortBy] = useState('releaseDate'); // releaseDate, name

  const quickAdd = useSealedQuickAdd(selectedGame);

(SealedByRelease is untouched — it's permanently gated to MTG-only now, so its own internal useSealedQuickAdd() call still needs a game argument per Task 2's new required signature. Find, inside SealedByRelease:

javascript
  const quickAdd = useSealedQuickAdd();

replace with:

javascript
  const quickAdd = useSealedQuickAdd('mtg');

— this is the one hardcoded 'mtg' literal in this diff, and it's correct: SealedByRelease only ever renders when byReleaseSupported is true, which only happens when selectedGame === 'mtg'.)

  • [ ] Step 4: Run test to verify it passes

Run: npx vitest run client/src/pages/catalog/CatalogSealedPage.test.jsx --config vitest.client.config.js Expected: PASS (all tests, including the updated 'sorcery' test and the new Pokemon test)

  • [ ] Step 5: Run the full client test suite

Run: npm run test:client Expected: all suites PASS, no regressions (this also confirms Task 2's useSealedQuickAdd.test.js still passes now that its real-world caller is fixed).

  • [ ] Step 6: Commit
bash
git add client/src/pages/catalog/CatalogSealedPage.jsx client/src/pages/catalog/CatalogSealedPage.test.jsx
git commit -m "$(cat <<'EOF'
Enable Pokemon sealed product browse and quick-add in the catalog UI

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
EOF
)"

Task 4: Browser verification (no code changes)

Per this project's UI-change rule, passing unit tests isn't sufficient proof — the feature must actually be exercised in a browser. This task has no commit.

Files: none (verification only)

Scope of what's verifiable here: the dev server needs a real Shopify session to authenticate embedded/standalone requests, and the actual quick-add submit call creates a real Shopify product — both need live Shopify credentials this environment doesn't have access to (same blocker as PR 2's Task 10). What's fully verifiable without that: the gating/tab-visibility logic (pure client-side rendering) and the read-only /catalog/sealed browse data load for Pokemon (already proven server-side working in PR 2's Task 10 data-layer verification — this task proves the client renders that real data correctly).

  • [ ] Step 1: Confirm local infrastructure is up

Run: docker ps — expect lgs-ledger-mongodb-1 and lgs-ledger-redis-1 both Up. If not running, docker compose up -d from the repo root first.

  • [ ] Step 2: Start the client dev server only (no backend auth needed for this check)

The gating logic (Step 3 below) is pure client-side rendering and doesn't need a live backend at all — Vite's dev server can render CatalogSealedPage with a mocked/failing API call and the gating still shows correctly, since sealedSupported/importSupported/byReleaseSupported are computed purely from the :game route param, before any fetch happens.

Use the Browser tool's preview_start with a .claude/launch.json configuration (create if it doesn't exist) pointing at the client dev server:

json
{
  "version": "0.0.1",
  "configurations": [
    {
      "name": "client-dev",
      "runtimeExecutable": "npm",
      "runtimeArgs": ["run", "dev"],
      "port": 5173,
      "cwd": "client"
    }
  ]
}

Start it, then navigate the Browser pane to http://localhost:5173/catalog/pokemon/sealed (adjust the path if the app's actual routing differs — check client/src/App.jsx's route table for the exact path pattern first if this 404s).

  • [ ] Step 3: Verify gating renders correctly for Pokemon, MTG, and Riftbound

For /catalog/pokemon/sealed: confirm the page does NOT show "Sealed products are not yet available for this game" (Pokemon is now supported), confirm there is NO "Browse & Import" / "By Release" tab bar visible (since byReleaseSupported is false for Pokemon — only one section exists, so no toggle renders), and confirm the browse view's copy includes the quick-add-enabled text ("Use the + on any product to set a price and add it to your inventory").

For /catalog/mtg/sealed: confirm the tab bar IS visible with both "Browse & Import" and "By Release" buttons (unchanged from before this PR).

For /catalog/riftbound/sealed: confirm the page loads in browse-only mode (no tab bar, no quick-add-enabled copy — this game's importSupported is still false, unchanged from before this PR).

  • [ ] Step 4: Verify real Pokemon sealed data loads in the Browse view

This step needs the actual API server running too (for the /catalog/sealed fetch to return real data), not just the client. Start the server with DEV_BYPASS_AUTH=true DEV_BYPASS_SHOP=<a shop already in local Mongo, e.g. ufkes-dev-custom-app.myshopify.com> (per PR 1's session notes) alongside the client.

Navigate to /catalog/pokemon/sealed and confirm real sealed products render in the grid (the same 46 products for sv01-scarlet-violet-base-set verified server-side in PR 2's Task 10, if that data is still in the local Mongo instance — re-run node server/scripts/data-loading/updatePokemonData.js --set sv01-scarlet-violet-base-set first if it's been reset since then). Confirm each product card shows a "+" quick-add control (proving quickAddEnabled renders the control, matching MTG's existing behavior) — do NOT click it to complete a real submit, since that requires the Shopify credentials this environment doesn't have (see the scope note above).

  • [ ] Step 5: Record the result

Note in this task's write-up (for the PR description) which steps were completed and which remain for a merchant/operator to verify post-deploy (the actual quick-add submit against a real Shopify store) — mirroring exactly how PR 2's Task 10 documented its own live-verification gap.

PR 3 exit bar: MET when Steps 3-4 both confirm against a real running client + server + local Mongo data — this is the concrete "used it in a browser" proof CLAUDE.md's UI rule asks for, scoped to what's actually reachable without live Shopify credentials.


Self-review

Spec coverage:

  • Thread game through the two consumed sealedProductsApi methods — Task 1 (API layer), Task 2 (hook layer).
  • Widen sealedSupported/importSupported to include Pokemon — Task 3.
  • New byReleaseSupported gate keeping "By Release" MTG-only — Task 3.
  • Real browser verification — Task 4.
  • sealedProductsApi.list/.get/.getPricingConfig/.updatePricingConfig and SealedByRelease's internals — explicitly out of scope per Global Constraints, not touched by any task.

Placeholder scan: no TBD/"add appropriate X" language; every code step shows the literal before/after text.

Type consistency: useSealedQuickAdd(game)'s parameter name and sealedProductsApi.quickAdd(game, uuid, quantity, price)/.quickAddBulk(game, items)'s first-argument position are used identically across Tasks 1, 2, and 3's two call sites (SealedBrowse's useSealedQuickAdd(selectedGame), SealedByRelease's useSealedQuickAdd('mtg')).