Skip to content

feat: improve tools panel flow and active result context#23

Merged
rohan-patnaik merged 6 commits intomainfrom
codex/donation-no-premium-pool
Feb 7, 2026
Merged

feat: improve tools panel flow and active result context#23
rohan-patnaik merged 6 commits intomainfrom
codex/donation-no-premium-pool

Conversation

@rohan-patnaik
Copy link
Copy Markdown
Owner

@rohan-patnaik rohan-patnaik commented Feb 6, 2026

Summary

  • make top navigation/header bar fully opaque in both light and dark themes (remove translucency)
  • improve tool flow discoverability:
    • when a user clicks any tool in the left column, the page now auto-scrolls to the active-tool panel
  • enhance active-tool result context near primary action buttons:
    • keep Download beside Queue job
    • add an elegant inline latest-result summary strip showing:
      • output filename
      • status badge (Succeeded)
      • completion timestamp
      • output/input sizing summary (Output X from Y)
      • compression-specific savings when available (Saved Z (P%))
    • clear this context when switching to another tool so it stays tool-scoped

Validation

  • cd apps/web && npm run lint
  • cd apps/web && npx tsc --noEmit
  • cd apps/web && npm test
  • cd apps/web && npm run build

CodeRabbit

  • ran coderabbit review --plain --type uncommitted --base main
  • addressed actionable feedback:
    • made the active panel status badge derive from actual job status, not a hardcoded label
  • attempted final rerun, but CLI returned temporary rate limit (try after ~3m26s)

Scope

  • UI-only changes in tools workflow and nav styling
  • no backend/API/schema/auth-contract changes

Summary by CodeRabbit

  • New Features

    • Contextual results panel with downloadable artifacts and summaries.
    • Runtime environment indicator (Local vs Hosted) shown in header (compact on mobile).
    • Capacity & enforcement pages explaining limits, pooling, and access.
    • UPI payment flow: "Pay via UPI", mobile launch feedback, and QR/scan guidance.
  • Style

    • Dark-theme palette refresh, updated component styling, opaque nav, responsive spacing and typography.
  • Bug Fixes / Improvements

    • Theme handling more resilient in restricted environments.
    • App icons/metadata and README preview/links added.
  • Chores

    • Ignore rule added for deployment directory.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

Updates UI theme and spacing, adds a runtime environment indicator and header/layout changes, enhances the tools page with contextual job/result tracking and download actions, adds a Capacity Logic page and global limits, refactors Donate/UPI flow, wires Clerk publishable key and proxy auth checks, and introduces several stability and storage-tolerance tweaks.

Changes

Cohort / File(s) Summary
Global styling & theme
apps/web/src/app/globals.css
Reworked dark-mode color tokens (forest/leaf), removed wood-nav translucency/backdrop-filter, added runtime-indicator styles, responsive tweaks, and adjusted borders/shadows.
Tools page & job context
apps/web/src/app/tools/page.tsx
Added finishedAt?: number to JobRecord; introduced contextual job/tool state (contextJobId, contextToolId), contextResult derivation, toolCatalog UI, contextual results display + Download action, selection/reset/scroll anchoring, and expanded success status handling.
Header & runtime indicator
apps/web/src/components/SiteHeader.tsx, apps/web/src/components/RuntimeEnvironmentIndicator.tsx, apps/web/src/app/layout.tsx
Redesigned header (desktop + mobile rows), added RuntimeEnvironmentIndicator (compact + desktop variants) into header, and added icons entries to app metadata.
Donate / UPI flow
apps/web/src/components/DonateBookmark.tsx
Added UPI helpers (normalization, pick-first), mobile detection, launchUpiApp flow with timeout/fallback and launch messaging, QR resolution prioritization, and related UI/state changes and cleanup.
Auth & proxy
apps/web/src/proxy.ts, apps/web/src/app/providers.tsx
Simplified proxy bypass logic, added missing-Clerk-env validation (returns 500 with error), introduced NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY usage and passed publishableKey to ClerkProvider.
Capacity docs & limits
apps/web/src/app/capacity-logic/page.tsx, apps/web/src/lib/limits.ts
Added a new Capacity Logic static page and exported DEFAULT_GLOBAL_LIMITS for global quotas.
Content/spacing tweaks
apps/web/src/app/page.tsx, apps/web/src/app/self-host/page.tsx, apps/web/src/app/usage-capacity/page.tsx
Reduced paddings and heading sizes for denser layout, added capacity-logic navigation links, and adjusted responsive spacing and card paddings.
Runtime/UX components
apps/web/src/components/ThemeModeProvider.tsx, apps/web/src/components/RuntimeEnvironmentIndicator.tsx, apps/web/src/components/SiteHeader.tsx
Added runtime indicator component and inserted into header; wrapped localStorage reads/writes in try/catch to tolerate unavailable storage.
Misc / repo
README.md, .gitignore
Added live app URLs and preview image to README; added .vercel to .gitignore.

Sequence Diagram(s)

sequenceDiagram
participant User
participant UI as Web UI (tools/page)
participant API as Server/Convex
participant Storage as Artifact/Download

User->>UI: Select tool / create job
UI->>API: Create job request (toolId, inputs)
API-->>UI: Job queued (jobId)
Note right of API: Job executes asynchronously
API->>Storage: Store artifact/result (on completion)
API-->>UI: Job status update (succeeded/done, finishedAt, artifact metadata)
UI->>UI: derive contextResult/contextResultSummary
User->>UI: Click Download contextual result
UI->>Storage: Download artifact (signed URL or proxy)
Storage-->>User: Return file
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through CSS and header glows,
I sniffed the jobs where contextual data grows.
A UPI nudge, a proxy check so neat,
I fetched the artefacts and handed out a treat.
Carrots saved, the download drum goes beat. 🎋

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat: improve tools panel flow and active result context' directly and clearly summarizes the main changes: enhancing the tools panel flow (auto-scroll to active tool) and adding active result context (inline result summary). It accurately reflects the primary objectives of the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codex/donation-no-premium-pool

No actionable comments were generated in the recent review. 🎉


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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@apps/web/src/app/tools/page.tsx`:
- Around line 806-843: The contextResult compute currently filters out any
non-"succeeded" jobs (see useMemo block and the normalizedStatus check that
returns undefined unless normalizedStatus === "succeeded"), making the status
badge branches for "failed" and "running" unreachable; fix by either (A)
allowing in-progress/failed context by removing the normalizedStatus !==
"succeeded" guard so contextResult is returned for matchingJob/output regardless
of status (and keep normalizedStatus in the returned object), or (B) if the
intent is succeeded-only, simplify the UI by removing the "failed"/"running"
branches and always render "Succeeded" for contextResult — update the useMemo
logic around matchingJob, normalizedStatus, and the UI badge accordingly (refer
to contextResult, normalizedStatus, matchingJob, and the status badge rendering
code).
🧹 Nitpick comments (2)
apps/web/src/app/tools/page.tsx (2)

806-843: contextResult duplicates logic already in JobCard.

The input-size extraction (Lines 821–824), compression-result derivation (Lines 825–830), and savings formatting are repeated from JobCard. Consider extracting a shared helper (e.g., deriveJobSummary(job)) to keep these in sync and reduce duplication.

Also applies to: 846-879


1279-1325: Inline context result block — minor accessibility note.

The download button and summary strip appear/disappear based on contextResult. Screen reader users won't be notified of the new content. Consider wrapping this block in an element with role="status" or aria-live="polite" (similar to the status message at Line 1329) so assistive technology announces the result availability.

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 7, 2026

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

Project Deployment Actions Updated (UTC)
skill-deploy-0brhd1ir9e Error Error Feb 7, 2026 9:26pm
thezenpdf Ready Ready Preview, Comment Feb 7, 2026 9:26pm

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In `@apps/web/src/app/providers.tsx`:
- Around line 28-29: The ClerkProvider is being passed publishableKey even when
clerkPublishableKey may be undefined; update the component usage (ClerkProvider)
to either remove the publishableKey prop entirely so Clerk auto-detects
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, or conditionally pass it only when defined
(use a conditional spread or ternary around clerkPublishableKey) so you don't
pass publishableKey={undefined}; locate the ClerkProvider declaration in
providers.tsx and change the prop handling accordingly.

In `@apps/web/src/components/DonateBookmark.tsx`:
- Around line 55-56: In DonateBookmark, qrUrl is always set to a truthy value
because it falls back to defaultUpiQrImage, which prevents generatedQrUrl from
ever being used; change qrUrl to only reflect the explicit config (const qrUrl =
configuredQrUrl) and update resolvedQrUrl to prefer generated QR when no
explicit URL is provided (resolvedQrUrl = qrUrl || generatedQrUrl ||
defaultUpiQrImage), and adjust the QR generation useEffect to only run when
there is no configuredQrUrl (useEffect dependency on configuredQrUrl and guard
so it generates only when configuredQrUrl is empty) so dynamic generation is
used when appropriate and the static fallback only applies last.

In `@apps/web/src/components/SiteHeader.tsx`:
- Around line 60-90: Two sibling <nav> landmarks in SiteHeader.tsx both use
aria-label="Primary", causing duplicate navigation announcements; update the
aria-labels on the two <nav> elements to be unique (e.g., change the desktop nav
— the element with className "hidden min-w-0 flex-1 items-center justify-end
gap-2 text-sm lg:flex lg:flex-wrap" — to "Primary navigation" and the mobile nav
— the element with className "mobile-scroll-row flex min-w-0 flex-1 items-center
gap-2 overflow-x-auto pb-1 text-sm" — to "Primary navigation (mobile)"), or
alternatively refactor to render a single <nav> responsively; make the change in
the two <nav> elements in SiteHeader.tsx so screen readers see distinct
landmarks.
🧹 Nitpick comments (5)
apps/web/src/components/RuntimeEnvironmentIndicator.tsx (1)

7-8: Regex accepts octets > 255 (e.g., 10.999.999.999).

\d{1,3} matches 0–999, not just valid octets 0–255. In practice this is harmless since browsers won't produce invalid IP hostnames, but worth noting for correctness.

apps/web/src/app/globals.css (1)

247-253: Hidden scrollbar may reduce scroll discoverability.

The .mobile-scroll-row hides both the standard scrollbar (scrollbar-width: none) and WebKit scrollbar. For horizontally scrollable content (the "Quick switch" tool row in tools/page.tsx), users may not realize the row is scrollable. Consider adding a visual affordance like a fade/gradient at the edges or keeping a thin scrollbar visible.

apps/web/src/app/tools/page.tsx (2)

783-810: toolCatalog references selectTool before its declaration — safe at runtime but fragile.

toolCatalog is defined at Line 783 and calls selectTool in its onClick handlers, but selectTool is declared at Line 909. This works because the click handlers are closures that only resolve selectTool at invocation time (after the component body has fully executed), so there's no TDZ error. However, this ordering is fragile — if someone refactors selectTool into a different scope or an early-invoked callback, it would break silently. Consider moving toolCatalog below selectTool, or extracting it as a local component.


1149-1155: Tool catalog is rendered in two DOM locations.

toolCatalog is rendered both inside a <details> (mobile, lg:hidden) and a <div> (desktop, hidden lg:block). This means the full catalog DOM exists twice. This is a standard responsive pattern, but with 26 tools × 2, it's a non-trivial amount of duplicated DOM. If performance on low-end devices becomes a concern, consider conditionally rendering based on a media query hook instead.

apps/web/src/components/DonateBookmark.tsx (1)

37-40: Hardcoded personal UPI ID in source code.

defaultUpiId contains a real UPI identifier (rohanpatnaik1997-1@okhdfcbank). While this is intentional for the donation use case and overridable via NEXT_PUBLIC_DONATE_UPI_ID, be aware this is committed to the public repo. For a personal open-source project this is likely acceptable, but consider documenting that self-hosters should override these defaults.

Comment on lines 28 to +29
<ClerkProvider
publishableKey={clerkPublishableKey}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

ClerkProvider publishableKey prop undefined fallback environment variable

💡 Result:

In Next.js, you typically don’t need to pass publishableKey to <ClerkProvider /> at all—Clerk will read it from the environment as long as you set:

  • NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_(test|live)_... (frontend) (clerk.com)

So if you’re seeing publishableKey as undefined, the usual fixes are:

  1. Set the correct env var name
  • Use NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY (not just CLERK_PUBLISHABLE_KEY) for anything that runs in the browser. (clerk.com)
  1. Restart the dev server after editing .env*
  • Next.js won’t pick up new env vars reliably without a restart (stop + start).
  1. Don’t pass an undefined prop
  • This is fine (recommended) if your env var is set:
    <ClerkProvider>{children}</ClerkProvider>
    Clerk’s Next.js package explicitly documents relying on NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY for the Provider. (npmjs.com)
  1. If deployed, ensure the env var exists in the hosting provider
  • The common “Missing publishableKey” runtime error happens when the deployed environment doesn’t actually have NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY available. (stackoverflow.com)

If you tell me your framework target (@clerk/nextjs vs @clerk/clerk-react, App Router vs Pages Router, and where you set env vars—local vs Vercel/Netlify/etc.), I can narrow it down to the exact cause.

Citations:


🏁 Script executed:

head -40 apps/web/src/app/providers.tsx

Repository: rohan-patnaik/ZenPDF

Length of output: 1438


Remove or conditionally pass publishableKey to align with Clerk's documented best practice.

Clerk explicitly recommends omitting the publishableKey prop entirely and letting it auto-detect from NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY. Passing publishableKey={undefined} in development is not a documented fallback pattern. Either omit the prop: <ClerkProvider> or conditionally pass it only when defined:

<ClerkProvider
  publishableKey={clerkPublishableKey}
  signInUrl="/sign-in"
  signUpUrl="/sign-up"

should be:

<ClerkProvider
  {...(clerkPublishableKey && { publishableKey: clerkPublishableKey })}
  signInUrl="/sign-in"
  signUpUrl="/sign-up"

Or simply remove the prop and trust Clerk's environment variable detection.

🤖 Prompt for AI Agents
In `@apps/web/src/app/providers.tsx` around lines 28 - 29, The ClerkProvider is
being passed publishableKey even when clerkPublishableKey may be undefined;
update the component usage (ClerkProvider) to either remove the publishableKey
prop entirely so Clerk auto-detects NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, or
conditionally pass it only when defined (use a conditional spread or ternary
around clerkPublishableKey) so you don't pass publishableKey={undefined}; locate
the ClerkProvider declaration in providers.tsx and change the prop handling
accordingly.

Comment on lines +60 to +90
<nav
className="hidden min-w-0 flex-1 items-center justify-end gap-2 text-sm lg:flex lg:flex-wrap"
aria-label="Primary"
>
Usage & Capacity
</Link>
<ThemeToggle />
<SignedOut>
<SignInButton mode="modal">
<button className="paper-button" type="button">
Sign in with Google
</button>
</SignInButton>
</SignedOut>
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
</nav>
{navLinks}
<RuntimeEnvironmentIndicator />
<SignedOut>
<SignInButton mode="modal">
<button className="paper-button hidden lg:inline-flex" type="button">
Sign in with Google
</button>
</SignInButton>
</SignedOut>
<div className="ml-1 flex shrink-0 items-center gap-2">
<ThemeToggle />
<SignedIn>
<UserButton afterSignOutUrl="/" />
</SignedIn>
</div>
</nav>
</div>

<div className="mt-2 flex items-center gap-2 lg:hidden">
<nav
className="mobile-scroll-row flex min-w-0 flex-1 items-center gap-2 overflow-x-auto pb-1 text-sm"
aria-label="Primary"
>
{navLinks}
</nav>
<RuntimeEnvironmentIndicator compact />
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Duplicate aria-label="Primary" on two sibling <nav> landmarks.

Both the desktop nav (line 60) and mobile nav (line 83) carry aria-label="Primary". CSS responsive hiding (hidden/lg:hidden) doesn't remove elements from the accessibility tree, so screen readers will announce two "Primary" navigation landmarks. Differentiate them — e.g., "Primary navigation" and "Primary navigation (mobile)" — or render only one <nav> and adjust its contents responsively.

Proposed fix
           <nav
-            className="hidden min-w-0 flex-1 items-center justify-end gap-2 text-sm lg:flex lg:flex-wrap"
-            aria-label="Primary"
+            className="hidden min-w-0 flex-1 items-center justify-end gap-2 text-sm lg:flex lg:flex-wrap"
+            aria-label="Primary navigation"
           >
           <nav
-            className="mobile-scroll-row flex min-w-0 flex-1 items-center gap-2 overflow-x-auto pb-1 text-sm"
-            aria-label="Primary"
+            className="mobile-scroll-row flex min-w-0 flex-1 items-center gap-2 overflow-x-auto pb-1 text-sm"
+            aria-label="Primary navigation (mobile)"
           >
🤖 Prompt for AI Agents
In `@apps/web/src/components/SiteHeader.tsx` around lines 60 - 90, Two sibling
<nav> landmarks in SiteHeader.tsx both use aria-label="Primary", causing
duplicate navigation announcements; update the aria-labels on the two <nav>
elements to be unique (e.g., change the desktop nav — the element with className
"hidden min-w-0 flex-1 items-center justify-end gap-2 text-sm lg:flex
lg:flex-wrap" — to "Primary navigation" and the mobile nav — the element with
className "mobile-scroll-row flex min-w-0 flex-1 items-center gap-2
overflow-x-auto pb-1 text-sm" — to "Primary navigation (mobile)"), or
alternatively refactor to render a single <nav> responsively; make the change in
the two <nav> elements in SiteHeader.tsx so screen readers see distinct
landmarks.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/web/src/components/DonateBookmark.tsx`:
- Around line 73-78: The current useMemo-based isProbablyMobileDevice runs
during SSR (returning false) causing hydration mismatches; replace the useMemo
constant with a function named isProbablyMobileDevice() that checks typeof
window !== "undefined" and tests navigator.userAgent at call time, and update
launchUpiApp to call isProbablyMobileDevice() inside the click handler instead
of reading a memoized value; remove the useMemo import/usage so the device check
never runs during SSR and only executes on the client when the handler is
invoked.

In `@apps/web/src/components/SiteHeader.tsx`:
- Line 56: The <UserButton> component is passing the deprecated afterSignOutUrl
prop; remove the prop from <UserButton> (symbol: UserButton) since
afterSignOutUrl="/" is redundant, or if you need a global custom redirect, move
the setting into the ClerkProvider configuration in your providers.tsx (symbol:
ClerkProvider / providers.tsx) by setting the afterSignOutUrl there instead of
on UserButton.
🧹 Nitpick comments (4)
apps/web/src/components/DonateBookmark.tsx (2)

378-378: Redundant fallback: upiId already incorporates defaultUpiId.

upiId is derived via pickFirstNonEmptyValue(..., defaultUpiId) followed by normalizeUpiId, so it will always equal the normalized default when the env var is unset. The || defaultUpiId here would only kick in if normalizeUpiId returned "", which it can't given a non-empty input. The fallback is dead code and slightly misleading.

-                {upiId || defaultUpiId}
+                {upiId}

156-169: Consider extracting the repeated timeout-cleanup logic.

The same clear-timeout-and-nullify pattern appears in handleOpen, handleClose, and the unmount effect. A small helper would reduce duplication:

Example
+  const clearUpiLaunchTimeout = useCallback(() => {
+    if (upiLaunchTimeoutRef.current !== null) {
+      window.clearTimeout(upiLaunchTimeoutRef.current);
+      upiLaunchTimeoutRef.current = null;
+    }
+  }, []);

Then replace each inline block with clearUpiLaunchTimeout().

Also applies to: 171-181

apps/web/src/app/tools/page.tsx (2)

834-872: Past review concern is properly addressed — status badge is now always "Succeeded".

The normalizedStatus filter (Line 846) still computes "failed" / "running" branches (Lines 843-845) that can never survive the guard. Consider simplifying:

♻️ Optional simplification to remove dead normalization branches
-    const normalizedStatus =
-      matchingJob?.status === "succeeded" || matchingJob?.status === "done"
-        ? "succeeded"
-        : matchingJob?.status === "failed"
-          ? "failed"
-          : "running";
-    if (!matchingJob || !output || normalizedStatus !== "succeeded") {
+    const isSucceeded =
+      matchingJob?.status === "succeeded" || matchingJob?.status === "done";
+    if (!matchingJob || !output || !isSucceeded) {
       return undefined;
     }

Then drop status from the returned object (the UI already hardcodes "Succeeded").


1174-1193: Quick-switch horizontal row for mobile — minor accessibility enhancement possible.

The row has aria-pressed on each button (good), but the scrollable container itself has no accessible label. A screen reader user encountering this section won't know its purpose without visual context.

♻️ Suggested enhancement
-            <div className="mt-4 lg:hidden">
-              <p className="field-label">Quick switch</p>
-              <div className="mobile-scroll-row mt-2 flex gap-2 overflow-x-auto pb-1">
+            <div className="mt-4 lg:hidden">
+              <p className="field-label" id="quick-switch-label">Quick switch</p>
+              <div role="toolbar" aria-labelledby="quick-switch-label" className="mobile-scroll-row mt-2 flex gap-2 overflow-x-auto pb-1">

Comment on lines +73 to +78
const isProbablyMobileDevice = useMemo(() => {
if (typeof window === "undefined") {
return false;
}
return /Android|iPhone|iPad|iPod|Mobile/i.test(window.navigator.userAgent);
}, []);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hydration mismatch on mobile devices.

useMemo with [] runs during SSR (where window is undefinedfalse) and again on the client. On a mobile device the client computation returns true, producing a React hydration mismatch warning. Since the value is only needed inside launchUpiApp (a click handler), you can avoid memoizing it entirely and just evaluate it at call time:

Proposed fix
-  const isProbablyMobileDevice = useMemo(() => {
-    if (typeof window === "undefined") {
-      return false;
-    }
-    return /Android|iPhone|iPad|iPod|Mobile/i.test(window.navigator.userAgent);
-  }, []);
+  const isProbablyMobileDevice = useCallback(() => {
+    if (typeof window === "undefined") {
+      return false;
+    }
+    return /Android|iPhone|iPad|iPod|Mobile/i.test(window.navigator.userAgent);
+  }, []);

Then in launchUpiApp, call isProbablyMobileDevice() instead of reading it as a value. This avoids SSR computation entirely and removes it from the hook's dependency array.

🤖 Prompt for AI Agents
In `@apps/web/src/components/DonateBookmark.tsx` around lines 73 - 78, The current
useMemo-based isProbablyMobileDevice runs during SSR (returning false) causing
hydration mismatches; replace the useMemo constant with a function named
isProbablyMobileDevice() that checks typeof window !== "undefined" and tests
navigator.userAgent at call time, and update launchUpiApp to call
isProbablyMobileDevice() inside the click handler instead of reading a memoized
value; remove the useMemo import/usage so the device check never runs during SSR
and only executes on the client when the handler is invoked.

</SignInButton>
</SignedOut>
<SignedIn>
<UserButton afterSignOutUrl="/" />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

@clerk/nextjs v6.36.10 UserButton afterSignOutUrl prop documentation

💡 Result:

In @clerk/nextjs@6.36.10, the <UserButton /> prop afterSignOutUrl?: string is documented, but it is deprecated:

  • afterSignOutUrl (deprecated): “The full URL or path to navigate to after a successful sign-out.” Clerk’s docs say to move afterSignOutUrl to <ClerkProvider /> instead. [1][2]

Related note from Clerk’s Core 2 / Next.js upgrade guide:

  • The default “after sign-out” redirect is now /, so explicitly passing afterSignOutUrl="/" is no longer necessary in Core 2. [3]

Docs pages: the <UserButton /> component reference lists afterSignOutUrl under Properties with the deprecation note. [1][2]

Sources
[1] Clerk Docs – <UserButton /> component (Next.js reference)
[2] Clerk Docs – <UserButton /> component (Next.js components page)
[3] Clerk Docs – Upgrading @clerk/nextjs to Core 2 (Next.js)


🏁 Script executed:

find . -name "package.json" -o -name "package-lock.json" -o -name "pnpm-lock.yaml" -o -name "yarn.lock" | head -5

Repository: rohan-patnaik/ZenPDF

Length of output: 116


🏁 Script executed:

grep -r "@clerk/nextjs" --include="package.json" --include="*.ts" --include="*.tsx" | head -10

Repository: rohan-patnaik/ZenPDF

Length of output: 794


🏁 Script executed:

sed -n '50,80p' apps/web/src/components/SiteHeader.tsx

Repository: rohan-patnaik/ZenPDF

Length of output: 1149


🏁 Script executed:

cat -n apps/web/src/app/providers.tsx

Repository: rohan-patnaik/ZenPDF

Length of output: 4041


Remove deprecated afterSignOutUrl prop or migrate to <ClerkProvider>.

The afterSignOutUrl prop on <UserButton> is deprecated as of @clerk/nextjs v6.36.10. Clerk recommends moving this setting to <ClerkProvider> instead. Additionally, afterSignOutUrl="/" is now redundant since the default after sign-out redirect is /. Either remove the prop entirely or configure it at the provider level in providers.tsx if a custom redirect is needed globally.

🤖 Prompt for AI Agents
In `@apps/web/src/components/SiteHeader.tsx` at line 56, The <UserButton>
component is passing the deprecated afterSignOutUrl prop; remove the prop from
<UserButton> (symbol: UserButton) since afterSignOutUrl="/" is redundant, or if
you need a global custom redirect, move the setting into the ClerkProvider
configuration in your providers.tsx (symbol: ClerkProvider / providers.tsx) by
setting the afterSignOutUrl there instead of on UserButton.

@rohan-patnaik rohan-patnaik merged commit acdb2dd into main Feb 7, 2026
6 checks passed
@rohan-patnaik rohan-patnaik deleted the codex/donation-no-premium-pool branch February 7, 2026 21:38
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.

1 participant