fix: resolve chain switch race condition for cross-chain project updates#1127
fix: resolve chain switch race condition for cross-chain project updates#1127
Conversation
- 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>
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.
…ailsSidebar" This reverts commit eb908cd.
- 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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
…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>
Summary
switchChainAsyncwas 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 actualeth_chainIdpolling verification (up to 20 retries, 300ms apart) that confirms the provider switched before proceeding.wallet-helpers.ts: Added chain validation insafeGetWalletClientas 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
🤖 Generated with Claude Code