Appearance
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 asx.test.js/x.test.jsx. - Do not touch
sealedProductsApi.list/.get/.getPricingConfig/.updatePricingConfig— confirmed zero client consumers today (grep -rn "sealedProductsApi\." client/srcmatches onlyuseSealedQuickAdd.js'squickAdd/quickAddBulkcalls). Wiring those up is a different, unscoped feature. - Do not touch
SealedByRelease's internals beyond nothing — it stays permanently gated behindbyReleaseSupported === (selectedGame === 'mtg'), so its existing hardcodedgame: 'mtg'fetches are still always correct; no change needed inside that function. - No
|| 'mtg'-style defaults —gameis threaded explicitly fromselectedGame(the route param), never assumed. npm run test:clientexits 0, no regressions, ≥70% coverage maintained on modified files.- Known environment issue, not a code concern: running
npm installin this worktree with the system's stale npm 6.14.8 binary corruptspackage-lock.json/client/package-lock.json(downgrades lockfileVersion). If a step needs a fresh install, run it and thengit status/git restoreany 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) — notmain. If PR 2 merges before this lands, rebase ontomainbefore 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 mergesgameonto each item before sending, matching the server'squickAddBulkSchema→z.array(quickAddSchema)where each array item requires its owngame). 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)(wasuseSealedQuickAdd()— the hook's default export now takes one required argument).add/addBulk's own signatures are unchanged (add(product, rowKey),addBulk(products, rowKeyFor)) —gameis 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:
sealedSupportednow includes'pokemon';importSupportednow includes'pokemon'; newbyReleaseSupported(selectedGame === 'mtg'only) gates the "By Release" tab's visibility, decoupled fromimportSupported.[ ] 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
gameinto 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 & 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 & 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
gamethrough the two consumedsealedProductsApimethods — Task 1 (API layer), Task 2 (hook layer). - Widen
sealedSupported/importSupportedto include Pokemon — Task 3. - New
byReleaseSupportedgate keeping "By Release" MTG-only — Task 3. - Real browser verification — Task 4.
sealedProductsApi.list/.get/.getPricingConfig/.updatePricingConfigandSealedByRelease'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')).
