-
Notifications
You must be signed in to change notification settings - Fork 2.8k
Dub Partners popup #2522
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dub Partners popup #2522
Conversation
|
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
WalkthroughA new popup component for the Dub Partners program was introduced and integrated into the sidebar navigation. The workspace schema was updated to track the dismissal state of this popup. The sidebar navigation logic was extended to support rendering popups anchored to navigation items. Minor UI simplifications were made to the workspace dropdown placeholder. Additionally, a previously existing Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant SidebarNav
participant DubPartnersPopup
participant WorkspaceStore
User->>SidebarNav: Hover/click "Partner Program" nav item
SidebarNav->>DubPartnersPopup: Render with referenceElement
DubPartnersPopup->>WorkspaceStore: Check dismissal state & workspace data
alt Not dismissed and eligible
DubPartnersPopup-->>User: Show popup anchored to nav item
User->>DubPartnersPopup: Click dismiss
DubPartnersPopup->>WorkspaceStore: Set dismissed flag
DubPartnersPopup-->>User: Hide popup
else Dismissed or ineligible
DubPartnersPopup-->>User: No popup rendered
end
Suggested reviewers
Poem
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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
🧹 Nitpick comments (5)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (1)
34-35: Consider lazy-loading the popup component
DubPartnersPopupis only needed when the sidebar is rendered client-side and the relevant nav link is visible. Importing it eagerly makes the whole component (and its dependencies such as@floating-ui) part of the main sidebar bundle.
Wrapping the import innext/dynamic(or React .lazy) can keep the sidebar’s first-load footprint smaller.-import { DubPartnersPopup } from "./dub-partners-popup"; +const DubPartnersPopup = dynamic( + () => import("./dub-partners-popup").then((m) => m.DubPartnersPopup), + { ssr: false }, +);apps/web/ui/layout/sidebar/workspace-dropdown.tsx (1)
113-116: Minor a11y / visual nitpick on the skeleton placeholderThe placeholder div currently has
gap-x-1.5but no child elements, so the gap utility has no effect and slightly bloats the class list.
Also, consider addingrole="img" aria-label="loading"(oraria-hidden="true") to keep screen readers silent.No functional impact, just polish.
apps/web/ui/layout/sidebar/sidebar-nav.tsx (2)
45-48: Prop name collision riskExposing the prop as
popupis clear, but we now have bothNavGroupType.popupandNavGroupItem.Popupwithin the same file.
Renaming the local destructured variable (popup: Popup) to something likePopupComponentwould avoid mis-reads and keep lint rules happy.- popup: Popup, + popup: PopupComponent, ... - {Popup && element && <Popup referenceElement={element} />} + {PopupComponent && element && ( + <PopupComponent referenceElement={element} /> + )}
270-303: Element ref is never cleared on unmount
setElementstores the anchor element but we don’t reset it if the nav group unmounts.
A tiny cleanup prevents leaking stale DOM refs:-const [element, setElement] = useState<HTMLAnchorElement | null>(null); +const [element, setElement] = useState<HTMLAnchorElement | null>(null); +useEffect(() => () => setElement(null), []);apps/web/ui/layout/sidebar/dub-partners-popup.tsx (1)
74-82: TypeScriptwhileElementsMountedsignatureThe inline method form is valid, but the parameter names (
referenceEl,floatingEl) shadow nothing and differ from the surrounding context (referenceElement). Rename for clarity to avoid confusion:-whileElementsMounted(referenceEl, floatingEl, update) { - return autoUpdate(referenceEl, floatingEl, update, { animationFrame: true }); +whileElementsMounted(ref, floating, update) { + return autoUpdate(ref, floating, update, { animationFrame: true }); }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
apps/web/lib/zod/schemas/workspaces.ts(1 hunks)apps/web/ui/layout/sidebar/app-sidebar-nav.tsx(2 hunks)apps/web/ui/layout/sidebar/dub-partners-popup.tsx(1 hunks)apps/web/ui/layout/sidebar/sidebar-nav.tsx(3 hunks)apps/web/ui/layout/sidebar/workspace-dropdown.tsx(1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (1)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (1)
apps/web/ui/layout/sidebar/dub-partners-popup.tsx (1)
DubPartnersPopup(21-63)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
🔇 Additional comments (3)
apps/web/ui/layout/sidebar/app-sidebar-nav.tsx (1)
85-86: Popup integration looks correctThe new
popupprop is wired up properly and matches the extendedNavGroupTypeinterface. Nice.apps/web/lib/zod/schemas/workspaces.ts (1)
169-171: Verify downstream handling of the new store keyAdding
"dubPartnersPopupDismissed"to theworkspaceStoreKeysenum is fine, but remember to:
- Run any required DB/data-migration so that existing records don’t violate enum constraints.
- Ensure API layers that validate this enum have been regenerated or recompiled.
Confirming these avoids 422/400 responses in production.
apps/web/ui/layout/sidebar/dub-partners-popup.tsx (1)
148-151: Potential undefined slug in “Create program” linkIf
slugis unexpectedlyundefined, the CTA becomes/undefined/program/new, leading to a 404.
Either guard the render or fallback towindow.location.pathname.split("/")[1].href={slug ? `/${slug}/program/new` : "/program/new"}
| if ( | ||
| // Loading | ||
| loadingWorkspace || | ||
| loadingDismissed || | ||
| // Program/partners status/capabilities | ||
| // defaultProgramId || | ||
| !partnersEnabled || | ||
| !canManageProgram || | ||
| // Device | ||
| isMobile || | ||
| // Dismissed | ||
| dismissed | ||
| ) | ||
| return null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Popup shows even when a program already exists
The guard for defaultProgramId is commented out.
If a workspace already has a Partner Program, the popup will still appear, which feels spammy.
- // defaultProgramId ||
+ defaultProgramId ||Uncommenting (or explicitly checking !defaultProgramId) will suppress the popup for workspaces that have already onboarded.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if ( | |
| // Loading | |
| loadingWorkspace || | |
| loadingDismissed || | |
| // Program/partners status/capabilities | |
| // defaultProgramId || | |
| !partnersEnabled || | |
| !canManageProgram || | |
| // Device | |
| isMobile || | |
| // Dismissed | |
| dismissed | |
| ) | |
| return null; | |
| if ( | |
| // Loading | |
| loadingWorkspace || | |
| loadingDismissed || | |
| // Program/partners status/capabilities | |
| defaultProgramId || | |
| !partnersEnabled || | |
| !canManageProgram || | |
| // Device | |
| isMobile || | |
| // Dismissed | |
| dismissed | |
| ) | |
| return null; |
🤖 Prompt for AI Agents
In apps/web/ui/layout/sidebar/dub-partners-popup.tsx between lines 42 and 55,
the condition checking for defaultProgramId is commented out, causing the popup
to show even when a program already exists. Uncomment the check for
!defaultProgramId in the if statement to prevent the popup from appearing for
workspaces that have already onboarded a Partner Program.
| const { refs, floatingStyles, context } = useFloating({ | ||
| placement: "right", | ||
| strategy: "fixed", | ||
| whileElementsMounted(referenceEl, floatingEl, update) { | ||
| return autoUpdate(referenceEl, floatingEl, update, { | ||
| // Not good for performance but keeps the arrow and floating element in the right spot | ||
| animationFrame: true, | ||
| }); | ||
| }, | ||
| elements: { | ||
| reference: referenceElement, | ||
| }, | ||
| middleware: [ | ||
| offset({ | ||
| mainAxis: 12, | ||
| }), | ||
| shift({ | ||
| padding: 32, | ||
| }), | ||
| arrow({ | ||
| element: arrowRef, | ||
| }), | ||
| ], | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Duplicate SWR call & unnecessary re-rendering cost
DubPartnersPopup fetches workspace data once, yet DubPartnersPopupInner invokes useWorkspace() again just to obtain slug.
That triggers an extra SWR request and an additional re-render.
Pass slug (or the whole workspace object) down as a prop instead:
- return (
- <DubPartnersPopupInner
- referenceElement={referenceElement}
- onDismiss={() => setDismissed(true)}
- />
- );
+ return (
+ <DubPartnersPopupInner
+ referenceElement={referenceElement}
+ slug={slug}
+ onDismiss={() => setDismissed(true)}
+ />
+ );…and remove the second useWorkspace() call.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web/ui/layout/sidebar/dub-partners-popup.tsx around lines 74 to 97, the
DubPartnersPopupInner component calls useWorkspace() again to get the slug,
causing a duplicate SWR fetch and extra re-render. To fix this, retrieve the
slug once in DubPartnersPopup and pass it down as a prop to
DubPartnersPopupInner, then remove the second useWorkspace() call inside
DubPartnersPopupInner to avoid redundant data fetching and rendering.
|
🤖 Bug0 QA Agent Here are the results of the automated tests for PR #2522:
To re-run the tests, please comment |
|
🤖 Bug0 QA Agent Here are the results of the automated tests for PR #2522:
To re-run the tests, please comment |
There was a problem hiding this 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
🧹 Nitpick comments (3)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/auth.tsx (1)
33-37: Consider extracting the composite check into a named constant for clarity & testabilityThe three-part condition (
!partnersEnabled || !getPlanCapabilities(plan).canManageProgram || !defaultProgramId) is correct but fairly dense. Moving this into a well-named boolean (e.g.needsPartnersUpgradeCTA) improves readability, avoids accidental precedence mistakes when the condition evolves, and makes it trivial to unit-test the auth logic in isolation.- if ( - !partnersEnabled || - !getPlanCapabilities(plan).canManageProgram || - !defaultProgramId - ) { + const needsPartnersUpgradeCTA = + !partnersEnabled || + !getPlanCapabilities(plan).canManageProgram || + !defaultProgramId; + + if (needsPartnersUpgradeCTA) {apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners-ugrade-cta.tsx (2)
8-8:useMemoimport becomes redundant if you inline the CTA computationShould you decide to remove
useMemoper the previous comment, remember to delete theuseMemoimport to avoid an unused-import linter error.-import { useMemo } from "react"; +// import { useMemo } from "react"; // remove if `useMemo` is dropped
72-79: External wait-list URL opens in the same tabWhen
partnersEnabled === false,hrefresolves tohttps://dub.co/partners, but theLinkcomponent omitstarget="_blank" rel="noopener"so the dashboard is replaced by the marketing page.
Consider conditionally adding these attributes for external destinations to preserve user context.- <Link - href={href} + <Link + href={href} + {...(href.startsWith("http") && { + target: "_blank", + rel: "noopener noreferrer", + })}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (6)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/auth.tsx(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners-ugrade-cta.tsx(3 hunks)apps/web/ui/layout/sidebar/create-program-card.tsx(0 hunks)apps/web/ui/layout/sidebar/dub-partners-popup.tsx(1 hunks)apps/web/ui/layout/sidebar/sidebar-nav-old.tsx(0 hunks)apps/web/ui/layout/sidebar/sidebar-nav.tsx(3 hunks)
💤 Files with no reviewable changes (2)
- apps/web/ui/layout/sidebar/sidebar-nav-old.tsx
- apps/web/ui/layout/sidebar/create-program-card.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
- apps/web/ui/layout/sidebar/sidebar-nav.tsx
- apps/web/ui/layout/sidebar/dub-partners-popup.tsx
🧰 Additional context used
🧬 Code Graph Analysis (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners-ugrade-cta.tsx (1)
apps/web/lib/plan-capabilities.ts (1)
getPlanCapabilities(4-14)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/auth.tsx (1)
apps/web/lib/plan-capabilities.ts (1)
getPlanCapabilities(4-14)
⏰ Context from checks skipped due to timeout of 90000ms (1)
- GitHub Check: build
| const { cta, href } = useMemo(() => { | ||
| if (partnersEnabled) { | ||
| if (!canManageProgram) { | ||
| return { | ||
| cta: "Upgrade plan", | ||
| href: `/${slug}/upgrade`, | ||
| }; | ||
| } else { | ||
| return { | ||
| cta: store?.programOnboarding ? "Finish creating" : "Create program", | ||
| href: `/${slug}/program/new`, | ||
| }; | ||
| } | ||
| } else { | ||
| return { | ||
| cta: "Join the waitlist", | ||
| href: "https://dub.co/partners", | ||
| }; | ||
| } | ||
| }, [canManageProgram, partnersEnabled, slug]); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
useMemo is missing store?.programOnboarding in its dependency list
store?.programOnboarding directly affects the returned { cta, href }, yet it is omitted from the dependency array.
If onboarding status changes after the first render, the CTA label will stay stale (“Create program” vs “Finish creating”).
Either add the missing dependency or drop the useMemo (the computation is cheap).
- }, [canManageProgram, partnersEnabled, slug]);
+ }, [canManageProgram, partnersEnabled, slug, store?.programOnboarding]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const { cta, href } = useMemo(() => { | |
| if (partnersEnabled) { | |
| if (!canManageProgram) { | |
| return { | |
| cta: "Upgrade plan", | |
| href: `/${slug}/upgrade`, | |
| }; | |
| } else { | |
| return { | |
| cta: store?.programOnboarding ? "Finish creating" : "Create program", | |
| href: `/${slug}/program/new`, | |
| }; | |
| } | |
| } else { | |
| return { | |
| cta: "Join the waitlist", | |
| href: "https://dub.co/partners", | |
| }; | |
| } | |
| }, [canManageProgram, partnersEnabled, slug]); | |
| const { cta, href } = useMemo(() => { | |
| if (partnersEnabled) { | |
| if (!canManageProgram) { | |
| return { | |
| cta: "Upgrade plan", | |
| href: `/${slug}/upgrade`, | |
| }; | |
| } else { | |
| return { | |
| cta: store?.programOnboarding ? "Finish creating" : "Create program", | |
| href: `/${slug}/program/new`, | |
| }; | |
| } | |
| } else { | |
| return { | |
| cta: "Join the waitlist", | |
| href: "https://dub.co/partners", | |
| }; | |
| } | |
| }, [canManageProgram, partnersEnabled, slug, store?.programOnboarding]); |
🤖 Prompt for AI Agents
In
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/partners-ugrade-cta.tsx
around lines 21 to 41, the useMemo hook's dependency array is missing
store?.programOnboarding, which affects the returned cta and href values. To fix
this, add store?.programOnboarding to the dependency array to ensure the
memoized values update correctly when onboarding status changes, or
alternatively remove useMemo entirely since the computation is inexpensive.
|
@CodeRabbit final review |
|
Tip For best results, initiate chat on the files or code changes. @steven-tey below is the holistic “final review” for PR #2522. 🌟 What looks great
|
| Area | Concern | Impact | Suggested action |
|---|---|---|---|
| State key propagation | You added the new key dubPartnersPopupDismissed to the Zod workspace schema, but server-side validation/migrations aren’t visible in this PR. |
Workspace API calls that include this key could be rejected if the backend schema isn’t updated simultaneously. | Confirm the API/schema change is already deployed, or coordinate a backend PR. |
| Persisted dismissal | The dismissal flag is written via useWorkspaceStore.setValue, which only saves to localStorage (per the existing store pattern). That means the popup re-appears on another browser / after clearing storage. |
Could annoy users who use multiple devices. | If product requirements expect cross-device persistence, also POST the dismissal to the workspace backend. Otherwise, leave as-is. |
| Accessibility | The popup contains interactive elements but is rendered in a FloatingPortal without a focus trap. Screen-reader / keyboard users may tab behind the overlay. |
WCAG non-compliance risk. | Wrap content in a focus-scope (e.g., @radix-ui/react-focus-scope) or at least set aria-modal="true" and move focus on open. |
| Mobile check | const isMobile = useMediaQuery("(max-width: 768px)") is evaluated client-side only. During SSR the first paint may briefly show the popup on mobile, then hide. |
Layout jank on mobile devices. | Default isMobile to true on first render when typeof window === "undefined". |
| CSS arrow | The popup arrow relies on [data-side="right"] & selector inside <Arrow /> but @floating-ui sets data-placement. Works as shipped, yet any placement change needs matching styles. |
Minor – future-proofing. | Consider using [data-placement^="right"] to cover right-start/end. |
| Test coverage | No new unit/e2e tests were added for the popup visibility logic. | Regression risk if gating logic changes. | Add a Jest test for DubPartnersPopup render conditions or a Playwright e2e that asserts dismissal persistence. |
✨ Finishing touches
- Several new/modified components (
dub-partners-popup.tsx,sidebar-nav.tsx,partners-ugrade-cta.tsx) lack JSDoc/TS docstrings. Adding a brief summary + param/return annotations will satisfy internal lint rules and help future maintainers.
✅ Merge checklist
- Backend accepts
dubPartnersPopupDismissed. - Decide if dismissal should sync across devices.
- (Optional) Add focus-management for accessibility.
- (Optional) Add tests.
- Add docstrings (see “Finishing Touches” task tick-box in PR).
Let me know if you’d like code snippets or follow-up issues opened for any of the above. Great work!
|
🤖 Bug0 QA Agent Here are the results of the automated tests for PR #2522:
To re-run the tests, please comment |
Summary by CodeRabbit
New Features
Improvements
Bug Fixes
Style
Removals