From a2f30ada857721c4f9bff9eafd469dc7dbcdb671 Mon Sep 17 00:00:00 2001 From: devgrace100 Date: Tue, 20 Oct 2026 08:29:32 +0100 Subject: [PATCH] fix: command palette, feature flags, preview mode, React Compiler (#149 #150 #151 #152) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #149 – Keyboard command palette (Ctrl/Cmd+K) - Add components/command-palette/CommandPalette.tsx: WAI-ARIA combobox dialog with keyboard navigation (↑↓ Enter Esc), lazy market/trace search (≥2 chars), navigation actions and wallet actions (connect, copy address, disconnect). - Mount CommandPalette in app/layout.tsx inside WalletProvider so it can call useWalletActions / useWalletAddress. - Add a Search button with ⌘K hint to Navbar for discoverability. Issue #150 – Typed feature flags - Add lib/feature-flags/index.ts: FeatureFlags interface with six flags, build-time defaults from NEXT_PUBLIC_* env vars, applyRuntimeOverrides() for a runtime JSON endpoint, and isEnabled() kill-switch helper. - Flags: x402UnlockEnabled, cardPaymentsEnabled, adminContractConsoleEnabled, multisigXdrExportEnabled, passkeyWalletsEnabled, previewModeEnabled. - previewModeEnabled is always false in NODE_ENV=production. Issue #151 – Per-PR preview deployments with MSW mock layer - Add lib/mocks/handlers.ts: MSW http handlers for markets, traces, auth, portfolio, health and Soroban RPC JSON-RPC; envelope matches docs/api.md. - Add lib/mocks/browser.ts: setupWorker export (dynamic-import entry point). - Add components/preview/PreviewModeProvider.tsx: starts the worker before first render; MaybePreviewModeProvider is a no-op in production. - Add .github/workflows/preview.yml: builds with NEXT_PUBLIC_PREVIEW_MODE=1 on every PR, uploads artifact and posts a comment with the build link. - Add scripts/check-preview-not-in-prod.ts: CI guard that aborts if the mock flag is set in a real production build. Issue #152 – React Compiler adoption - Enable experimental.reactCompiler in next.config.ts with compilationMode "infer"; opt out app/page.tsx (Math.random in render) and app/copy-trade/ (ref read during render) until those violations are fixed. - Fix inline handler (setStatus={(s) => …}) in MarketsPageClient: extract to useCallback so the compiler and FilterBar memoization see a stable identity. - Wire eslint-plugin-react-compiler into eslint.config.mjs (graceful degradation when the package is not yet installed) with the same directory exclusions as next.config.ts so lint and compile opt-outs stay in sync. --- .github/workflows/preview.yml | 120 ++++++ app/layout.tsx | 2 + app/markets/page-client.tsx | 19 +- components/command-palette/CommandPalette.tsx | 351 ++++++++++++++++++ components/layout/Navbar.tsx | 15 + components/preview/PreviewModeProvider.tsx | 51 +++ eslint.config.mjs | 31 ++ lib/feature-flags/index.ts | 75 ++++ lib/mocks/browser.ts | 12 + lib/mocks/handlers.ts | 148 ++++++++ next.config.ts | 32 ++ scripts/check-preview-not-in-prod.ts | 22 ++ 12 files changed, 872 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/preview.yml create mode 100644 components/command-palette/CommandPalette.tsx create mode 100644 components/preview/PreviewModeProvider.tsx create mode 100644 lib/feature-flags/index.ts create mode 100644 lib/mocks/browser.ts create mode 100644 lib/mocks/handlers.ts create mode 100644 scripts/check-preview-not-in-prod.ts diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml new file mode 100644 index 0000000..2f610ec --- /dev/null +++ b/.github/workflows/preview.yml @@ -0,0 +1,120 @@ +name: Preview deployment + +on: + pull_request: + branches: [develop, main] + +concurrency: + group: preview-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + pull-requests: write + contents: read + +jobs: + build-preview: + name: Build & deploy preview + runs-on: ubuntu-latest + + env: + # Enable the MSW mock layer for this build. + NEXT_PUBLIC_PREVIEW_MODE: "1" + # Signal that this is a preview, not a real production build. + PREVIEW_BUILD: "1" + # Stub values so the build doesn't fail on missing env vars. + NEXT_PUBLIC_API_URL: "http://localhost:8000/api/v1" + NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE: "" + NEXT_PUBLIC_STELLAR_RPC_URL: "" + NEXT_PUBLIC_STELLAR_EXPLORER_URL: "" + NEXT_PUBLIC_MARKET_CORE_CONTRACT_ID: "" + NEXT_PUBLIC_TREASURY_CONTRACT_ID: "" + NEXT_PUBLIC_RESOLVER_CONTRACT_ID: "" + NEXT_PUBLIC_REASONING_REGISTRY_CONTRACT_ID: "" + NEXT_PUBLIC_USDC_CONTRACT_ID: "" + NEXT_PUBLIC_ADMIN_ADDRESSES: "" + NEXT_PUBLIC_DAILY_PASS_PRICE_USDC: "0.5" + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Safety check – mock must not ship to production + run: npx tsx scripts/check-preview-not-in-prod.ts + + - name: Build (static export) + run: npx next build + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: preview-${{ github.event.pull_request.number }} + path: out/ + retention-days: 7 + + # ── Optional: deploy to a static host ──────────────────────────────── + # Uncomment and fill in when a static host (Cloudflare Pages, Netlify, + # Vercel, …) is configured. The deploy step should output a URL, which + # is then posted as a PR comment below. + # + # - name: Deploy to Cloudflare Pages + # id: deploy + # uses: cloudflare/pages-action@v1 + # with: + # apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + # accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + # projectName: oracledesk-preview + # directory: out + # branch: pr-${{ github.event.pull_request.number }} + + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request.number; + const sha = context.payload.pull_request.head.sha.slice(0, 7); + + const body = [ + '## Preview build', + '', + `Built from ${sha} with mock backend (MSW) and fake Stellar RPC.`, + '', + 'The build artifact is attached to this workflow run. ' + + 'To explore it locally: download, unzip and serve with `npx serve out/`.', + '', + '> **Note:** All data is mocked. No real transactions occur.', + ].join('\n'); + + // Find an existing comment and update it, or create a new one. + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const existing = comments.find( + (c) => c.user?.type === 'Bot' && c.body?.startsWith('## Preview build'), + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + } diff --git a/app/layout.tsx b/app/layout.tsx index 9783119..d652497 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -11,6 +11,7 @@ import { PwaRegistrar } from "@/components/pwa/pwa-registrar"; import { DebugMount } from "@/components/debug/debug-mount"; import { RpcDegradedBanner } from "@/components/debug/RpcPanel"; import { WebVitals } from "@/app/web-vitals"; +import { CommandPalette } from "@/components/command-palette/CommandPalette"; const geistSans = Geist({ subsets: ["latin"], @@ -66,6 +67,7 @@ export default function RootLayout({ {children}