-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Partner network invite limits #2935
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
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds network-invite limits and weekly usage tracking: plan limit fields, DB/schema field and persistence, an enterprise-only GET API and server util, SWR hook and UI components, enforcement in the invite action, plus minor UI and schema tweaks. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor U as User
participant P as Network Page
participant H as usePartnerNetworkInvitesUsage
participant API as GET /api/network/partners/invites-usage
participant S as getNetworkInvitesUsage
participant DB as Database
U->>P: Open partner network page
P->>H: Initialize (workspaceId)
H->>API: GET usage
API->>S: compute usage(workspaceId)
S->>DB: count invites since billing start
DB-->>S: usage count
S-->>API: usage
API-->>H: {usage, limit, remaining}
H-->>P: render InvitesUsage with remaining
sequenceDiagram
autonumber
actor U as User
participant Sheet as Partner Sheet
participant H as usePartnerNetworkInvitesUsage
participant Action as invitePartnerFromNetworkAction
participant S as getNetworkInvitesUsage
participant DB as Database
U->>Sheet: Open sheet
Sheet->>H: fetch remaining
H->>API: GET /invites-usage
API->>S: compute usage
S->>DB: count invites
DB-->>S: usage
S-->>Sheet: remaining
alt remaining == 0
Sheet->>U: disable invite UI/shortcut
else
U->>Sheet: Click Invite
Sheet->>Action: invitePartnerFromNetworkAction
Action->>S: getNetworkInvitesUsage
S->>DB: count invites
DB-->>S: usage
S-->>Action: usage
alt usage >= limit
Action-->>Sheet: Error "limit reached"
else
Action-->>Sheet: proceed with invite
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
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 |
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: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
apps/web/ui/partners/partner-network/network-partner-sheet.tsx (1)
197-197
: Disable keyboard shortcut when button is disabled.The global keyboard shortcut "s" (line 197) remains active even when the invite button is disabled (
disabled = true
at line 205). This creates inconsistent UX where pressing "s" opens the confirmation modal even when the button shows no shortcut indicator and is visually disabled.Apply this diff to conditionally register the shortcut:
- useKeyboardShortcut("s", () => setShowConfirmModal(true), { sheet: true }); + useKeyboardShortcut("s", () => setShowConfirmModal(true), { + sheet: true, + enabled: !disabled + });Alternatively, if the
useKeyboardShortcut
hook doesn't support anenabled
option, conditionally call it:- useKeyboardShortcut("s", () => setShowConfirmModal(true), { sheet: true }); + if (!disabled) { + useKeyboardShortcut("s", () => setShowConfirmModal(true), { sheet: true }); + }Note: The second approach may cause issues with React hooks rules. Verify the
useKeyboardShortcut
API to determine the best solution.
♻️ Duplicate comments (2)
apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts (1)
72-72
: Verify thatnetworkInvites
is defined in all plan limits.The non-null assertion on
plan.limits.networkInvites!
will cause a runtime error if any plan doesn't define this limit. See similar comment onupdate-workspace-plan.ts
.apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts (1)
121-121
: Verify thatnetworkInvites
is defined in FREE_PLAN limits.The non-null assertion on
FREE_PLAN.limits.networkInvites!
will cause a runtime error if the free plan doesn't define this limit. See similar comments on other webhook handlers.
🧹 Nitpick comments (2)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/network-empty-state.tsx (1)
48-57
: LGTM! Sensible UI improvement.The conditional rendering of the "Clear all filters" button aligns perfectly with the filter state and the description text above (lines 19-30). When no filters are active, hiding the button improves the UX by avoiding a misleading or non-functional action.
Optional refinement:
The condition
isFiltered || isStarred
is repeated at lines 19 and 49. Consider extracting it to a constant for better maintainability:+const hasActiveFilters = isFiltered || isStarred; + return ( <AnimatedEmptyState title="No partners found" description={ - isFiltered || isStarred ? ( + hasActiveFilters ? ( <> Press{" "} <span className="text-content-default bg-bg-emphasis rounded-md px-1 py-0.5 text-xs font-semibold"> Esc </span>{" "} to clear all filters. </> ) : ( "There are no partners for you to discover yet." ) } className="border-none md:min-h-[400px]" cardClassName="py-3" cardCount={2} cardContent={(idx) => ( <div className="flex grow items-center gap-4"> {idx % 2 === 0 || isStarred ? ( <StarFill className="size-3 shrink-0 text-amber-500" /> ) : ( <Star className="text-content-muted size-3 shrink-0" /> )} <DemoAvatar className="text-content-default size-9 shrink-0" /> <div className="flex grow flex-col gap-2"> <div className="h-2.5 w-full min-w-0 rounded bg-neutral-200" /> <div className="h-2.5 w-12 min-w-0 rounded bg-neutral-200" /> </div> </div> )} addButton={ - isFiltered || isStarred ? ( + hasActiveFilters ? ( <Button type="button" text="Clear all filters" className="h-9 rounded-lg" onClick={onClearAllFilters} /> ) : undefined } /> );apps/web/lib/actions/partners/invite-partner-from-network.ts (1)
25-28
: Consider enhancing the error message with context.While the current error message is clear, providing additional context could improve the user experience:
- if (networkInvitesUsage >= workspace.networkInvitesLimit) - throw new Error( - "You have reached your partner network invitations limit.", - ); + if (networkInvitesUsage >= workspace.networkInvitesLimit) { + throw new Error( + `You have reached your partner network invitations limit (${networkInvitesUsage}/${workspace.networkInvitesLimit} used). This limit resets on a rolling 7-day basis.`, + ); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (17)
apps/web/app/(ee)/api/network/partners/invites-usage/route.ts
(1 hunks)apps/web/app/(ee)/api/stripe/webhook/checkout-session-completed.ts
(1 hunks)apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts
(1 hunks)apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/network-empty-state.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx
(1 hunks)apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page.tsx
(1 hunks)apps/web/lib/actions/partners/invite-partner-from-network.ts
(2 hunks)apps/web/lib/api/partners/get-network-invites-usage.ts
(1 hunks)apps/web/lib/swr/use-partner-network-invites-usage.ts
(1 hunks)apps/web/lib/zod/schemas/workspaces.ts
(1 hunks)apps/web/ui/partners/partner-network/invites-usage.tsx
(1 hunks)apps/web/ui/partners/partner-network/network-partner-sheet.tsx
(4 hunks)apps/web/ui/shared/animated-empty-state.tsx
(1 hunks)packages/prisma/schema/network.prisma
(1 hunks)packages/prisma/schema/workspace.prisma
(1 hunks)packages/utils/src/constants/pricing.tsx
(5 hunks)
🧰 Additional context used
🧬 Code graph analysis (6)
apps/web/app/(ee)/api/network/partners/invites-usage/route.ts (2)
apps/web/lib/auth/workspace.ts (1)
withWorkspace
(42-436)apps/web/lib/api/partners/get-network-invites-usage.ts (1)
getNetworkInvitesUsage
(3-21)
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page.tsx (2)
apps/web/ui/layout/page-content/index.tsx (1)
PageContent
(11-100)apps/web/ui/partners/partner-network/invites-usage.tsx (1)
InvitesUsage
(7-47)
apps/web/ui/partners/partner-network/invites-usage.tsx (1)
apps/web/lib/swr/use-partner-network-invites-usage.ts (1)
usePartnerNetworkInvitesUsage
(5-34)
apps/web/ui/partners/partner-network/network-partner-sheet.tsx (2)
apps/web/lib/swr/use-partner-network-invites-usage.ts (1)
usePartnerNetworkInvitesUsage
(5-34)apps/web/ui/partners/partner-network/invites-usage.tsx (1)
InvitesUsage
(7-47)
apps/web/lib/actions/partners/invite-partner-from-network.ts (1)
apps/web/lib/api/partners/get-network-invites-usage.ts (1)
getNetworkInvitesUsage
(3-21)
apps/web/app/(ee)/api/stripe/webhook/customer-subscription-deleted.ts (1)
packages/utils/src/constants/pricing.tsx (1)
FREE_PLAN
(361-361)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (13)
apps/web/ui/shared/animated-empty-state.tsx (1)
47-47
: LGTM! Dynamic card count enhances flexibility.The change from a hardcoded value to
cardCount * 2
makes the component more configurable while maintaining backward compatibility (default 3 × 2 = 6 cards). The multiplication by 2 correctly supports the infinite scroll animation by rendering duplicate sets, and theidx % cardCount
on line 50 properly cycles through content indices.Edge cases (zero or negative
cardCount
) are handled gracefully by JavaScript's array constructor behavior, which creates an empty array in such cases.packages/prisma/schema/network.prisma (1)
18-18
: LGTM!The composite index on
[programId, invitedAt]
appropriately supports queries for counting network invites within time ranges per program, which aligns with the 7-day rolling limit feature introduced in this PR.apps/web/lib/zod/schemas/workspaces.ts (1)
71-73
: LGTM!The
networkInvitesLimit
field is properly defined and follows the same pattern as other workspace limit fields.apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page-client.tsx (1)
15-15
: LGTM!Import path update reflects the reorganization of partner-related UI components.
apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/program/network/page.tsx (1)
3-3
: LGTM!Clean integration of the
InvitesUsage
component to display remaining network invites in the page header.apps/web/app/(ee)/api/stripe/webhook/utils/update-workspace-plan.ts (1)
63-63
: No action needed fornetworkInvites
non-null assertion
All plans inpackages/utils/src/constants/pricing.tsx
include anetworkInvites
limit, so the non-null assertion is safe.apps/web/ui/partners/partner-network/network-partner-sheet.tsx (2)
201-205
: LGTM! Clean integration of invites usage.The hook integration correctly computes the disabled state and only fetches usage data when needed (
enabled: !alreadyInvited
).
210-215
: LGTM! Proper conditional rendering.The
InvitesUsage
component is correctly rendered only when the partner hasn't been invited yet, which aligns with the UX requirements.packages/utils/src/constants/pricing.tsx (1)
63-63
: LGTM! Consistent plan limit definitions.The
networkInvites
limits are correctly set across all plans, with only Enterprise receiving the 20 invites allowance while all other plans default to 0. This aligns with the enterprise-only API endpoint restriction.Also applies to: 86-86, 161-161, 260-260, 352-352
apps/web/app/(ee)/api/network/partners/invites-usage/route.ts (1)
6-21
: LGTM! Proper plan enforcement and safe calculation.The route correctly:
- Restricts access to enterprise plan workspaces only
- Uses
Math.max(0, ...)
to ensure remaining never goes negative- Returns structured data that the client hook expects
The 403 response for non-enterprise workspaces is handled gracefully by the client (the UI component returns
null
whenremaining === undefined
).apps/web/ui/partners/partner-network/invites-usage.tsx (2)
13-25
: Update tooltip text to match backend implementation.The tooltip claims "Invite limits are based on a 7-day rolling period," but the backend implementation in
apps/web/lib/api/partners/get-network-invites-usage.ts
counts ALL invited partners without any date filtering. This creates a discrepancy between the UI messaging and actual behavior.Either:
- Update the backend to implement the 7-day rolling period (see comment in
get-network-invites-usage.ts
), or- Update this tooltip text to accurately describe the actual limit behavior
If choosing option 2, consider this revised text:
- Invite limits are based on a 7-day rolling period. If you need more - weekly invites,{" "} + Invite limits are refreshed based on your plan. If you need a higher + limit,{" "}
29-44
: LGTM! Excellent UX with progressive visual indicators.The component provides clear visual feedback:
- Shows remaining count prominently
- Applies violet emphasis when count is low (1-5)
- Dims text when depleted (0)
- Responsive text with mobile-friendly abbreviation
apps/web/lib/swr/use-partner-network-invites-usage.ts (1)
5-34
: LGTM! Well-structured SWR hook with proper caching.The hook correctly:
- Conditionally fetches based on
workspaceId
andenabled
flag- Uses
keepPreviousData
to prevent UI flicker during revalidation- Accepts custom SWR configuration via
swrOpts
- Returns a clean API with destructured data plus loading/error states
Note: The
workspaceId
query parameter in the URL (line 21) serves as an SWR cache key rather than being consumed by the API endpoint, which retrieves the workspace from the authentication context. This is an intentional and common pattern.
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: 0
🧹 Nitpick comments (1)
apps/web/lib/api/partners/get-network-invites-usage.ts (1)
8-18
: 7-day rolling period now correctly implemented.The critical issue from the previous review has been resolved—this implementation now properly filters
invitedAt
to within the last 7 days, matching the UI's advertised "7-day rolling period."Optional: Extract date calculation for improved readability.
Consider extracting the date calculation to a named constant for clarity:
export async function getNetworkInvitesUsage({ workspaceId, }: { workspaceId: string; }) { + const sevenDaysAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000); + const invites = await prisma.discoveredPartner.aggregate({ _count: true, where: { program: { workspaceId, }, invitedAt: { - gt: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), + gt: sevenDaysAgo, }, }, }); return invites._count; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
apps/web/lib/api/partners/get-network-invites-usage.ts
(1 hunks)apps/web/lib/zod/schemas/leads.ts
(1 hunks)apps/web/ui/partners/partner-network/invites-usage.tsx
(1 hunks)apps/web/ui/partners/partner-network/network-partner-sheet.tsx
(4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/ui/partners/partner-network/network-partner-sheet.tsx
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-06T15:48:14.178Z
Learnt from: TWilson023
PR: dubinc/dub#2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.178Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
apps/web/lib/api/partners/get-network-invites-usage.ts
🧬 Code graph analysis (1)
apps/web/ui/partners/partner-network/invites-usage.tsx (1)
apps/web/lib/swr/use-partner-network-invites-usage.ts (1)
usePartnerNetworkInvitesUsage
(5-34)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
apps/web/lib/zod/schemas/leads.ts (1)
12-12
: Documentation improvement enhances API clarity.The additional bracketed note clearly explains the deferred lead tracking behavior when an empty
clickId
is provided, improving developer experience.apps/web/ui/partners/partner-network/invites-usage.tsx (1)
7-46
: LGTM! Well-designed UI component with good UX.The component properly:
- Reflects the backend's 7-day rolling period (line 14 matches the implementation in
get-network-invites-usage.ts
)- Provides visual feedback with color coding (violet for 1-5 remaining, subtle for 0)
- Handles loading states gracefully by returning
null
whenremaining
isundefined
- Offers a clear upgrade path via the sales contact link
- Maintains clarity on mobile with responsive text ("5 remaining" vs "5 invites remaining")
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
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
apps/web/app/(ee)/api/network/partners/invites-usage/route.ts
(1 hunks)apps/web/lib/actions/partners/invite-partner-from-network.ts
(2 hunks)apps/web/lib/api/partners/get-network-invites-usage.ts
(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/web/app/(ee)/api/network/partners/invites-usage/route.ts
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: TWilson023
PR: dubinc/dub#2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.178Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
📚 Learning: 2025-10-06T15:48:14.178Z
Learnt from: TWilson023
PR: dubinc/dub#2935
File: apps/web/lib/actions/partners/invite-partner-from-network.ts:21-28
Timestamp: 2025-10-06T15:48:14.178Z
Learning: For the network invites limit check in apps/web/lib/actions/partners/invite-partner-from-network.ts, the team accepts that concurrent invites may bypass the limit due to race conditions. Perfect atomicity is not required for this feature.
Applied to files:
apps/web/lib/actions/partners/invite-partner-from-network.ts
🧬 Code graph analysis (1)
apps/web/lib/actions/partners/invite-partner-from-network.ts (1)
apps/web/lib/api/partners/get-network-invites-usage.ts (1)
getNetworkInvitesUsage
(5-21)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (3)
apps/web/lib/actions/partners/invite-partner-from-network.ts (2)
6-6
: LGTM!The import of
getNetworkInvitesUsage
is correctly placed and will be used to enforce invite limits.
21-26
: LGTM!The usage check correctly enforces the network invites limit before creating an invite. The error message is clear and user-friendly.
Note: As per learnings, the team accepts that concurrent invites may bypass the limit due to race conditions between the check and invite creation.
apps/web/lib/api/partners/get-network-invites-usage.ts (1)
14-16
: Consider usinggte
to include invites at the exact billing cycle start
getBillingStartDate
returns a Date at 00:00:00 local time—usinggt
will exclude invites timestamped exactly at the start of the billing day. If you intend to count all invites from the cycle start onward, switchgt
togte
in theinvitedAt
filter.
Summary by CodeRabbit
New Features
Improvements
Subscription & Billing
Documentation