Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions apps/web/app/(ee)/api/network/partners/invites-usage/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { getNetworkInvitesUsage } from "@/lib/api/partners/get-network-invites-usage";
import { withWorkspace } from "@/lib/auth";
import { NextResponse } from "next/server";

// GET /api/network/partners/invites-usage - get the usage and limits for partner network invitations
export const GET = withWorkspace(
async ({ workspace }) => {
const usage = await getNetworkInvitesUsage(workspace);

return NextResponse.json({
usage,
limit: workspace.networkInvitesLimit,
remaining: Math.max(0, workspace.networkInvitesLimit - usage),
});
},
{
requiredPlan: ["enterprise"],
},
);
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function checkoutSessionCompleted(event: Stripe.Event) {
tagsLimit: plan.limits.tags!,
foldersLimit: plan.limits.folders!,
groupsLimit: plan.limits.groups!,
networkInvitesLimit: plan.limits.networkInvites!,
usersLimit: plan.limits.users!,
paymentFailedAt: null,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ export async function customerSubscriptionDeleted(event: Stripe.Event) {
tagsLimit: FREE_PLAN.limits.tags!,
foldersLimit: FREE_PLAN.limits.folders!,
groupsLimit: FREE_PLAN.limits.groups!,
networkInvitesLimit: FREE_PLAN.limits.networkInvites!,
usersLimit: FREE_PLAN.limits.users!,
paymentFailedAt: null,
foldersUsage: 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export async function updateWorkspacePlan({
tagsLimit: plan.limits.tags!,
foldersLimit: plan.limits.folders!,
groupsLimit: plan.limits.groups!,
networkInvitesLimit: plan.limits.networkInvites!,
usersLimit: plan.limits.users!,
paymentFailedAt: null,
...(shouldDeleteFolders && { foldersUsage: 0 }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,14 @@ export function NetworkEmptyState({
</div>
)}
addButton={
<Button
type="button"
text="Clear all filters"
className="h-9 rounded-lg"
onClick={onClearAllFilters}
/>
isFiltered || isStarred ? (
<Button
type="button"
text="Clear all filters"
className="h-9 rounded-lg"
onClick={onClearAllFilters}
/>
) : undefined
}
/>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import useWorkspace from "@/lib/swr/use-workspace";
import { NetworkPartnerProps } from "@/lib/types";
import { PARTNER_NETWORK_MAX_PAGE_SIZE } from "@/lib/zod/schemas/partner-network";
import { ConversionScoreIcon } from "@/ui/partners/conversion-score-icon";
import { NetworkPartnerSheet } from "@/ui/partners/network-partner-sheet";
import { ConversionScoreTooltip } from "@/ui/partners/partner-network/conversion-score-tooltip";
import { NetworkPartnerSheet } from "@/ui/partners/partner-network/network-partner-sheet";
import {
AnimatedSizeContainer,
BadgeCheck2Fill,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { PageContent } from "@/ui/layout/page-content";
import { PageWidthWrapper } from "@/ui/layout/page-width-wrapper";
import { InvitesUsage } from "@/ui/partners/partner-network/invites-usage";
import { ProgramPartnerNetworkPageClient } from "./page-client";

export default function ProgramPartnerNetwork() {
return (
<PageContent title="Partner Network">
<PageContent title="Partner Network" controls={<InvitesUsage />}>
<PageWidthWrapper className="mb-10">
<ProgramPartnerNetworkPageClient />
</PageWidthWrapper>
Expand Down
9 changes: 9 additions & 0 deletions apps/web/lib/actions/partners/invite-partner-from-network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { recordAuditLog } from "@/lib/api/audit-logs/record-audit-log";
import { createId } from "@/lib/api/create-id";
import { createAndEnrollPartner } from "@/lib/api/partners/create-and-enroll-partner";
import { getNetworkInvitesUsage } from "@/lib/api/partners/get-network-invites-usage";
import { getDefaultProgramIdOrThrow } from "@/lib/api/programs/get-default-program-id-or-throw";
import { invitePartnerFromNetworkSchema } from "@/lib/zod/schemas/partner-network";
import { sendEmail } from "@dub/email";
Expand All @@ -16,6 +17,14 @@ export const invitePartnerFromNetworkAction = authActionClient
.schema(invitePartnerFromNetworkSchema)
.action(async ({ parsedInput, ctx }) => {
const { workspace, user } = ctx;

const networkInvitesUsage = await getNetworkInvitesUsage(workspace);

if (networkInvitesUsage >= workspace.networkInvitesLimit)
throw new Error(
"You have reached your partner network invitations limit.",
);

const { partnerId, groupId } = parsedInput;

const programId = getDefaultProgramIdOrThrow(workspace);
Expand Down
21 changes: 21 additions & 0 deletions apps/web/lib/api/partners/get-network-invites-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { prisma } from "@dub/prisma";
import { getBillingStartDate } from "@dub/utils";
import { Project } from "@prisma/client";

export async function getNetworkInvitesUsage(
workspace: Pick<Project, "id" | "billingCycleStart">,
) {
const invites = await prisma.discoveredPartner.aggregate({
_count: true,
where: {
program: {
workspaceId: workspace.id,
},
invitedAt: {
gt: getBillingStartDate(workspace.billingCycleStart),
},
},
});

return invites._count;
}
34 changes: 34 additions & 0 deletions apps/web/lib/swr/use-partner-network-invites-usage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { fetcher } from "@dub/utils";
import useSWR, { SWRConfiguration } from "swr";
import useWorkspace from "./use-workspace";

export default function usePartnerNetworkInvitesUsage({
enabled,
swrOpts,
}: {
enabled?: boolean;
swrOpts?: SWRConfiguration;
} = {}) {
const { id: workspaceId } = useWorkspace();

const { data, isLoading, error } = useSWR<{
usage: number;
limit: number;
remaining: number;
}>(
workspaceId &&
enabled !== false &&
`/api/network/partners/invites-usage?workspaceId=${workspaceId}`,
fetcher,
{
keepPreviousData: true,
...swrOpts,
},
);

return {
...data,
isLoading,
error,
};
}
2 changes: 1 addition & 1 deletion apps/web/lib/zod/schemas/leads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const trackLeadRequestSchema = z.object({
.string()
.trim()
.describe(
"The unique ID of the click that the lead conversion event is attributed to. You can read this value from `dub_id` cookie. If an empty string is provided, Dub will try to find an existing customer with the provided `customerExternalId` and use the `clickId` from the customer if found.",
"The unique ID of the click that the lead conversion event is attributed to. You can read this value from `dub_id` cookie. [For deferred lead tracking]: If an empty string is provided, Dub will try to find an existing customer with the provided `customerExternalId` and use the `clickId` from the customer if found.",
),
eventName: z
.string()
Expand Down
3 changes: 3 additions & 0 deletions apps/web/lib/zod/schemas/workspaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ export const WorkspaceSchema = z
foldersUsage: z.number().describe("The folders usage of the workspace."),
foldersLimit: z.number().describe("The folders limit of the workspace."),
groupsLimit: z.number().describe("The groups limit of the workspace."),
networkInvitesLimit: z
.number()
.describe("The weekly network invites limit of the workspace."),
usersLimit: z.number().describe("The users limit of the workspace."),
aiUsage: z.number().describe("The AI usage of the workspace."),
aiLimit: z.number().describe("The AI limit of the workspace."),
Expand Down
47 changes: 47 additions & 0 deletions apps/web/ui/partners/partner-network/invites-usage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"use client";

import usePartnerNetworkInvitesUsage from "@/lib/swr/use-partner-network-invites-usage";
import { EnvelopeArrowRight, Tooltip } from "@dub/ui";
import { cn } from "@dub/utils";

export function InvitesUsage() {
const { remaining } = usePartnerNetworkInvitesUsage();

return remaining === undefined ? null : (
<Tooltip
content={
<p className="max-w-xs p-2 text-center text-xs font-medium text-neutral-600">
Invitation limits are reset at the start of your billing cycle. If you
need more invites,{" "}
<a
href="https://dub.co/contact/sales"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
contact us
</a>
.
</p>
}
align="end"
>
<div className="animate-fade-in flex cursor-default items-center gap-2">
<EnvelopeArrowRight className="text-content-default size-4 shrink-0" />
<span
className={cn(
"text-content-emphasis text-sm font-medium",
remaining === 0 && "text-content-subtle",
)}
>
<span
className={cn(remaining > 0 && remaining <= 5 && "text-violet-600")}
>
{remaining} <span className="hidden sm:inline">invites</span>
</span>{" "}
remaining
</span>
</div>
</Tooltip>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { invitePartnerFromNetworkAction } from "@/lib/actions/partners/invite-partner-from-network";
import { updateDiscoveredPartnerAction } from "@/lib/actions/partners/update-discovered-partner";
import { mutatePrefix } from "@/lib/swr/mutate";
import usePartnerNetworkInvitesUsage from "@/lib/swr/use-partner-network-invites-usage";
import useProgram from "@/lib/swr/use-program";
import useWorkspace from "@/lib/swr/use-workspace";
import { NetworkPartnerProps } from "@/lib/types";
Expand All @@ -18,10 +19,11 @@ import { timeAgo } from "@dub/utils";
import { useAction } from "next-safe-action/hooks";
import { Dispatch, SetStateAction, useState } from "react";
import { toast } from "sonner";
import { PartnerAbout } from "./partner-about";
import { PartnerComments } from "./partner-comments";
import { PartnerInfoCards } from "./partner-info-cards";
import { PartnerSheetTabs } from "./partner-sheet-tabs";
import { PartnerAbout } from "../partner-about";
import { PartnerComments } from "../partner-comments";
import { PartnerInfoCards } from "../partner-info-cards";
import { PartnerSheetTabs } from "../partner-sheet-tabs";
import { InvitesUsage } from "./invites-usage";

type NetworkPartnerSheetProps = {
partner: NetworkPartnerProps;
Expand Down Expand Up @@ -192,14 +194,28 @@ function PartnerControls({
},
});

useKeyboardShortcut("s", () => setShowConfirmModal(true), { sheet: true });

const alreadyInvited = Boolean(partner.invitedAt || partner.recruitedAt);

const { remaining } = usePartnerNetworkInvitesUsage({
enabled: !alreadyInvited,
});

const disabled = alreadyInvited || remaining === 0;

useKeyboardShortcut("s", () => setShowConfirmModal(true), {
sheet: true,
enabled: !disabled,
});

return (
<>
{confirmModal}
<div className="flex justify-end gap-2">
<div className="flex items-center justify-end gap-2">
{!alreadyInvited && (
<div className="mr-2">
<InvitesUsage />
</div>
)}
{!alreadyInvited && (
<div className="flex-shrink-0">
<PartnerIgnoreButton partner={partner} setIsOpen={setIsOpen} />
Expand All @@ -215,8 +231,8 @@ function PartnerControls({
? `Invited ${timeAgo(partner.invitedAt, { withAgo: true })}`
: "Send invite"
}
disabled={alreadyInvited}
shortcut={alreadyInvited ? undefined : "S"}
disabled={disabled}
shortcut={disabled ? undefined : "S"}
loading={isPending}
onClick={() => setShowConfirmModal(true)}
className="w-fit shrink-0"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/ui/shared/animated-empty-state.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export function AnimatedEmptyState({
style={{ "--scroll": "-50%" } as CSSProperties}
className="animate-infinite-scroll-y flex flex-col [animation-duration:10s]"
>
{[...Array(6)].map((_, idx) => (
{[...Array(cardCount * 2)].map((_, idx) => (
<Card key={idx} className={cardClassName}>
{typeof cardContent === "function"
? cardContent(idx % cardCount)
Expand Down
31 changes: 16 additions & 15 deletions packages/prisma/schema/workspace.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,22 @@ model Project {
totalLinks Int @default(0) // Total number of links in the workspace
totalClicks Int @default(0) // Total number of clicks in the workspace

usage Int @default(0)
usageLimit Int @default(1000)
linksUsage Int @default(0)
linksLimit Int @default(25)
payoutsUsage Int @default(0)
payoutsLimit Int @default(0)
payoutFee Float @default(0.05) // processing fee (in decimals) for partner payouts
domainsLimit Int @default(3)
tagsLimit Int @default(5)
foldersUsage Int @default(0)
foldersLimit Int @default(0)
groupsLimit Int @default(0)
usersLimit Int @default(1)
aiUsage Int @default(0)
aiLimit Int @default(10)
usage Int @default(0)
usageLimit Int @default(1000)
linksUsage Int @default(0)
linksLimit Int @default(25)
payoutsUsage Int @default(0)
payoutsLimit Int @default(0)
payoutFee Float @default(0.05) // processing fee (in decimals) for partner payouts
domainsLimit Int @default(3)
tagsLimit Int @default(5)
foldersUsage Int @default(0)
foldersLimit Int @default(0)
groupsLimit Int @default(0)
usersLimit Int @default(1)
aiUsage Int @default(0)
aiLimit Int @default(10)
networkInvitesLimit Int @default(0)

referralLinkId String? @unique
referredSignups Int @default(0)
Expand Down
5 changes: 5 additions & 0 deletions packages/utils/src/constants/pricing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const PLANS = [
tags: 5,
folders: 0,
groups: 0,
networkInvites: 0,
users: 1,
ai: 10,
api: 60,
Expand All @@ -82,6 +83,7 @@ export const PLANS = [
tags: 25,
folders: 3,
groups: 0,
networkInvites: 0,
users: 3,
ai: 1_000,
api: 600,
Expand Down Expand Up @@ -156,6 +158,7 @@ export const PLANS = [
tags: INFINITY_NUMBER,
folders: 20,
groups: 3,
networkInvites: 0,
users: 10,
ai: 1_000,
api: 1_200,
Expand Down Expand Up @@ -254,6 +257,7 @@ export const PLANS = [
tags: INFINITY_NUMBER,
folders: 50,
groups: 10,
networkInvites: 0,
users: 20,
ai: 1_000,
api: 3_000,
Expand Down Expand Up @@ -345,6 +349,7 @@ export const PLANS = [
tags: INFINITY_NUMBER,
folders: INFINITY_NUMBER,
groups: INFINITY_NUMBER,
networkInvites: 20,
users: 30,
ai: 1_000,
api: 3_000,
Expand Down