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
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ export const GET = withPartnerProfile(async ({ partner, params }) => {

return NextResponse.json(
PartnerProfileCustomerSchema.extend({
email: z.string(),
...(customerDataSharingEnabledAt && { name: z.string().nullish() }),
}).parse({
Comment on lines +93 to 94
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Guard against spreading null into the schema extension.

When customer data sharing isn’t enabled, customerDataSharingEnabledAt is null, so customerDataSharingEnabledAt && { … } yields null, and { ...(null) } throws TypeError: Cannot convert undefined or null to object. That would cause this endpoint to 500 for every program that hasn’t opted into sharing. Please gate the extension with a ternary (or hoist the schema selection) before parsing.

-  return NextResponse.json(
-    PartnerProfileCustomerSchema.extend({
-      ...(customerDataSharingEnabledAt && { name: z.string().nullish() }),
-    }).parse({
+  const customerSchema = customerDataSharingEnabledAt
+    ? PartnerProfileCustomerSchema.extend({
+        name: z.string().nullish(),
+      })
+    : PartnerProfileCustomerSchema;
+
+  return NextResponse.json(
+    customerSchema.parse({
📝 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.

Suggested change
...(customerDataSharingEnabledAt && { name: z.string().nullish() }),
}).parse({
const customerSchema = customerDataSharingEnabledAt
? PartnerProfileCustomerSchema.extend({
name: z.string().nullish(),
})
: PartnerProfileCustomerSchema;
return NextResponse.json(
customerSchema.parse({
🤖 Prompt for AI Agents
In
apps/web/app/(ee)/api/partner-profile/programs/[programId]/customers/[customerId]/route.ts
around lines 93-94, the object spread uses ...(customerDataSharingEnabledAt && {
name: z.string().nullish() }) which evaluates to null when
customerDataSharingEnabledAt is null and causes a TypeError; change this to a
safe gate such as using a ternary (customerDataSharingEnabledAt ? { name:
z.string().nullish() } : {}) or select/compose the schema before calling parse
so you never spread null/undefined into the schema extension.

...transformCustomer({
...customer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,18 @@ export function ProgramCustomerPageClient() {

<div className="flex flex-col gap-1">
{customer ? (
customer.email && (
<span className="text-sm font-medium text-neutral-500">
{customer.email}
</span>
)
<>
{customer["name"] && (
<h1 className="text-base font-semibold leading-tight text-neutral-900">
{customer["name"]}
</h1>
)}
{customer.email && (
<span className="text-sm font-medium text-neutral-500">
{customer.email}
</span>
)}
</>
) : (
<div className="h-5 w-24 animate-pulse rounded-md bg-neutral-200" />
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,16 @@ import useProgramEnrollment from "@/lib/swr/use-program-enrollment";
import { usePartnerLinkModal } from "@/ui/modals/partner-link-modal";
import { AnimatedEmptyState } from "@/ui/shared/animated-empty-state";
import SimpleDateRangePicker from "@/ui/shared/simple-date-range-picker";
import { Button, CardList, useKeyboardShortcut, useRouterStuff } from "@dub/ui";
import {
Button,
CardList,
ToggleGroup,
useKeyboardShortcut,
useRouterStuff,
} from "@dub/ui";
import { ChartTooltipSync } from "@dub/ui/charts";
import { CursorRays, Hyperlink } from "@dub/ui/icons";
import { createContext, useContext, useState } from "react";
import { CursorRays, GridIcon, GridLayoutRows, Hyperlink } from "@dub/ui/icons";
import { createContext, useContext, useEffect, useState } from "react";
import { PartnerLinkCard } from "./partner-link-card";

const PartnerLinksContext = createContext<{
Expand All @@ -22,6 +28,7 @@ const PartnerLinksContext = createContext<{
interval: (typeof DATE_RANGE_INTERVAL_PRESETS)[number];
openMenuLinkId: string | null;
setOpenMenuLinkId: (id: string | null) => void;
displayOption: "full" | "cards";
} | null>(null);

export function usePartnerLinksContext() {
Expand All @@ -37,10 +44,20 @@ export function usePartnerLinksContext() {
export function ProgramLinksPageClient() {
const { searchParamsObj } = useRouterStuff();
const { links, error, loading, isValidating } = usePartnerLinks();
const { programEnrollment } = useProgramEnrollment();
const { programEnrollment, showDetailedAnalytics } = useProgramEnrollment();
const { setShowPartnerLinkModal, PartnerLinkModal } = usePartnerLinkModal();
const [openMenuLinkId, setOpenMenuLinkId] = useState<string | null>(null);

const [displayOption, setDisplayOption] = useState<"full" | "cards">("full");

useEffect(() => {
if ((links && links.length > 5) || !showDetailedAnalytics) {
setDisplayOption("cards");
} else {
setDisplayOption("full");
}
}, [links, showDetailedAnalytics]);

const {
start,
end,
Expand Down Expand Up @@ -74,22 +91,51 @@ export function ProgramLinksPageClient() {
align="start"
defaultInterval={DUB_PARTNERS_ANALYTICS_INTERVAL}
/>
<Button
text="Create Link"
className="w-fit"
shortcut="C"
onClick={() => setShowPartnerLinkModal(true)}
disabled={!canCreateNewLink}
disabledTooltip={
status === "deactivated"
? "You cannot create links in this program because your partnership has been deactivated."
: hasLinksLimitReached
? `You have reached the limit of ${maxPartnerLinks} referral links.`
: !hasAdditionalLinks
? `${program?.name ?? "This"} program does not allow partners to create new links.`
: undefined
}
/>
<div className="flex items-center gap-3">
{!!showDetailedAnalytics && (
<ToggleGroup
className="rounded-lg"
options={[
{
value: "full",
label: (
<div className="p-1">
<GridIcon className="size-4" />
</div>
),
},
{
value: "cards",
label: (
<div className="p-1">
<GridLayoutRows className="size-4" />
</div>
),
},
]}
selected={displayOption}
selectAction={(option) =>
setDisplayOption(option as "full" | "cards")
}
/>
)}
<Button
text="Create Link"
className="w-fit"
shortcut="C"
onClick={() => setShowPartnerLinkModal(true)}
disabled={!canCreateNewLink}
disabledTooltip={
status === "deactivated"
? "You cannot create links in this program because your partnership has been deactivated."
: hasLinksLimitReached
? `You have reached the limit of ${maxPartnerLinks} referral links.`
: !hasAdditionalLinks
? `${program?.name ?? "This"} program does not allow partners to create new links.`
: undefined
}
/>
</div>
</div>
<PartnerLinksContext.Provider
value={{
Expand All @@ -98,6 +144,7 @@ export function ProgramLinksPageClient() {
interval,
openMenuLinkId,
setOpenMenuLinkId,
displayOption,
}}
>
<ChartTooltipSync>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ const CHARTS = [
];

export function PartnerLinkCard({ link }: { link: PartnerProfileLinkProps }) {
const { programEnrollment, showDetailedAnalytics } = useProgramEnrollment();
const { programEnrollment } = useProgramEnrollment();
const { displayOption } = usePartnerLinksContext();

const partnerLink = constructPartnerLink({
group: programEnrollment?.group,
Expand Down Expand Up @@ -203,19 +204,24 @@ export function PartnerLinkCard({ link }: { link: PartnerProfileLinkProps }) {
</div>
</Tooltip>
)}
{!showDetailedAnalytics && <StatsBadge link={link} />}
{displayOption === "cards" && <StatsBadge link={link} />}
<Controls link={link} />
</div>
</div>
</div>
{showDetailedAnalytics && <StatsCharts link={link} />}
{displayOption === "full" && <StatsCharts link={link} />}
</CardList.Card>
);
}

const StatsBadge = memo(({ link }: { link: PartnerProfileLinkProps }) => {
const { programEnrollment, showDetailedAnalytics } = useProgramEnrollment();
const As = showDetailedAnalytics ? Link : "div";
return (
<div className="flex items-center gap-0.5 rounded-md border border-neutral-200 bg-neutral-50 p-0.5 text-sm text-neutral-600">
<As
href={`/programs/${programEnrollment?.program.slug}/analytics?domain=${link.domain}&key=${link.key}`}
className="flex items-center gap-0.5 rounded-md border border-neutral-200 bg-neutral-50 p-0.5 text-sm text-neutral-600"
>
{[
{
id: "clicks",
Expand Down Expand Up @@ -258,7 +264,7 @@ const StatsBadge = memo(({ link }: { link: PartnerProfileLinkProps }) => {
</span>
</div>
))}
</div>
</As>
);
});

Expand Down