Skip to content

fix: resolve chain switch race condition for cross-chain project updates#1127

Closed
Arthh wants to merge 684 commits intodevfrom
fix/chain-switch-race-condition
Closed

fix: resolve chain switch race condition for cross-chain project updates#1127
Arthh wants to merge 684 commits intodevfrom
fix/chain-switch-race-condition

Conversation

@Arthh
Copy link
Contributor

@Arthh Arthh commented Mar 21, 2026

Summary

  • Root cause: When MetaMask is on a different chain than the project's chain, the 500ms hardcoded delay after switchChainAsync was insufficient — wagmi's wallet client cache stayed stale, so the signer was created on the wrong chain, causing attestation failures.
  • ensureCorrectChain.ts: Replaced the blind 500ms delay with actual eth_chainId polling verification (up to 20 retries, 300ms apart) that confirms the provider switched before proceeding.
  • wallet-helpers.ts: Added chain validation in safeGetWalletClient as a safety net — retries up to 5 times if wagmi returns a wallet client on the wrong chain, with a descriptive error if it still doesn't match.

Test plan

  • Login with MetaMask on Arbitrum, open a project on Celo, and update it — should auto-switch to Celo and succeed
  • Login with MetaMask already on the correct chain — should proceed immediately without delay
  • Reject the MetaMask chain switch prompt — should show a clear error message
  • Test with embedded wallet (email/Google login) — should still work as before (gasless path unaffected)

🤖 Generated with Claude Code

brunodmsi and others added 30 commits February 27, 2026 21:12
- Use shared code block across both tabs via JSX variable
- Each tab gets its own header and contextual steps
- Point skill URL to base repo (github.com/show-karma/skills)
- Use existing useCopyToClipboard hook instead of custom CopyButton
- Fix word wrap (break-words instead of break-all)
feat(funding-map): add human/agent toggle card to sidebar
Add shared whitelabel entity types, programs listing and detail pages,
hooks for data fetching via React Query + fetchData, Zustand store for
UI state, and all supporting components (cards, filters, sidebar,
social links, budget badge).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Port public/private commenting system for grant applications.
Adapts whitelabel patterns to gap-app-v2 conventions: shadcn/radix
components, fetchData() API, React Query mutations with cache
invalidation, and React.memo on list items.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds public browse-applications pages with infinite scroll, authenticated
application detail/edit pages, and my-applications dashboard with filters,
stats, and pagination. All routes include loading skeletons and error
boundaries per project conventions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds modal-based application lookup feature allowing users to find their
application by reference number and see masked credentials (email/wallet).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Port the complete application form system including dynamic form rendering,
Zod schema validation, milestone management, file uploads, AI evaluation
display, access code gating, draft saving, and post-approval submissions.

Includes 40 files: types, schema builders, form utilities, React Query hooks,
and React components with field renderers for all supported question types.

Also fixes fetchData import in get-tenant-server.ts (default vs named export).
Add full multi-tenant infrastructure with domain-based tenant detection,
middleware rewriting for all whitelabel paths, tenant-aware Privy auth,
whitelabel navbar/footer, Zustand tenant store with CSS theming, and
navigation wrappers. Also includes claim funds (Hedgey integration) and
KYC verification features ported from gap-whitelabel-app.

- Overhaul middleware to rewrite ALL paths for whitelabel domains
- Add TenantConfig system with per-tenant themes, navigation, SEO
- Enable Privy auth on whitelabel (previously read-only)
- Port whitelabel navbar/footer from HeroUI to plain Tailwind
- Add claim funds pages with Hedgey smart contract integration
- Add KYC verification components and hooks
- Delete unused QueryOnlyProvider
- Fix layout and middleware tests for new async patterns
- Delete src/features/kyc/ — entire module duplicated existing
  hooks/useKycStatus.ts, types/kyc.ts, and components/KycStatusIcon.tsx
  with query key mismatch bugs that would break cache invalidation
- Replace 4 duplicate truncateAddress() functions in claim-funds with
  formatAddressForDisplay() from utilities/donations/helpers.ts
- Port ApplicationStatusChip, AccessDenied, TruncatedMarkdown, ReadMoreModal
  from whitelabel app, converting HeroUI to shadcn/Tailwind
- Add WhitelabelJsonLd component for per-tenant structured data
- Convert use-claim-transaction and use-delegated-claim to useMutation pattern
- Replace N individual readContract calls with multicall in use-claimed-status
- Add EMPTY_MAP/EMPTY_CAMPAIGNS constants to avoid new allocations per render
…, broken logos

- Add static asset path bypass in middleware for whitelabel domains
  (images, logo, tenants, icons, shared, fonts directories were being
  rewritten to /community/<slug>/... causing 404s)
- Add double-prefix stripping in middleware to prevent
  /community/optimism/community/optimism/... paths
- Add auth buttons (Sign in/out) to whitelabel navbar (desktop + mobile)
- Add "My Applications" link (auth-gated) and "Claim Funds" link
  (tenant config-gated) to navbar
- Fix broken Karma logo path in navbar and footer
  (karma-logo-white.svg → logo/karma-logo-light.svg)
- Fix "View applications" button 404 on program detail page
  (route /programs/:id/applications doesn't exist, use
  /browse-applications?programId= instead)
- Add Privy compatibility check to skip Privy on non-HTTPS custom
  hostnames (prevents SDK crash during local whitelabel dev)
- Add dev:wl script for local HTTPS whitelabel development
All application-related API endpoints were using incorrect paths that
don't exist on the backend. The whitelabel app uses /v2/funding-applications/
endpoints, not /v2/applications/community/ or /v2/communities/.

Fixes:
- Browse applications: /v2/applications/community/.../program/.../public
  → /v2/funding-applications/program/{id}
- Application details by ref: /v2/applications/community/.../public/{ref}
  → /v2/funding-applications/{ref}
- My applications: /v2/applications/community/.../my-applications
  → /v2/funding-applications/user/my-applications?communitySlug=...
- Single application: /v2/applications/{id} and /v2/communities/.../applications/{id}
  → /v2/funding-applications/{id}
- Application access: → /v2/funding-applications/{ref}/access
- Team members: → /v2/funding-applications/{ref}/team-members
- Milestone completions: → /v2/funding-applications/{ref}/milestone-completions
- Post-approval: → /v2/funding-applications/{ref}/post-approval
- Application submit: → /v2/funding-applications/{programId}
- Public applications list: → /v2/funding-applications/program/{id}
…let UI

- Replace custom absolute-positioned dropdowns with Radix DropdownMenu
  for proper focus management, click-outside handling, and animations
- Style wallet address as a pill button with green connected indicator,
  dropdown menu with Copy Address and Sign Out options
- Reorder nav links to match reference: My Applications first when
  authenticated, then Applications
- Gate Claim Funds on authenticated state (not just href existence)
- Insert Claim Funds before "More" dropdown (matching reference pattern)
- Add hover states with rounded backgrounds on all nav links
- Use lucide-react icons (ChevronDown, ExternalLink, Copy, Menu, X)
  instead of inline SVGs
- Improve mobile menu with section headers, external link indicators,
  and better spacing
- Add CommentTimeline to authenticated application page
  (/applications/[applicationId]) replacing the separate Status History
  section — the timeline merges comments and status changes chronologically
- Add PublicComments to public application details page
  (/browse-applications/[referenceNumber]) for public commenting
The API returns the Application object directly, but the queryFn was
casting it as PublicApplicationResponse and accessing data.application
which was undefined, causing "Application Not Found" on all public
application detail pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix Rules of Hooks violation in CommentTimeline (useMemo after early return)
- Add URL scheme validation in UrlRenderer to prevent javascript: XSS
- Remove test-wl.local from production domain config
- Use case-insensitive hostname matching in whitelabel config
- Remove router from useEffect deps to prevent infinite loop
- Add retry button to program list error state
- Pass search/status filters to programs API query string
- Replace raw error.message with user-friendly message in browse apps
- Remove public email display from application detail (privacy)
- Fix nested button inside Link in ProgramDetailsSidebar
- Normalize external URLs missing http(s):// prefix in social links
- Guard against undefined communityId in useCommunityBasePath
- Fix [object Object] rendering for array items containing objects

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Admin Emails is now required when editing a funding program from the
admin dashboard. Remains optional in the public program creation form
and the admin create modal.

Split shared emailFields into createEmailFields (optional) and
updateEmailFields (required with .min(1) validation). Updated the
label to show a required asterisk.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Revert workflow runners from blacksmith-4vcpu-ubuntu-2404 back to
ubuntu-latest and remove Blacksmith bot allowed_bots configuration.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: migrate reviewer removal from wallet address to email-based endpoints

Switch removeReviewer calls to use the new /by-email DELETE endpoints
that send email in request body instead of wallet address in URL path.
Update hooks, services, UI component, and tests accordingly.

* feat: migrate user entrypoints from wallet address to email input

Replace wallet address inputs with email fields across all user addition
dialogs. Users now enter an email address which is resolved to a wallet
via the resolve-email API before any on-chain or off-chain operation.

- AddAdminDialog: email-only form (name field removed per feedback)
- TransferOwnershipDialog: email input with client-side validation
- AdminTransferOwnershipDialog: Zod email schema + react-hook-form
- Add communityAdminsService with resolveEmailToWallet method
- Add tests for all three dialogs and the service layer
- Merge with origin/main (resolve conflicts in AddAdminDialog, CommunityAdmin)
Add 23 new test cases covering parsing fatal errors (empty CSV,
missing columns), validation edge cases (missing fields, slug/name
matching, decimal precision), extractProjectSlug edge cases,
toErrorReport output, and summarizeSaveResponse counting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add 3 tests documenting that name/slug matching is scoped to loaded
page data while direct UID pairs work across all pages. Update the
panel hint text so admins know to use grantUID + projectUID for
projects on other pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add bulk payout import workflow to control center

* test: expand bulk payout import test coverage

Add 23 new test cases covering parsing fatal errors (empty CSV,
missing columns), validation edge cases (missing fields, slug/name
matching, decimal precision), extractProjectSlug edge cases,
toErrorReport output, and summarizeSaveResponse counting.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add pagination-limited matching tests and clarify UI hint

Add 3 tests documenting that name/slug matching is scoped to loaded
page data while direct UID pairs work across all pages. Update the
panel hint text so admins know to use grantUID + projectUID for
projects on other pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Amaury Magalhães <amaurymagalhaesf@hotmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- Wrap decodeURIComponent in try-catch to prevent URIError on
  malformed percent-encoding outside the URL parsing block
- Remove overly permissive includes() fallback in header alias
  matching that could map "project" alias to a "projectUID" column
- Add aria-expanded and aria-controls to the expand/collapse button
  for screen reader accessibility
- Replace as never[] casts with properly typed PayoutGrantConfig mock
- Add regression tests for malformed URL decoding and header ambiguity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	__tests__/control-center/unit/bulk-payout-import.test.ts
#	components/Pages/Admin/ControlCenter/BulkPayoutImportPanel.tsx
#	components/Pages/Admin/ControlCenter/bulkPayoutImport.ts
…lter, and navigation fixes

- Add pending disbursal badge to Control Center rows showing verified-but-unpaid milestones
- Add "My Milestones" / "All Milestones" toggle for milestone reviewers on report page
- Pass reviewerAddress to pending verification API for filtered results
- Add referenceNumber fallback from milestone completion data on review page
- Fix 404 when clicking "Milestones" breadcrumb by adding redirect page
- Add unit tests for new components and reviewer filter logic

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
brunodmsi and others added 25 commits March 17, 2026 20:35
The @mention dropdown in application comments previously only showed
milestone reviewers. This adds program reviewers to the same list,
deduplicated by email so users who hold both roles appear only once.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: include program reviewers in @mention autocomplete
- Use fixed h-[90vh] instead of max-h so modal size stays consistent
  across all tabs
- Remove mr-1.5 from icons inside ui/button — the component already
  has gap-2 and [&_svg]:size-4 built in
Replace hand-rolled nav buttons with SidebarMenu, SidebarMenuItem, and
SidebarMenuButton from the shadcn sidebar component. This ensures visual
consistency with the manage sidebar being introduced in the program
setup refactor.

Brings in sidebar.tsx, sheet.tsx, and use-mobile.tsx from the
refactor/program-setup-ui branch as dependencies.
- Store grant UID instead of stale row snapshot in ControlCenterPage
  so sidebar reflects fresh data after config saves
- Add onDirtyChange/onSavingChange callbacks to PayoutConfigurationContent
  so parent sidebar footer updates reactively
- Gate form initialization on successful queries, show error/retry UI
  when config or milestones fetch fails
- Use dynamic default network instead of hardcoded chain ID 10
- Rebuild allocations from current milestones when editing existing config
  to surface milestones added after last save
- Expand dirty tracking to cover network, token, and allocation changes
- Add retry button on history error state, CTA text on empty state
- Split reset effect so agreement refreshes don't reset active tab
- Render Dialog shell even when grant is null to avoid flash
- Make table rows keyboard-accessible with tabIndex and onKeyDown
- Clean up tests: remove unused userEvent.setup(), use clearAllMocks()
- Replace window.confirm("You have unsaved changes. Discard?") with a
  proper Dialog component for design system consistency
- Use configIsDirty state instead of configRef.current?.isDirty in
  hasUnsavedChanges for proper reactivity
- Remove forwardRef wrapper from PayoutConfigurationContent (React 19
  treats ref as a regular prop)
- Update tests to verify discard dialog instead of window.confirm
…ation

Use a dataVersion counter to force child components to remount when
payout config is saved or a disbursement is created, ensuring Settings
form, History tab, and milestone edits reflect fresh data. Also wrap
the disabled Create Disbursement button in a Tooltip for better UX.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* perf: defer Privy SDK and dynamically import navbar components

Split PrivyProviderWrapper into a shell + deferred PrivyProvider to keep
the Privy SDK (~400KB) out of the initial bundle. Introduce a
PrivyBridgeContext so useAuth() reads from context defaults (ready=false)
during the loading window instead of calling usePrivy() directly.

Dynamic-import NavbarMobileMenu and NavbarUserMenu so desktop visitors
never download mobile drawer code and unauthenticated visitors skip the
user menu bundle.

Consolidate chain imports in privy-config.ts (use appNetwork IDs instead
of re-importing from @wagmi/core/chains) and simplify getExplorerUrl to
use the existing appNetwork array.

Lighthouse desktop (local prod build):
  Performance score  78 → 92  (+14)
  LCP                2.26s → 1.68s  (−26%)
  TBT                250ms → 125ms  (−50%)
  Speed Index        1.09s → 0.89s  (−18%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: include prior wave changes (dynamic imports, lazy loading)

Prior waves: dynamic imports for modals/charts, React.memo optimizations,
deferred layout components, and heavy library lazy loading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate project page loading flashes

Root cause: multiple layered loading states causing 4 visual transitions
instead of 1.

- PrivyProviderWrapper: replace useState(mounted) conditional rendering
  with useEffect + dynamic import(). Children always stay mounted —
  PrivyProvider wraps them once loaded, no blank flash from tree swap.

- Remove ssr-lcp-shell hack from project layout. The SSR shell + CSS
  sibling-selector hiding was a workaround for data not reaching client
  hooks. HydrationBoundary already merges prefetched data into the
  singleton QueryClient, so client hooks find data in cache on mount.

- loading.tsx: replace LoadingSpinner with ProjectProfileLayoutSkeleton
  so the route-level Suspense fallback matches the final layout shape.

- (profile)/layout.tsx: remove dynamic() code-split on ProjectProfileLayout.
  It's the primary layout for this route — code-splitting it adds a
  chunk-loading Suspense state between skeleton and content. The Suspense
  boundary stays (required for useSearchParams in production).

Expected flow: skeleton → full page (2 states, not 5).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: sidecar architecture for deferred Privy loading

Replace the wrapper pattern (PrivyProvider wrapping children) with a
sidecar pattern (PrivyProvider as a sibling that renders null).

Problem: when PrivyModule loaded, it wrapped children in a new provider
subtree, changing their position in the React tree. React detected a
different element type at that position and unmounted/remounted the
entire app — resetting state, re-running effects, and potentially
causing a visible flash.

Solution: PrivySidecar mounts as a sibling to children inside a stable
PrivyBridgeProvider. It creates its own PrivyProvider + WagmiProvider
subtree (renders null), reads usePrivy/useWallets/useAccount, and
pushes values into PrivyBridgeContext via setState. Children see auth
state changes as context value updates (re-render), not tree
restructures (re-mount).

Tree structure is now stable across the entire lifecycle:

  QueryClientProvider
    WagmiProvider (wagmi native, SSR)
      PrivyBridgeProvider
        PrivySidecar (lazy, renders null)
        {children} (stable position, never re-mounts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address code review issues from PR review

- Replace `any` types in PrivyBridgeValue with proper Privy types
  (User, ConnectedWallet) imported as type-only to avoid bundling
- Replace hardcoded karmahq.xyz domain with envVars.VERCEL_URL for
  logo paths (staging/whitelabel compatibility)
- Add missing Mainnet, Base (8453), and Polygon (137) RPC entries to
  privy-config transport map — these production chains were falling
  back to rate-limited public RPCs
- Add .catch() handler on Privy SDK dynamic import for graceful
  degradation when chunk loading fails (network error, ad-blocker)
- Document the intentional dual WagmiProvider pattern: outer (wagmi)
  provides SSR hook support, inner (@privy-io/wagmi) runs
  PrivyWagmiConnector for Privy↔wagmi wallet sync on shared store
- Add "use client" directive to GrantSizeSlider (imports @radix-ui)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: bridge updater stale closure and missing deps

The useEffect without a dependency array fired after every render but
captured stale closure values. Privy returns new object references each
render, so comparing by identity caused either infinite loops (if
setBridge was called unconditionally) or stale values (if guarded
by a fingerprint that didn't detect function reference changes).

Fix: depend on primitive values only (ready, authenticated, user.id,
wallets.length, isConnected) and read functions/objects from refs.
This ensures the effect fires exactly when auth state changes, and
always pushes fresh values from the latest render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore wrapper pattern — sidecar breaks direct Privy hook calls

The sidecar architecture mounted PrivyProvider as a sibling that
renders null. This broke components that call usePrivy()/useWallets()
directly (permission-context, useGaslessSigner, useZeroDevSigner,
AlreadyAppliedBanner, use-claim-transaction) — they need PrivyProvider
wrapping them, not just bridge context.

Restore the wrapper pattern: PrivyWagmiProviders wraps children with
PrivyProvider + @privy-io/wagmi WagmiProvider + PrivyBridgeUpdater.
The one-time re-mount when the dynamic import loads is acceptable
because React Query cache survives it (HydrationBoundary data persists
in the singleton QueryClient).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: updates list not refreshing after creating activity

invalidateQueries was awaited correctly, but router.refresh() was called
immediately after — triggering a full server re-render that raced with
the client-side cache refetch. The dialog closed before the refetch
completed, so the updates list showed stale data.

Fix: remove router.refresh() (redundant — invalidateQueries already
triggers a background refetch and awaiting it ensures fresh data is in
cache before the dialog closes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: CI test failures from dynamic imports and missing testid

- Mock next/dynamic in navbar test setup to resolve synchronously,
  so dynamic() components render their actual content instead of
  loading skeletons in Jest
- Update ProjectActivityChart test: component no longer has
  data-testid="chart-card", check for .animate-pulse + .bg-white
  container instead

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scope next/dynamic mock to navbar integration tests only

The global jest.mock("next/dynamic") in navbar/setup.ts ran for every
test file (setupFilesAfterEnv), breaking 16 unrelated test suites that
use dynamic() with different expectations.

Move the mock into modal-integration.test.tsx where it's actually
needed — the only test file that renders the full <Navbar> and expects
dynamically imported child components to be interactive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address second round of review issues

- Logo URL: use window.location.origin (with SSR fallback) instead of
  envVars.VERCEL_URL so whitelabel tenants on custom domains resolve
  logos correctly
- Error fallback: when Privy SDK fails to load (network/ad-blocker),
  push ready=true + authenticated=false to bridge so auth-gated pages
  redirect to login instead of showing infinite skeletons
- Use absolute @/ imports for navbar-user-skeleton and navbar-user-menu
  per project convention
- Document CSS co-location rationale in DatePicker and MarkdownPreview
  (CSS bundles with component chunk via dynamic import, not eagerly)
- Clean up dead comment in profile layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add next/dynamic mock to all navbar integration tests

Create shared setup-dynamic-mock.ts for navbar integration tests that
render <Navbar /> with dynamic() child components. Without it, dynamic()
with ssr:false renders loading skeletons in Jest and tests can't find
the real component elements.

Applied to: modal-integration, responsive-behavior, navigation-flow,
search-flow tests.

Full test suite: 397 suites, 7811 tests — all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace any type in GrantSizeSlider onChange handler

Use rc-slider's inferred type with `as number[]` cast since this is a
range slider (always returns array). Pre-existing issue, not introduced
by this PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: migrate direct Privy hook calls to bridge sidecar pattern

Switch PrivyProviderWrapper to a sidecar pattern where PrivyProvider
renders as a sibling instead of wrapping children. This prevents React
from unmounting/remounting the entire app when the dynamic import
resolves, eliminating the visible flash on first load.

- Extend PrivyBridgeContext with smartWalletClient from useSmartWallets
- Migrate permission-context, use-claim-transaction, useZeroDevSigner,
  and useGaslessSigner from direct Privy hooks to usePrivyBridge()
- Convert PrivyWagmiProviders to a sidecar (renders null, no children)
- Update PrivyLoader to render Privy as sibling, keeping children stable
- Update all affected test mocks to use bridge context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove clickable row behavior from ControlCenterTable and add an
explicit gear icon (Cog6ToothIcon) action column. This makes the
interaction more discoverable and avoids accidental sidebar opens
when clicking checkboxes or other row elements.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add disabled/loading state to PayoutConfigurationModal buttons
- Fix sidebar blanking on page change with ref-based fallback
- Extract DetailsSection and MilestonesSection into separate files
- Wrap both extracted components in React.memo
- Rename test file to match component rename (modal → sidebar)
- Remove unused hasAllocationsError variable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
feat: consolidate Control Center CTAs into sidebar navigation
* perf: defer Privy SDK and dynamically import navbar components

Split PrivyProviderWrapper into a shell + deferred PrivyProvider to keep
the Privy SDK (~400KB) out of the initial bundle. Introduce a
PrivyBridgeContext so useAuth() reads from context defaults (ready=false)
during the loading window instead of calling usePrivy() directly.

Dynamic-import NavbarMobileMenu and NavbarUserMenu so desktop visitors
never download mobile drawer code and unauthenticated visitors skip the
user menu bundle.

Consolidate chain imports in privy-config.ts (use appNetwork IDs instead
of re-importing from @wagmi/core/chains) and simplify getExplorerUrl to
use the existing appNetwork array.

Lighthouse desktop (local prod build):
  Performance score  78 → 92  (+14)
  LCP                2.26s → 1.68s  (−26%)
  TBT                250ms → 125ms  (−50%)
  Speed Index        1.09s → 0.89s  (−18%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: include prior wave changes (dynamic imports, lazy loading)

Prior waves: dynamic imports for modals/charts, React.memo optimizations,
deferred layout components, and heavy library lazy loading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate project page loading flashes

Root cause: multiple layered loading states causing 4 visual transitions
instead of 1.

- PrivyProviderWrapper: replace useState(mounted) conditional rendering
  with useEffect + dynamic import(). Children always stay mounted —
  PrivyProvider wraps them once loaded, no blank flash from tree swap.

- Remove ssr-lcp-shell hack from project layout. The SSR shell + CSS
  sibling-selector hiding was a workaround for data not reaching client
  hooks. HydrationBoundary already merges prefetched data into the
  singleton QueryClient, so client hooks find data in cache on mount.

- loading.tsx: replace LoadingSpinner with ProjectProfileLayoutSkeleton
  so the route-level Suspense fallback matches the final layout shape.

- (profile)/layout.tsx: remove dynamic() code-split on ProjectProfileLayout.
  It's the primary layout for this route — code-splitting it adds a
  chunk-loading Suspense state between skeleton and content. The Suspense
  boundary stays (required for useSearchParams in production).

Expected flow: skeleton → full page (2 states, not 5).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: sidecar architecture for deferred Privy loading

Replace the wrapper pattern (PrivyProvider wrapping children) with a
sidecar pattern (PrivyProvider as a sibling that renders null).

Problem: when PrivyModule loaded, it wrapped children in a new provider
subtree, changing their position in the React tree. React detected a
different element type at that position and unmounted/remounted the
entire app — resetting state, re-running effects, and potentially
causing a visible flash.

Solution: PrivySidecar mounts as a sibling to children inside a stable
PrivyBridgeProvider. It creates its own PrivyProvider + WagmiProvider
subtree (renders null), reads usePrivy/useWallets/useAccount, and
pushes values into PrivyBridgeContext via setState. Children see auth
state changes as context value updates (re-render), not tree
restructures (re-mount).

Tree structure is now stable across the entire lifecycle:

  QueryClientProvider
    WagmiProvider (wagmi native, SSR)
      PrivyBridgeProvider
        PrivySidecar (lazy, renders null)
        {children} (stable position, never re-mounts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address code review issues from PR review

- Replace `any` types in PrivyBridgeValue with proper Privy types
  (User, ConnectedWallet) imported as type-only to avoid bundling
- Replace hardcoded karmahq.xyz domain with envVars.VERCEL_URL for
  logo paths (staging/whitelabel compatibility)
- Add missing Mainnet, Base (8453), and Polygon (137) RPC entries to
  privy-config transport map — these production chains were falling
  back to rate-limited public RPCs
- Add .catch() handler on Privy SDK dynamic import for graceful
  degradation when chunk loading fails (network error, ad-blocker)
- Document the intentional dual WagmiProvider pattern: outer (wagmi)
  provides SSR hook support, inner (@privy-io/wagmi) runs
  PrivyWagmiConnector for Privy↔wagmi wallet sync on shared store
- Add "use client" directive to GrantSizeSlider (imports @radix-ui)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: bridge updater stale closure and missing deps

The useEffect without a dependency array fired after every render but
captured stale closure values. Privy returns new object references each
render, so comparing by identity caused either infinite loops (if
setBridge was called unconditionally) or stale values (if guarded
by a fingerprint that didn't detect function reference changes).

Fix: depend on primitive values only (ready, authenticated, user.id,
wallets.length, isConnected) and read functions/objects from refs.
This ensures the effect fires exactly when auth state changes, and
always pushes fresh values from the latest render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore wrapper pattern — sidecar breaks direct Privy hook calls

The sidecar architecture mounted PrivyProvider as a sibling that
renders null. This broke components that call usePrivy()/useWallets()
directly (permission-context, useGaslessSigner, useZeroDevSigner,
AlreadyAppliedBanner, use-claim-transaction) — they need PrivyProvider
wrapping them, not just bridge context.

Restore the wrapper pattern: PrivyWagmiProviders wraps children with
PrivyProvider + @privy-io/wagmi WagmiProvider + PrivyBridgeUpdater.
The one-time re-mount when the dynamic import loads is acceptable
because React Query cache survives it (HydrationBoundary data persists
in the singleton QueryClient).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: updates list not refreshing after creating activity

invalidateQueries was awaited correctly, but router.refresh() was called
immediately after — triggering a full server re-render that raced with
the client-side cache refetch. The dialog closed before the refetch
completed, so the updates list showed stale data.

Fix: remove router.refresh() (redundant — invalidateQueries already
triggers a background refetch and awaiting it ensures fresh data is in
cache before the dialog closes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: CI test failures from dynamic imports and missing testid

- Mock next/dynamic in navbar test setup to resolve synchronously,
  so dynamic() components render their actual content instead of
  loading skeletons in Jest
- Update ProjectActivityChart test: component no longer has
  data-testid="chart-card", check for .animate-pulse + .bg-white
  container instead

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scope next/dynamic mock to navbar integration tests only

The global jest.mock("next/dynamic") in navbar/setup.ts ran for every
test file (setupFilesAfterEnv), breaking 16 unrelated test suites that
use dynamic() with different expectations.

Move the mock into modal-integration.test.tsx where it's actually
needed — the only test file that renders the full <Navbar> and expects
dynamically imported child components to be interactive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address second round of review issues

- Logo URL: use window.location.origin (with SSR fallback) instead of
  envVars.VERCEL_URL so whitelabel tenants on custom domains resolve
  logos correctly
- Error fallback: when Privy SDK fails to load (network/ad-blocker),
  push ready=true + authenticated=false to bridge so auth-gated pages
  redirect to login instead of showing infinite skeletons
- Use absolute @/ imports for navbar-user-skeleton and navbar-user-menu
  per project convention
- Document CSS co-location rationale in DatePicker and MarkdownPreview
  (CSS bundles with component chunk via dynamic import, not eagerly)
- Clean up dead comment in profile layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add next/dynamic mock to all navbar integration tests

Create shared setup-dynamic-mock.ts for navbar integration tests that
render <Navbar /> with dynamic() child components. Without it, dynamic()
with ssr:false renders loading skeletons in Jest and tests can't find
the real component elements.

Applied to: modal-integration, responsive-behavior, navigation-flow,
search-flow tests.

Full test suite: 397 suites, 7811 tests — all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace any type in GrantSizeSlider onChange handler

Use rc-slider's inferred type with `as number[]` cast since this is a
range slider (always returns array). Pre-existing issue, not introduced
by this PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: migrate direct Privy hook calls to bridge sidecar pattern

Switch PrivyProviderWrapper to a sidecar pattern where PrivyProvider
renders as a sibling instead of wrapping children. This prevents React
from unmounting/remounting the entire app when the dynamic import
resolves, eliminating the visible flash on first load.

- Extend PrivyBridgeContext with smartWalletClient from useSmartWallets
- Migrate permission-context, use-claim-transaction, useZeroDevSigner,
  and useGaslessSigner from direct Privy hooks to usePrivyBridge()
- Convert PrivyWagmiProviders to a sidecar (renders null, no children)
- Update PrivyLoader to render Privy as sibling, keeping children stable
- Update all affected test mocks to use bridge context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ion (#1119)

* perf: defer Privy SDK and dynamically import navbar components

Split PrivyProviderWrapper into a shell + deferred PrivyProvider to keep
the Privy SDK (~400KB) out of the initial bundle. Introduce a
PrivyBridgeContext so useAuth() reads from context defaults (ready=false)
during the loading window instead of calling usePrivy() directly.

Dynamic-import NavbarMobileMenu and NavbarUserMenu so desktop visitors
never download mobile drawer code and unauthenticated visitors skip the
user menu bundle.

Consolidate chain imports in privy-config.ts (use appNetwork IDs instead
of re-importing from @wagmi/core/chains) and simplify getExplorerUrl to
use the existing appNetwork array.

Lighthouse desktop (local prod build):
  Performance score  78 → 92  (+14)
  LCP                2.26s → 1.68s  (−26%)
  TBT                250ms → 125ms  (−50%)
  Speed Index        1.09s → 0.89s  (−18%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: include prior wave changes (dynamic imports, lazy loading)

Prior waves: dynamic imports for modals/charts, React.memo optimizations,
deferred layout components, and heavy library lazy loading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate project page loading flashes

Root cause: multiple layered loading states causing 4 visual transitions
instead of 1.

- PrivyProviderWrapper: replace useState(mounted) conditional rendering
  with useEffect + dynamic import(). Children always stay mounted —
  PrivyProvider wraps them once loaded, no blank flash from tree swap.

- Remove ssr-lcp-shell hack from project layout. The SSR shell + CSS
  sibling-selector hiding was a workaround for data not reaching client
  hooks. HydrationBoundary already merges prefetched data into the
  singleton QueryClient, so client hooks find data in cache on mount.

- loading.tsx: replace LoadingSpinner with ProjectProfileLayoutSkeleton
  so the route-level Suspense fallback matches the final layout shape.

- (profile)/layout.tsx: remove dynamic() code-split on ProjectProfileLayout.
  It's the primary layout for this route — code-splitting it adds a
  chunk-loading Suspense state between skeleton and content. The Suspense
  boundary stays (required for useSearchParams in production).

Expected flow: skeleton → full page (2 states, not 5).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: sidecar architecture for deferred Privy loading

Replace the wrapper pattern (PrivyProvider wrapping children) with a
sidecar pattern (PrivyProvider as a sibling that renders null).

Problem: when PrivyModule loaded, it wrapped children in a new provider
subtree, changing their position in the React tree. React detected a
different element type at that position and unmounted/remounted the
entire app — resetting state, re-running effects, and potentially
causing a visible flash.

Solution: PrivySidecar mounts as a sibling to children inside a stable
PrivyBridgeProvider. It creates its own PrivyProvider + WagmiProvider
subtree (renders null), reads usePrivy/useWallets/useAccount, and
pushes values into PrivyBridgeContext via setState. Children see auth
state changes as context value updates (re-render), not tree
restructures (re-mount).

Tree structure is now stable across the entire lifecycle:

  QueryClientProvider
    WagmiProvider (wagmi native, SSR)
      PrivyBridgeProvider
        PrivySidecar (lazy, renders null)
        {children} (stable position, never re-mounts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address code review issues from PR review

- Replace `any` types in PrivyBridgeValue with proper Privy types
  (User, ConnectedWallet) imported as type-only to avoid bundling
- Replace hardcoded karmahq.xyz domain with envVars.VERCEL_URL for
  logo paths (staging/whitelabel compatibility)
- Add missing Mainnet, Base (8453), and Polygon (137) RPC entries to
  privy-config transport map — these production chains were falling
  back to rate-limited public RPCs
- Add .catch() handler on Privy SDK dynamic import for graceful
  degradation when chunk loading fails (network error, ad-blocker)
- Document the intentional dual WagmiProvider pattern: outer (wagmi)
  provides SSR hook support, inner (@privy-io/wagmi) runs
  PrivyWagmiConnector for Privy↔wagmi wallet sync on shared store
- Add "use client" directive to GrantSizeSlider (imports @radix-ui)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: bridge updater stale closure and missing deps

The useEffect without a dependency array fired after every render but
captured stale closure values. Privy returns new object references each
render, so comparing by identity caused either infinite loops (if
setBridge was called unconditionally) or stale values (if guarded
by a fingerprint that didn't detect function reference changes).

Fix: depend on primitive values only (ready, authenticated, user.id,
wallets.length, isConnected) and read functions/objects from refs.
This ensures the effect fires exactly when auth state changes, and
always pushes fresh values from the latest render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore wrapper pattern — sidecar breaks direct Privy hook calls

The sidecar architecture mounted PrivyProvider as a sibling that
renders null. This broke components that call usePrivy()/useWallets()
directly (permission-context, useGaslessSigner, useZeroDevSigner,
AlreadyAppliedBanner, use-claim-transaction) — they need PrivyProvider
wrapping them, not just bridge context.

Restore the wrapper pattern: PrivyWagmiProviders wraps children with
PrivyProvider + @privy-io/wagmi WagmiProvider + PrivyBridgeUpdater.
The one-time re-mount when the dynamic import loads is acceptable
because React Query cache survives it (HydrationBoundary data persists
in the singleton QueryClient).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: updates list not refreshing after creating activity

invalidateQueries was awaited correctly, but router.refresh() was called
immediately after — triggering a full server re-render that raced with
the client-side cache refetch. The dialog closed before the refetch
completed, so the updates list showed stale data.

Fix: remove router.refresh() (redundant — invalidateQueries already
triggers a background refetch and awaiting it ensures fresh data is in
cache before the dialog closes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: CI test failures from dynamic imports and missing testid

- Mock next/dynamic in navbar test setup to resolve synchronously,
  so dynamic() components render their actual content instead of
  loading skeletons in Jest
- Update ProjectActivityChart test: component no longer has
  data-testid="chart-card", check for .animate-pulse + .bg-white
  container instead

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scope next/dynamic mock to navbar integration tests only

The global jest.mock("next/dynamic") in navbar/setup.ts ran for every
test file (setupFilesAfterEnv), breaking 16 unrelated test suites that
use dynamic() with different expectations.

Move the mock into modal-integration.test.tsx where it's actually
needed — the only test file that renders the full <Navbar> and expects
dynamically imported child components to be interactive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address second round of review issues

- Logo URL: use window.location.origin (with SSR fallback) instead of
  envVars.VERCEL_URL so whitelabel tenants on custom domains resolve
  logos correctly
- Error fallback: when Privy SDK fails to load (network/ad-blocker),
  push ready=true + authenticated=false to bridge so auth-gated pages
  redirect to login instead of showing infinite skeletons
- Use absolute @/ imports for navbar-user-skeleton and navbar-user-menu
  per project convention
- Document CSS co-location rationale in DatePicker and MarkdownPreview
  (CSS bundles with component chunk via dynamic import, not eagerly)
- Clean up dead comment in profile layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add next/dynamic mock to all navbar integration tests

Create shared setup-dynamic-mock.ts for navbar integration tests that
render <Navbar /> with dynamic() child components. Without it, dynamic()
with ssr:false renders loading skeletons in Jest and tests can't find
the real component elements.

Applied to: modal-integration, responsive-behavior, navigation-flow,
search-flow tests.

Full test suite: 397 suites, 7811 tests — all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace any type in GrantSizeSlider onChange handler

Use rc-slider's inferred type with `as number[]` cast since this is a
range slider (always returns array). Pre-existing issue, not introduced
by this PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: migrate direct Privy hook calls to bridge sidecar pattern

Switch PrivyProviderWrapper to a sidecar pattern where PrivyProvider
renders as a sibling instead of wrapping children. This prevents React
from unmounting/remounting the entire app when the dynamic import
resolves, eliminating the visible flash on first load.

- Extend PrivyBridgeContext with smartWalletClient from useSmartWallets
- Migrate permission-context, use-claim-transaction, useZeroDevSigner,
  and useGaslessSigner from direct Privy hooks to usePrivyBridge()
- Convert PrivyWagmiProviders to a sidecar (renders null, no children)
- Update PrivyLoader to render Privy as sibling, keeping children stable
- Update all affected test mocks to use bridge context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: RSC sidebar, ISR, lazy ethers, font optimization, moment removal

- Convert project profile layout to async RSC with server-rendered
  SidebarProfileCardStatic, eliminating blank-content LCP
- Add ISR with 60s revalidation on project pages for CDN caching
- Dynamic import ethers in useProjectPermissions (-276KB for visitors)
- Migrate Inter font to next/font/local with display:optional (no FOUT/CLS)
- Replace moment with date-fns in fillDateRangeWithValues (-300KB)
- Remove dead moment locale webpack plugin from next.config
- Delete unused Inter.ttf (804KB), keep woff2 only
- Extract ProjectActivityChart from header (below-fold, Zustand bug)

Lighthouse /project/karma: 42 → 100 (LCP 19.8s → 1.7s, TBT 3.5s → 0ms)

* fix: resolve Turbopack ESM bundling bug breaking markdown-it

Turbopack incorrectly resolves named imports across markdown-it's
internal .mjs modules, leaving isSpace as an undefined free variable
at runtime. Force CJS bundle via resolveAlias to avoid the bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rce hints (#1120)

* perf: defer Privy SDK and dynamically import navbar components

Split PrivyProviderWrapper into a shell + deferred PrivyProvider to keep
the Privy SDK (~400KB) out of the initial bundle. Introduce a
PrivyBridgeContext so useAuth() reads from context defaults (ready=false)
during the loading window instead of calling usePrivy() directly.

Dynamic-import NavbarMobileMenu and NavbarUserMenu so desktop visitors
never download mobile drawer code and unauthenticated visitors skip the
user menu bundle.

Consolidate chain imports in privy-config.ts (use appNetwork IDs instead
of re-importing from @wagmi/core/chains) and simplify getExplorerUrl to
use the existing appNetwork array.

Lighthouse desktop (local prod build):
  Performance score  78 → 92  (+14)
  LCP                2.26s → 1.68s  (−26%)
  TBT                250ms → 125ms  (−50%)
  Speed Index        1.09s → 0.89s  (−18%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: include prior wave changes (dynamic imports, lazy loading)

Prior waves: dynamic imports for modals/charts, React.memo optimizations,
deferred layout components, and heavy library lazy loading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate project page loading flashes

Root cause: multiple layered loading states causing 4 visual transitions
instead of 1.

- PrivyProviderWrapper: replace useState(mounted) conditional rendering
  with useEffect + dynamic import(). Children always stay mounted —
  PrivyProvider wraps them once loaded, no blank flash from tree swap.

- Remove ssr-lcp-shell hack from project layout. The SSR shell + CSS
  sibling-selector hiding was a workaround for data not reaching client
  hooks. HydrationBoundary already merges prefetched data into the
  singleton QueryClient, so client hooks find data in cache on mount.

- loading.tsx: replace LoadingSpinner with ProjectProfileLayoutSkeleton
  so the route-level Suspense fallback matches the final layout shape.

- (profile)/layout.tsx: remove dynamic() code-split on ProjectProfileLayout.
  It's the primary layout for this route — code-splitting it adds a
  chunk-loading Suspense state between skeleton and content. The Suspense
  boundary stays (required for useSearchParams in production).

Expected flow: skeleton → full page (2 states, not 5).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: sidecar architecture for deferred Privy loading

Replace the wrapper pattern (PrivyProvider wrapping children) with a
sidecar pattern (PrivyProvider as a sibling that renders null).

Problem: when PrivyModule loaded, it wrapped children in a new provider
subtree, changing their position in the React tree. React detected a
different element type at that position and unmounted/remounted the
entire app — resetting state, re-running effects, and potentially
causing a visible flash.

Solution: PrivySidecar mounts as a sibling to children inside a stable
PrivyBridgeProvider. It creates its own PrivyProvider + WagmiProvider
subtree (renders null), reads usePrivy/useWallets/useAccount, and
pushes values into PrivyBridgeContext via setState. Children see auth
state changes as context value updates (re-render), not tree
restructures (re-mount).

Tree structure is now stable across the entire lifecycle:

  QueryClientProvider
    WagmiProvider (wagmi native, SSR)
      PrivyBridgeProvider
        PrivySidecar (lazy, renders null)
        {children} (stable position, never re-mounts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address code review issues from PR review

- Replace `any` types in PrivyBridgeValue with proper Privy types
  (User, ConnectedWallet) imported as type-only to avoid bundling
- Replace hardcoded karmahq.xyz domain with envVars.VERCEL_URL for
  logo paths (staging/whitelabel compatibility)
- Add missing Mainnet, Base (8453), and Polygon (137) RPC entries to
  privy-config transport map — these production chains were falling
  back to rate-limited public RPCs
- Add .catch() handler on Privy SDK dynamic import for graceful
  degradation when chunk loading fails (network error, ad-blocker)
- Document the intentional dual WagmiProvider pattern: outer (wagmi)
  provides SSR hook support, inner (@privy-io/wagmi) runs
  PrivyWagmiConnector for Privy↔wagmi wallet sync on shared store
- Add "use client" directive to GrantSizeSlider (imports @radix-ui)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: bridge updater stale closure and missing deps

The useEffect without a dependency array fired after every render but
captured stale closure values. Privy returns new object references each
render, so comparing by identity caused either infinite loops (if
setBridge was called unconditionally) or stale values (if guarded
by a fingerprint that didn't detect function reference changes).

Fix: depend on primitive values only (ready, authenticated, user.id,
wallets.length, isConnected) and read functions/objects from refs.
This ensures the effect fires exactly when auth state changes, and
always pushes fresh values from the latest render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore wrapper pattern — sidecar breaks direct Privy hook calls

The sidecar architecture mounted PrivyProvider as a sibling that
renders null. This broke components that call usePrivy()/useWallets()
directly (permission-context, useGaslessSigner, useZeroDevSigner,
AlreadyAppliedBanner, use-claim-transaction) — they need PrivyProvider
wrapping them, not just bridge context.

Restore the wrapper pattern: PrivyWagmiProviders wraps children with
PrivyProvider + @privy-io/wagmi WagmiProvider + PrivyBridgeUpdater.
The one-time re-mount when the dynamic import loads is acceptable
because React Query cache survives it (HydrationBoundary data persists
in the singleton QueryClient).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: updates list not refreshing after creating activity

invalidateQueries was awaited correctly, but router.refresh() was called
immediately after — triggering a full server re-render that raced with
the client-side cache refetch. The dialog closed before the refetch
completed, so the updates list showed stale data.

Fix: remove router.refresh() (redundant — invalidateQueries already
triggers a background refetch and awaiting it ensures fresh data is in
cache before the dialog closes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: CI test failures from dynamic imports and missing testid

- Mock next/dynamic in navbar test setup to resolve synchronously,
  so dynamic() components render their actual content instead of
  loading skeletons in Jest
- Update ProjectActivityChart test: component no longer has
  data-testid="chart-card", check for .animate-pulse + .bg-white
  container instead

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scope next/dynamic mock to navbar integration tests only

The global jest.mock("next/dynamic") in navbar/setup.ts ran for every
test file (setupFilesAfterEnv), breaking 16 unrelated test suites that
use dynamic() with different expectations.

Move the mock into modal-integration.test.tsx where it's actually
needed — the only test file that renders the full <Navbar> and expects
dynamically imported child components to be interactive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address second round of review issues

- Logo URL: use window.location.origin (with SSR fallback) instead of
  envVars.VERCEL_URL so whitelabel tenants on custom domains resolve
  logos correctly
- Error fallback: when Privy SDK fails to load (network/ad-blocker),
  push ready=true + authenticated=false to bridge so auth-gated pages
  redirect to login instead of showing infinite skeletons
- Use absolute @/ imports for navbar-user-skeleton and navbar-user-menu
  per project convention
- Document CSS co-location rationale in DatePicker and MarkdownPreview
  (CSS bundles with component chunk via dynamic import, not eagerly)
- Clean up dead comment in profile layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add next/dynamic mock to all navbar integration tests

Create shared setup-dynamic-mock.ts for navbar integration tests that
render <Navbar /> with dynamic() child components. Without it, dynamic()
with ssr:false renders loading skeletons in Jest and tests can't find
the real component elements.

Applied to: modal-integration, responsive-behavior, navigation-flow,
search-flow tests.

Full test suite: 397 suites, 7811 tests — all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace any type in GrantSizeSlider onChange handler

Use rc-slider's inferred type with `as number[]` cast since this is a
range slider (always returns array). Pre-existing issue, not introduced
by this PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: migrate direct Privy hook calls to bridge sidecar pattern

Switch PrivyProviderWrapper to a sidecar pattern where PrivyProvider
renders as a sibling instead of wrapping children. This prevents React
from unmounting/remounting the entire app when the dynamic import
resolves, eliminating the visible flash on first load.

- Extend PrivyBridgeContext with smartWalletClient from useSmartWallets
- Migrate permission-context, use-claim-transaction, useZeroDevSigner,
  and useGaslessSigner from direct Privy hooks to usePrivyBridge()
- Convert PrivyWagmiProviders to a sidecar (renders null, no children)
- Update PrivyLoader to render Privy as sibling, keeping children stable
- Update all affected test mocks to use bridge context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: RSC sidebar, ISR, lazy ethers, font optimization, moment removal

- Convert project profile layout to async RSC with server-rendered
  SidebarProfileCardStatic, eliminating blank-content LCP
- Add ISR with 60s revalidation on project pages for CDN caching
- Dynamic import ethers in useProjectPermissions (-276KB for visitors)
- Migrate Inter font to next/font/local with display:optional (no FOUT/CLS)
- Replace moment with date-fns in fillDateRangeWithValues (-300KB)
- Remove dead moment locale webpack plugin from next.config
- Delete unused Inter.ttf (804KB), keep woff2 only
- Extract ProjectActivityChart from header (below-fold, Zustand bug)

Lighthouse /project/karma: 42 → 100 (LCP 19.8s → 1.7s, TBT 3.5s → 0ms)

* perf: defer Stripe/Privy SDK, expand tree-shaking, add resource hints

- Expand optimizePackageImports with 16 packages (headlessui, radix,
  wagmi, viem, axios, semver) to reduce duplicate modules (~158KB)
- Defer Stripe OnrampFlow via dynamic() import — removes 214KB and
  267ms scripting from initial load
- Split privyConfig: outer WagmiProvider uses minimal wagmi-native
  config; full @privy-io/wagmi config deferred to lazy-loaded sidecar
  (~80-120KB off shared bundle)
- Move DynamicStars CSS from global layout to component-level import
- Add preconnect for API origin, dns-prefetch for Privy/WalletConnect/Sentry

Build time improved 15% (1m58s → 1m40s). Full impact measurable only
on Vercel deployment where network transfer and script evaluation are
the dominant bottlenecks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve Turbopack ESM bundling bug breaking markdown-it

Turbopack incorrectly resolves named imports across markdown-it's
internal .mjs modules, leaving isSpace as an undefined free variable
at runtime. Force CJS bundle via resolveAlias to avoid the bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "fix: resolve Turbopack ESM bundling bug breaking markdown-it"

This reverts commit 03123d5.

* fix: always render full client SidebarProfileCard once data loads

The RSC slot pattern was permanently replacing the client-side
SidebarProfileCard with SidebarProfileCardStatic, which is missing
Share button, verified badge, Read More link, markdown rendering,
and custom links. The static card should only be used during the
loading state (already handled in ProjectProfileLayout).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use compareAllWallets for comment ownership checks

Farcaster users with multiple wallets couldn't edit/delete their own
comments because ownership checks compared a single address. Replace all
simple address === authorAddress comparisons with compareAllWallets()
which checks against all linked wallets.

- CommentTimeline: use useAuth() + compareAllWallets internally
- use-public-commenting: canDeleteComment uses compareAllWallets
- use-application-comments: isOwner uses compareAllWallets
- CommentItem (FundingPlatform): isAuthor uses compareAllWallets
- ApplicationDetailPage: replace useAccount() with useAuth()
- Add useIsCommentAuthor hook following useIsOwner pattern

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace confirm() with DeleteDialog in CommentItem

Use the project's DeleteDialog component instead of raw window.confirm()
for comment deletion, following the established pattern for destructive
actions. The dialog supports both admin and regular user confirmation
messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…rects

URLs like app.karmahq.xyz/community/celo/programs/1059/apply were
hitting 404 because the middleware treated "community" as a tenant slug
and redirected to karmahq.xyz/community/community/celo/... — now paths
already starting with /community/ are redirected as-is.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…y-redirect

fix: prevent double /community/ prefix on legacy umbrella redirects
* perf: defer Privy SDK and dynamically import navbar components

Split PrivyProviderWrapper into a shell + deferred PrivyProvider to keep
the Privy SDK (~400KB) out of the initial bundle. Introduce a
PrivyBridgeContext so useAuth() reads from context defaults (ready=false)
during the loading window instead of calling usePrivy() directly.

Dynamic-import NavbarMobileMenu and NavbarUserMenu so desktop visitors
never download mobile drawer code and unauthenticated visitors skip the
user menu bundle.

Consolidate chain imports in privy-config.ts (use appNetwork IDs instead
of re-importing from @wagmi/core/chains) and simplify getExplorerUrl to
use the existing appNetwork array.

Lighthouse desktop (local prod build):
  Performance score  78 → 92  (+14)
  LCP                2.26s → 1.68s  (−26%)
  TBT                250ms → 125ms  (−50%)
  Speed Index        1.09s → 0.89s  (−18%)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: include prior wave changes (dynamic imports, lazy loading)

Prior waves: dynamic imports for modals/charts, React.memo optimizations,
deferred layout components, and heavy library lazy loading.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: eliminate project page loading flashes

Root cause: multiple layered loading states causing 4 visual transitions
instead of 1.

- PrivyProviderWrapper: replace useState(mounted) conditional rendering
  with useEffect + dynamic import(). Children always stay mounted —
  PrivyProvider wraps them once loaded, no blank flash from tree swap.

- Remove ssr-lcp-shell hack from project layout. The SSR shell + CSS
  sibling-selector hiding was a workaround for data not reaching client
  hooks. HydrationBoundary already merges prefetched data into the
  singleton QueryClient, so client hooks find data in cache on mount.

- loading.tsx: replace LoadingSpinner with ProjectProfileLayoutSkeleton
  so the route-level Suspense fallback matches the final layout shape.

- (profile)/layout.tsx: remove dynamic() code-split on ProjectProfileLayout.
  It's the primary layout for this route — code-splitting it adds a
  chunk-loading Suspense state between skeleton and content. The Suspense
  boundary stays (required for useSearchParams in production).

Expected flow: skeleton → full page (2 states, not 5).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: sidecar architecture for deferred Privy loading

Replace the wrapper pattern (PrivyProvider wrapping children) with a
sidecar pattern (PrivyProvider as a sibling that renders null).

Problem: when PrivyModule loaded, it wrapped children in a new provider
subtree, changing their position in the React tree. React detected a
different element type at that position and unmounted/remounted the
entire app — resetting state, re-running effects, and potentially
causing a visible flash.

Solution: PrivySidecar mounts as a sibling to children inside a stable
PrivyBridgeProvider. It creates its own PrivyProvider + WagmiProvider
subtree (renders null), reads usePrivy/useWallets/useAccount, and
pushes values into PrivyBridgeContext via setState. Children see auth
state changes as context value updates (re-render), not tree
restructures (re-mount).

Tree structure is now stable across the entire lifecycle:

  QueryClientProvider
    WagmiProvider (wagmi native, SSR)
      PrivyBridgeProvider
        PrivySidecar (lazy, renders null)
        {children} (stable position, never re-mounts)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address code review issues from PR review

- Replace `any` types in PrivyBridgeValue with proper Privy types
  (User, ConnectedWallet) imported as type-only to avoid bundling
- Replace hardcoded karmahq.xyz domain with envVars.VERCEL_URL for
  logo paths (staging/whitelabel compatibility)
- Add missing Mainnet, Base (8453), and Polygon (137) RPC entries to
  privy-config transport map — these production chains were falling
  back to rate-limited public RPCs
- Add .catch() handler on Privy SDK dynamic import for graceful
  degradation when chunk loading fails (network error, ad-blocker)
- Document the intentional dual WagmiProvider pattern: outer (wagmi)
  provides SSR hook support, inner (@privy-io/wagmi) runs
  PrivyWagmiConnector for Privy↔wagmi wallet sync on shared store
- Add "use client" directive to GrantSizeSlider (imports @radix-ui)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: bridge updater stale closure and missing deps

The useEffect without a dependency array fired after every render but
captured stale closure values. Privy returns new object references each
render, so comparing by identity caused either infinite loops (if
setBridge was called unconditionally) or stale values (if guarded
by a fingerprint that didn't detect function reference changes).

Fix: depend on primitive values only (ready, authenticated, user.id,
wallets.length, isConnected) and read functions/objects from refs.
This ensures the effect fires exactly when auth state changes, and
always pushes fresh values from the latest render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: restore wrapper pattern — sidecar breaks direct Privy hook calls

The sidecar architecture mounted PrivyProvider as a sibling that
renders null. This broke components that call usePrivy()/useWallets()
directly (permission-context, useGaslessSigner, useZeroDevSigner,
AlreadyAppliedBanner, use-claim-transaction) — they need PrivyProvider
wrapping them, not just bridge context.

Restore the wrapper pattern: PrivyWagmiProviders wraps children with
PrivyProvider + @privy-io/wagmi WagmiProvider + PrivyBridgeUpdater.
The one-time re-mount when the dynamic import loads is acceptable
because React Query cache survives it (HydrationBoundary data persists
in the singleton QueryClient).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: updates list not refreshing after creating activity

invalidateQueries was awaited correctly, but router.refresh() was called
immediately after — triggering a full server re-render that raced with
the client-side cache refetch. The dialog closed before the refetch
completed, so the updates list showed stale data.

Fix: remove router.refresh() (redundant — invalidateQueries already
triggers a background refetch and awaiting it ensures fresh data is in
cache before the dialog closes).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: CI test failures from dynamic imports and missing testid

- Mock next/dynamic in navbar test setup to resolve synchronously,
  so dynamic() components render their actual content instead of
  loading skeletons in Jest
- Update ProjectActivityChart test: component no longer has
  data-testid="chart-card", check for .animate-pulse + .bg-white
  container instead

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: scope next/dynamic mock to navbar integration tests only

The global jest.mock("next/dynamic") in navbar/setup.ts ran for every
test file (setupFilesAfterEnv), breaking 16 unrelated test suites that
use dynamic() with different expectations.

Move the mock into modal-integration.test.tsx where it's actually
needed — the only test file that renders the full <Navbar> and expects
dynamically imported child components to be interactive.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address second round of review issues

- Logo URL: use window.location.origin (with SSR fallback) instead of
  envVars.VERCEL_URL so whitelabel tenants on custom domains resolve
  logos correctly
- Error fallback: when Privy SDK fails to load (network/ad-blocker),
  push ready=true + authenticated=false to bridge so auth-gated pages
  redirect to login instead of showing infinite skeletons
- Use absolute @/ imports for navbar-user-skeleton and navbar-user-menu
  per project convention
- Document CSS co-location rationale in DatePicker and MarkdownPreview
  (CSS bundles with component chunk via dynamic import, not eagerly)
- Clean up dead comment in profile layout

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add next/dynamic mock to all navbar integration tests

Create shared setup-dynamic-mock.ts for navbar integration tests that
render <Navbar /> with dynamic() child components. Without it, dynamic()
with ssr:false renders loading skeletons in Jest and tests can't find
the real component elements.

Applied to: modal-integration, responsive-behavior, navigation-flow,
search-flow tests.

Full test suite: 397 suites, 7811 tests — all passing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: replace any type in GrantSizeSlider onChange handler

Use rc-slider's inferred type with `as number[]` cast since this is a
range slider (always returns array). Pre-existing issue, not introduced
by this PR.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: migrate direct Privy hook calls to bridge sidecar pattern

Switch PrivyProviderWrapper to a sidecar pattern where PrivyProvider
renders as a sibling instead of wrapping children. This prevents React
from unmounting/remounting the entire app when the dynamic import
resolves, eliminating the visible flash on first load.

- Extend PrivyBridgeContext with smartWalletClient from useSmartWallets
- Migrate permission-context, use-claim-transaction, useZeroDevSigner,
  and useGaslessSigner from direct Privy hooks to usePrivyBridge()
- Convert PrivyWagmiProviders to a sidecar (renders null, no children)
- Update PrivyLoader to render Privy as sibling, keeping children stable
- Update all affected test mocks to use bridge context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: RSC sidebar, ISR, lazy ethers, font optimization, moment removal

- Convert project profile layout to async RSC with server-rendered
  SidebarProfileCardStatic, eliminating blank-content LCP
- Add ISR with 60s revalidation on project pages for CDN caching
- Dynamic import ethers in useProjectPermissions (-276KB for visitors)
- Migrate Inter font to next/font/local with display:optional (no FOUT/CLS)
- Replace moment with date-fns in fillDateRangeWithValues (-300KB)
- Remove dead moment locale webpack plugin from next.config
- Delete unused Inter.ttf (804KB), keep woff2 only
- Extract ProjectActivityChart from header (below-fold, Zustand bug)

Lighthouse /project/karma: 42 → 100 (LCP 19.8s → 1.7s, TBT 3.5s → 0ms)

* perf: defer Stripe/Privy SDK, expand tree-shaking, add resource hints

- Expand optimizePackageImports with 16 packages (headlessui, radix,
  wagmi, viem, axios, semver) to reduce duplicate modules (~158KB)
- Defer Stripe OnrampFlow via dynamic() import — removes 214KB and
  267ms scripting from initial load
- Split privyConfig: outer WagmiProvider uses minimal wagmi-native
  config; full @privy-io/wagmi config deferred to lazy-loaded sidecar
  (~80-120KB off shared bundle)
- Move DynamicStars CSS from global layout to component-level import
- Add preconnect for API origin, dns-prefetch for Privy/WalletConnect/Sentry

Build time improved 15% (1m58s → 1m40s). Full impact measurable only
on Vercel deployment where network transfer and script evaluation are
the dominant bottlenecks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: resolve Turbopack ESM bundling bug breaking markdown-it

Turbopack incorrectly resolves named imports across markdown-it's
internal .mjs modules, leaving isSpace as an undefined free variable
at runtime. Force CJS bundle via resolveAlias to avoid the bug.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "fix: resolve Turbopack ESM bundling bug breaking markdown-it"

This reverts commit 03123d5.

* fix: always render full client SidebarProfileCard once data loads

The RSC slot pattern was permanently replacing the client-side
SidebarProfileCard with SidebarProfileCardStatic, which is missing
Share button, verified badge, Read More link, markdown rendering,
and custom links. The static card should only be used during the
loading state (already handled in ProjectProfileLayout).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: lazy-init Web3 clients, remove bundle leaks, defer Privy for anonymous users

- Replace 12 eager createPublicClient() calls in rpcClient.ts with Map-based
  memoized factory + Proxy pattern (zero clients created at import time)
- Replace eager ENS client in fetchENS.ts with lazy getEnsClient() singleton
- Remove static re-exports of AlchemyProvider/ZeroDevProvider from gasless
  providers index (prevents tree-shaking bypass)
- Add multiInjectedProviderDiscovery: false to minimalWagmiConfig (Privy
  handles wallet discovery)
- Add content-visibility: auto CSS utility class for below-fold content
- Defer Privy SDK import for anonymous users via requestIdleCallback with
  5s timeout; returning users (privy:token in localStorage) load immediately
- Add useLoadPrivy() hook to bridge context for on-demand Privy loading

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The SSR-fetched application has ownerAddress sanitized to "" (backend
strips it for unauthenticated requests). useIsOwner("") always returned
false, hiding the edit button for application owners.

Replace useIsOwner(application.ownerAddress) with useApplicationAccess()
which makes an authenticated client-side call to the checkAccess endpoint
that correctly resolves multi-wallet ownership.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The wallet-switch detection used a snapshot of wagmi wallet addresses
to detect unauthorized wallet switches. For Farcaster users, Privy
syncs the custody (linked) wallet to wagmi AFTER the embedded wallet
is snapshotted, causing watchAccount to see an "unknown" address and
trigger logout.

Replace snapshot-based check with compareAllWallets(user, address)
which checks against all linked accounts (wallets, smart wallets,
farcaster custody, cross-app) from the Privy user object.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Temporary diagnostic logging to identify which code path is causing
Farcaster users to be logged out immediately after login. Each logout
trigger logs a unique [AUTH-DEBUG] message to the console.

Will be removed once the root cause is identified.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: prevent false logout and show comments for app owners

Two fixes for Farcaster multi-wallet users:

1. useAuth: Skip wallet-switch detection for social login users without
   external wallets. A stale wagmi connection from a previous wallet
   session was triggering logout because the MetaMask address wasn't
   in the Farcaster user's linkedAccounts.

2. ApplicationPageClient: Show comments section based on
   Permission.APPLICATION_COMMENT instead of only when
   showCommentsOnPublicPage is enabled. Application owners, admins,
   and reviewers should always see comments.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: update ApplicationPageClient tests for useApplicationAccess

Mock useApplicationAccess instead of relying on useIsOwner/compareAllWallets
since ownership is now determined by the backend access check. Add missing
`can` mock to usePermissionContext.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: use CommentTimeline for authenticated users, PublicComments for guests

Authenticated users with APPLICATION_COMMENT permission now get the full
CommentTimeline component (uses /comments authenticated endpoint) instead
of PublicComments (uses /comments/public which requires showCommentsOnPublicPage).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…updates to fail

When a user's MetaMask was on a different chain (e.g., Arbitrum) than the project's
chain (e.g., Celo), project updates would fail because the signer was created before
wagmi's wallet client cache reflected the chain switch. The previous 500ms hardcoded
delay was insufficient.

- ensureCorrectChain: replace blind delay with eth_chainId polling verification
- safeGetWalletClient: add chain validation with retry for stale wagmi cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@vercel
Copy link

vercel bot commented Mar 21, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gap-app-v2 Ready Ready Preview, Comment Mar 21, 2026 7:15pm

Request Review

@coderabbitai
Copy link

coderabbitai bot commented Mar 21, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: deee0313-3610-4e14-ae43-ec484bd9a761

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/chain-switch-race-condition

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

…rification

window.ethereum doesn't correspond to the actual provider wagmi/Privy uses
(EIP-6963, injected wallet aggregation, Privy wrapping), causing the
verification to always fail even after a successful switch.

- ensureCorrectChain: trust switchChainAsync promise resolution, remove
  window.ethereum polling that doesn't work with Privy
- safeGetWalletClient: call reconnect() to flush wagmi's stale connector
  cache before retrying wallet client fetch

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants