diff --git a/apps/web/app/(ee)/api/partner-profile/programs/[programId]/groups/[groupIdOrSlug]/route.ts b/apps/web/app/(ee)/api/partner-profile/programs/[programId]/groups/[groupIdOrSlug]/route.ts
index 941b4c130eb..49f516ffc4a 100644
--- a/apps/web/app/(ee)/api/partner-profile/programs/[programId]/groups/[groupIdOrSlug]/route.ts
+++ b/apps/web/app/(ee)/api/partner-profile/programs/[programId]/groups/[groupIdOrSlug]/route.ts
@@ -1,16 +1,40 @@
-import { getGroupOrThrow } from "@/lib/api/groups/get-group-or-throw";
+import { DubApiError } from "@/lib/api/errors";
import { withPartnerProfile } from "@/lib/auth/partner";
+import { prisma } from "@/lib/prisma";
import { PartnerProgramGroupSchema } from "@/lib/zod/schemas/groups";
import { NextResponse } from "next/server";
// GET /api/partner-profile/programs/[programId]/groups/[groupIdOrSlug] - get information about a program's group
-export const GET = withPartnerProfile(async ({ params }) => {
+export const GET = withPartnerProfile(async ({ params, partner }) => {
const { programId, groupIdOrSlug } = params;
- const group = await getGroupOrThrow({
- programId,
- groupId: groupIdOrSlug,
+ const group = await prisma.partnerGroup.findUnique({
+ where: {
+ ...(groupIdOrSlug.startsWith("grp_")
+ ? {
+ id: groupIdOrSlug,
+ }
+ : {
+ programId_slug: {
+ programId,
+ slug: groupIdOrSlug,
+ },
+ }),
+ programId,
+ partners: {
+ some: {
+ partnerId: partner.id,
+ },
+ },
+ },
});
+ if (!group) {
+ throw new DubApiError({
+ code: "not_found",
+ message: `Group "${groupIdOrSlug}" not found.`,
+ });
+ }
+
return NextResponse.json(PartnerProgramGroupSchema.parse(group));
});
diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/[id]/page.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/[id]/page.tsx
deleted file mode 100644
index 1058c9b72b1..00000000000
--- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/[id]/page.tsx
+++ /dev/null
@@ -1,9 +0,0 @@
-import { PostbackDetailPageClient } from "./page-client";
-
-export default async function PostbackDetailPage(props: {
- params: Promise<{ id: string }>;
-}) {
- const { id } = await props.params;
-
- return ;
-}
diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/[id]/page-client.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/[postbackId]/page.tsx
similarity index 96%
rename from apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/[id]/page-client.tsx
rename to apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/[postbackId]/page.tsx
index e6f00ec22ff..7f74640d397 100644
--- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/[id]/page-client.tsx
+++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/[postbackId]/page.tsx
@@ -15,18 +15,13 @@ import { BackLink } from "@/ui/shared/back-link";
import { TokenAvatar } from "@/ui/token-avatar";
import { EmptyState, Webhook } from "@dub/ui";
import { fetcher } from "@dub/utils";
-import { redirect, useRouter } from "next/navigation";
+import { redirect, useParams, useRouter } from "next/navigation";
import { useState } from "react";
import useSWR from "swr";
-interface PostbackDetailPageClientProps {
- postbackId: string;
-}
-
-export function PostbackDetailPageClient({
- postbackId,
-}: PostbackDetailPageClientProps) {
+export default function PostbackDetailPage() {
const router = useRouter();
+ const { postbackId } = useParams();
const [openPopover, setOpenPopover] = useState(false);
const {
diff --git a/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/add-postback-button.tsx b/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/add-postback-button.tsx
index 8e3f5962b41..86ff7b148dc 100644
--- a/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/add-postback-button.tsx
+++ b/apps/web/app/(ee)/partners.dub.co/(dashboard)/profile/postbacks/add-postback-button.tsx
@@ -5,10 +5,10 @@ import { Button } from "@dub/ui";
export function AddPostbackButton({ onClick }: { onClick: () => void }) {
return (
);
}
diff --git a/apps/web/app/api/analytics/dashboard/route.ts b/apps/web/app/api/analytics/dashboard/route.ts
index 5205fca9686..760dbfb8dbd 100644
--- a/apps/web/app/api/analytics/dashboard/route.ts
+++ b/apps/web/app/api/analytics/dashboard/route.ts
@@ -8,6 +8,7 @@ import { redis } from "@/lib/upstash";
import { parseAnalyticsQuery } from "@/lib/zod/schemas/analytics";
import { DUB_DEMO_LINKS, DUB_WORKSPACE_ID, getSearchParams } from "@dub/utils";
import { waitUntil } from "@vercel/functions";
+import { cookies } from "next/headers";
import { NextResponse } from "next/server";
export const dynamic = "force-dynamic";
@@ -48,7 +49,12 @@ export const GET = async (req: Request) => {
},
select: {
id: true,
- dashboard: true,
+ dashboard: {
+ select: {
+ id: true,
+ password: true,
+ },
+ },
projectId: true,
project: {
select: {
@@ -80,6 +86,8 @@ export const GET = async (req: Request) => {
});
}
+ await assertDashboardPassword(folder.dashboard);
+
workspace = folder.project;
if ("links" in folder && folder.links?.length) link = folder.links[0];
@@ -102,7 +110,12 @@ export const GET = async (req: Request) => {
},
select: {
id: true,
- dashboard: true,
+ dashboard: {
+ select: {
+ id: true,
+ password: true,
+ },
+ },
projectId: true,
project: {
select: {
@@ -123,6 +136,8 @@ export const GET = async (req: Request) => {
});
}
+ await assertDashboardPassword(link.dashboard);
+
workspace = link.project;
}
}
@@ -185,3 +200,23 @@ export const GET = async (req: Request) => {
return handleAndReturnErrorResponse(error);
}
};
+
+async function assertDashboardPassword(dashboard: {
+ id: string;
+ password: string | null;
+}) {
+ if (!dashboard.password) {
+ return;
+ }
+
+ const cookiePassword = (await cookies()).get(
+ `dub_password_${dashboard.id}`,
+ )?.value;
+
+ if (cookiePassword !== dashboard.password) {
+ throw new DubApiError({
+ code: "unauthorized",
+ message: "This dashboard is password protected",
+ });
+ }
+}
diff --git a/apps/web/app/api/links/bulk/route.ts b/apps/web/app/api/links/bulk/route.ts
index a5c736069fb..60a917d0a08 100644
--- a/apps/web/app/api/links/bulk/route.ts
+++ b/apps/web/app/api/links/bulk/route.ts
@@ -11,6 +11,7 @@ import { includeProgramEnrollment } from "@/lib/api/links/include-program-enroll
import { includeTags } from "@/lib/api/links/include-tags";
import { throwIfLinksUsageExceeded } from "@/lib/api/links/usage-checks";
import { checkIfLinksHaveFolders } from "@/lib/api/links/utils/check-if-links-have-folders";
+import { checkIfLinksHaveProgramPartners } from "@/lib/api/links/utils/check-if-links-have-program-partners";
import { isRootDomainLinkKey } from "@/lib/api/links/utils/is-root-domain-link-key";
import { combineTagIds } from "@/lib/api/tags/combine-tag-ids";
import { parseRequestBody } from "@/lib/api/utils";
@@ -222,6 +223,55 @@ export const POST = withWorkspace(
});
}
+ if (checkIfLinksHaveProgramPartners(validLinks)) {
+ const partnerIds = [
+ ...new Set(
+ validLinks.map((link) => link.partnerId).filter(Boolean) as string[],
+ ),
+ ];
+
+ const enrollments =
+ workspace.defaultProgramId && partnerIds.length > 0
+ ? await prisma.programEnrollment.findMany({
+ where: {
+ programId: workspace.defaultProgramId,
+ partnerId: { in: partnerIds },
+ },
+ select: {
+ partnerId: true,
+ },
+ })
+ : [];
+
+ const enrolledPartnerIds = new Set(
+ enrollments.map(({ partnerId }) => partnerId),
+ );
+
+ validLinks = validLinks.filter((link) => {
+ if (link.programId && link.programId !== workspace.defaultProgramId) {
+ errorLinks.push({
+ error: `Invalid programId detected: ${link.programId}`,
+ code: "unprocessable_entity",
+ link,
+ });
+
+ return false;
+ }
+
+ if (link.partnerId && !enrolledPartnerIds.has(link.partnerId)) {
+ errorLinks.push({
+ error: `Invalid partnerId detected: ${link.partnerId}`,
+ code: "unprocessable_entity",
+ link,
+ });
+
+ return false;
+ }
+
+ return true;
+ });
+ }
+
if (checkIfLinksHaveWebhooks(validLinks)) {
if (workspace.plan === "free" || workspace.plan === "pro") {
throw new DubApiError({
@@ -425,6 +475,38 @@ export const PATCH = withWorkspace(
});
}
+ if (data.programId || data.partnerId) {
+ if (data.programId && data.programId !== workspace.defaultProgramId) {
+ throw new DubApiError({
+ code: "unprocessable_entity",
+ message: `Invalid programId detected: ${data.programId}`,
+ });
+ }
+
+ if (data.partnerId) {
+ const enrollment = workspace.defaultProgramId
+ ? await prisma.programEnrollment.findUnique({
+ where: {
+ partnerId_programId: {
+ partnerId: data.partnerId,
+ programId: workspace.defaultProgramId,
+ },
+ },
+ select: {
+ partnerId: true,
+ },
+ })
+ : null;
+
+ if (!enrollment) {
+ throw new DubApiError({
+ code: "unprocessable_entity",
+ message: `Invalid partnerId detected: ${data.partnerId}`,
+ });
+ }
+ }
+ }
+
const processedLinks = await Promise.all(
links.map(async (link) =>
processLink({
diff --git a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/webhooks/create-webhook-button.tsx b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/webhooks/create-webhook-button.tsx
index 86d923dfdb4..8d5ab89b2f0 100644
--- a/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/webhooks/create-webhook-button.tsx
+++ b/apps/web/app/app.dub.co/(dashboard)/[slug]/(ee)/settings/webhooks/create-webhook-button.tsx
@@ -27,9 +27,10 @@ export default function CreateWebhookButton() {
return (
);
diff --git a/apps/web/app/app.dub.co/(share)/share/[dashboardId]/action.ts b/apps/web/app/app.dub.co/(share)/share/[dashboardId]/action.ts
index 92bf4bfacc7..6ab5f934b1e 100644
--- a/apps/web/app/app.dub.co/(share)/share/[dashboardId]/action.ts
+++ b/apps/web/app/app.dub.co/(share)/share/[dashboardId]/action.ts
@@ -20,7 +20,7 @@ export async function verifyPassword(_prevState: any, data: FormData) {
}
(await cookies()).set(`dub_password_${dashboardId}`, password, {
- path: `/share/${dashboardId}`,
+ path: "/",
httpOnly: true,
secure: true,
});
diff --git a/apps/web/lib/api/commissions/create-manual-commissions.ts b/apps/web/lib/api/commissions/create-manual-commissions.ts
index 53f08033b76..01a9653a8a0 100644
--- a/apps/web/lib/api/commissions/create-manual-commissions.ts
+++ b/apps/web/lib/api/commissions/create-manual-commissions.ts
@@ -480,14 +480,18 @@ async function recordEvents(args: RecordEventsArgs) {
const clickId = nanoid(16);
const clickedAt = new Date(finalLeadEventDate.getTime() - 5 * 60 * 1000);
- // Record click event
+ // Record a dummy click event (start of the conversion funnel)
const clickEvent = recordClickZodSchema.parse({
timestamp: clickedAt.toISOString(),
identity_hash: targetCustomer.externalId || targetCustomer.id,
+ workspace_id: workspace.id,
click_id: clickId,
link_id: targetLink.id,
+ domain: targetLink.domain,
+ key: targetLink.key,
url: targetLink.url,
ip: "127.0.0.1",
+ country: targetCustomer.country,
continent: targetCustomer.country
? COUNTRIES_TO_CONTINENTS[targetCustomer.country.toUpperCase()] || ""
: "",
diff --git a/apps/web/lib/api/links/process-link.ts b/apps/web/lib/api/links/process-link.ts
index 7e3378408c9..9847aaee65f 100644
--- a/apps/web/lib/api/links/process-link.ts
+++ b/apps/web/lib/api/links/process-link.ts
@@ -436,22 +436,27 @@ export async function processLink>({
}
}
- // Program validity checks
+ // only perform program validity checks if not bulk creation (we do that check separately in the route itself)
if (programId && !skipProgramChecks) {
+ if (!partnerId && !tenantId) {
+ return {
+ link: payload,
+ error:
+ "programId was passed but no valid partnerId or tenantId was provided.",
+ code: "unprocessable_entity",
+ };
+ }
+
const program = await prisma.program.findUnique({
where: { id: programId },
select: {
workspaceId: true,
defaultFolderId: true,
- ...(!partnerId && tenantId
- ? {
- partners: {
- where: {
- tenantId,
- },
- },
- }
- : {}),
+ partners: {
+ where: {
+ ...(partnerId ? { partnerId } : { tenantId }),
+ },
+ },
},
});
@@ -463,11 +468,15 @@ export async function processLink>({
};
}
- if (!partnerId) {
- partnerId =
- program?.partners?.length > 0 ? program.partners[0].partnerId : null;
+ if (!program.partners.length) {
+ return {
+ link: payload,
+ error: "Invalid partnerId or tenantId provided.",
+ code: "not_found",
+ };
}
+ partnerId = program.partners[0].partnerId;
defaultProgramFolderId = program.defaultFolderId;
}
diff --git a/apps/web/lib/api/links/utils/check-if-links-have-program-partners.ts b/apps/web/lib/api/links/utils/check-if-links-have-program-partners.ts
new file mode 100644
index 00000000000..a82f0065fd1
--- /dev/null
+++ b/apps/web/lib/api/links/utils/check-if-links-have-program-partners.ts
@@ -0,0 +1,3 @@
+export const checkIfLinksHaveProgramPartners = (
+ links: { programId?: string | null; partnerId?: string | null }[],
+) => links.some((link) => link.programId || link.partnerId);
diff --git a/apps/web/playwright/api/links/links-bulk.spec.ts b/apps/web/playwright/api/links/links-bulk.spec.ts
new file mode 100644
index 00000000000..6856159e541
--- /dev/null
+++ b/apps/web/playwright/api/links/links-bulk.spec.ts
@@ -0,0 +1,268 @@
+import { nanoid } from "@dub/utils";
+import { expect } from "@playwright/test";
+import { apiError } from "../../utils";
+import { test, type ApiClient } from "../fixtures";
+import { createPartner, deletePartner } from "../partners/helpers";
+import { TEST_WORKSPACE } from "../setup-test-workspace";
+
+type BulkLink = {
+ id: string;
+ url: string;
+ domain: string;
+ programId: string | null;
+ partnerId: string | null;
+};
+
+type BulkLinkError = {
+ error: string;
+ code: string;
+ link: Record;
+};
+
+const domain = TEST_WORKSPACE.program.domain;
+
+function bulkLinkBody(overrides: Record = {}) {
+ return {
+ url: `https://example.com/${nanoid()}`,
+ domain,
+ ...overrides,
+ };
+}
+
+async function createBulkLinks(
+ api: ApiClient,
+ bodies: Record[],
+) {
+ return api.post<(BulkLink | BulkLinkError)[]>("/api/links/bulk", bodies);
+}
+
+async function deleteLinks(api: ApiClient, ids: (string | undefined)[]) {
+ const linkIds = ids.filter((id): id is string => Boolean(id));
+ if (linkIds.length === 0) return;
+ await api.delete(`/api/links/bulk?linkIds=${linkIds.join(",")}`);
+}
+
+function isBulkError(item: BulkLink | BulkLinkError): item is BulkLinkError {
+ return "error" in item;
+}
+
+test("POST /links/bulk – with valid programId and partnerId", async ({
+ api,
+ program,
+}) => {
+ let partnerId: string | undefined;
+ const createdIds: string[] = [];
+
+ try {
+ const { data: partner } = await createPartner(api);
+ partnerId = partner.id;
+
+ const body = bulkLinkBody({
+ programId: program.id,
+ partnerId,
+ });
+
+ const { status, data } = await createBulkLinks(api, [body]);
+ const created = data.filter((item) => !isBulkError(item));
+ createdIds.push(...created.map((link) => link.id));
+
+ expect(status).toEqual(200);
+ expect(created).toHaveLength(1);
+ expect(created[0]).toMatchObject({
+ url: body.url,
+ domain,
+ programId: program.id,
+ partnerId,
+ });
+ } finally {
+ await deleteLinks(api, createdIds);
+ await deletePartner(partnerId);
+ }
+});
+
+test("POST /links/bulk – rejects invalid programId", async ({ api }) => {
+ const invalidProgramId = `prog_${nanoid()}`;
+ const validBody = bulkLinkBody();
+ const invalidBody = bulkLinkBody({
+ programId: invalidProgramId,
+ partnerId: `pn_${nanoid()}`,
+ });
+
+ const createdIds: string[] = [];
+
+ try {
+ const { status, data } = await createBulkLinks(api, [
+ invalidBody,
+ validBody,
+ ]);
+ const created = data.filter((item) => !isBulkError(item));
+ const errors = data.filter(isBulkError);
+ createdIds.push(...created.map((link) => link.id));
+
+ expect(status).toEqual(200);
+ expect(created).toHaveLength(1);
+ expect(created[0].url).toEqual(validBody.url);
+ expect(errors).toEqual([
+ {
+ error: `Invalid programId detected: ${invalidProgramId}`,
+ code: "unprocessable_entity",
+ link: expect.any(Object),
+ },
+ ]);
+ } finally {
+ await deleteLinks(api, createdIds);
+ }
+});
+
+test("POST /links/bulk – rejects invalid partnerId", async ({
+ api,
+ program,
+}) => {
+ const invalidPartnerId = `pn_${nanoid()}`;
+ const validBody = bulkLinkBody();
+ const invalidBody = bulkLinkBody({
+ programId: program.id,
+ partnerId: invalidPartnerId,
+ });
+
+ const createdIds: string[] = [];
+
+ try {
+ const { status, data } = await createBulkLinks(api, [
+ invalidBody,
+ validBody,
+ ]);
+ const created = data.filter((item) => !isBulkError(item));
+ const errors = data.filter(isBulkError);
+ createdIds.push(...created.map((link) => link.id));
+
+ expect(status).toEqual(200);
+ expect(created).toHaveLength(1);
+ expect(created[0].url).toEqual(validBody.url);
+ expect(errors).toEqual([
+ {
+ error: `Invalid partnerId detected: ${invalidPartnerId}`,
+ code: "unprocessable_entity",
+ link: expect.any(Object),
+ },
+ ]);
+ } finally {
+ await deleteLinks(api, createdIds);
+ }
+});
+
+test("PATCH /links/bulk – with valid programId and partnerId", async ({
+ api,
+ program,
+}) => {
+ let partnerId: string | undefined;
+ const createdIds: string[] = [];
+
+ try {
+ const { data: partner } = await createPartner(api);
+ partnerId = partner.id;
+
+ const { data: created } = await createBulkLinks(api, [
+ bulkLinkBody(),
+ bulkLinkBody(),
+ ]);
+ const links = created.filter((item) => !isBulkError(item));
+ createdIds.push(...links.map((link) => link.id));
+
+ expect(links).toHaveLength(2);
+
+ const { status, data } = await api.patch("/api/links/bulk", {
+ linkIds: links.map((link) => link.id),
+ data: {
+ programId: program.id,
+ partnerId,
+ },
+ });
+
+ expect(status).toEqual(200);
+ expect(data).toHaveLength(2);
+ expect(data).toEqual(
+ expect.arrayContaining(
+ links.map((link) =>
+ expect.objectContaining({
+ id: link.id,
+ programId: program.id,
+ partnerId,
+ }),
+ ),
+ ),
+ );
+ } finally {
+ await deleteLinks(api, createdIds);
+ await deletePartner(partnerId);
+ }
+});
+
+test("PATCH /links/bulk – rejects invalid programId", async ({ api }) => {
+ const createdIds: string[] = [];
+ const invalidProgramId = `prog_${nanoid()}`;
+
+ try {
+ const { data: created } = await createBulkLinks(api, [bulkLinkBody()]);
+ const links = created.filter((item) => !isBulkError(item));
+ createdIds.push(...links.map((link) => link.id));
+
+ expect(
+ await api.patch("/api/links/bulk", {
+ linkIds: links.map((link) => link.id),
+ data: {
+ programId: invalidProgramId,
+ },
+ }),
+ ).toEqual(
+ apiError({
+ code: "unprocessable_entity",
+ message: `Invalid programId detected: ${invalidProgramId}`,
+ }),
+ );
+
+ const { data: unchanged } = await api.get(
+ `/api/links/${links[0].id}`,
+ );
+ expect(unchanged.programId).toBeNull();
+ } finally {
+ await deleteLinks(api, createdIds);
+ }
+});
+
+test("PATCH /links/bulk – rejects invalid partnerId", async ({
+ api,
+ program,
+}) => {
+ const createdIds: string[] = [];
+ const invalidPartnerId = `pn_${nanoid()}`;
+
+ try {
+ const { data: created } = await createBulkLinks(api, [bulkLinkBody()]);
+ const links = created.filter((item) => !isBulkError(item));
+ createdIds.push(...links.map((link) => link.id));
+
+ expect(
+ await api.patch("/api/links/bulk", {
+ linkIds: links.map((link) => link.id),
+ data: {
+ programId: program.id,
+ partnerId: invalidPartnerId,
+ },
+ }),
+ ).toEqual(
+ apiError({
+ code: "unprocessable_entity",
+ message: `Invalid partnerId detected: ${invalidPartnerId}`,
+ }),
+ );
+
+ const { data: unchanged } = await api.get(
+ `/api/links/${links[0].id}`,
+ );
+ expect(unchanged.partnerId).toBeNull();
+ expect(unchanged.programId).toBeNull();
+ } finally {
+ await deleteLinks(api, createdIds);
+ }
+});
diff --git a/apps/web/ui/analytics/device-icon.tsx b/apps/web/ui/analytics/device-icon.tsx
index 56cdbac8161..5e16221a741 100644
--- a/apps/web/ui/analytics/device-icon.tsx
+++ b/apps/web/ui/analytics/device-icon.tsx
@@ -12,6 +12,7 @@ import {
Watch,
Window,
} from "@dub/ui/icons";
+import { cn } from "@dub/utils";
import { TRIGGER_DISPLAY } from "./trigger-display";
export function DeviceIcon({
@@ -56,7 +57,8 @@ export function DeviceIcon({
alt={display}
width={20}
height={20}
- className={className}
+ // Non-square sources (e.g. Facebook/Instagram at 3:2) squash without this
+ className={cn(className, "object-cover")}
/>
);
}
diff --git a/apps/web/ui/partners/program-application-sheet.tsx b/apps/web/ui/partners/program-application-sheet.tsx
index bbac4fb78bc..dd1434fcb08 100644
--- a/apps/web/ui/partners/program-application-sheet.tsx
+++ b/apps/web/ui/partners/program-application-sheet.tsx
@@ -60,11 +60,9 @@ function ProgramApplicationSheetContent({
program?.defaultGroupId ||
DEFAULT_PARTNER_GROUP.slug;
- const {
- data: group,
- isLoading: isGroupLoading,
- error: groupError,
- } = useSWR>(
+ const { data: group, error: groupError } = useSWR<
+ z.infer
+ >(
groupIdOrSlug
? `/api/partner-profile/programs/${program.id}/groups/${groupIdOrSlug}`
: null,
diff --git a/apps/web/ui/postbacks/postback-secret-modal.tsx b/apps/web/ui/postbacks/postback-secret-modal.tsx
index d43734b7e9b..ffd58efed15 100644
--- a/apps/web/ui/postbacks/postback-secret-modal.tsx
+++ b/apps/web/ui/postbacks/postback-secret-modal.tsx
@@ -85,10 +85,13 @@ export function usePostbackSecretModal() {
setState({ show: false, secret: "" });
}
- function PostbackSecretModalWrapper() {
- if (!state.show) return null;
-
- return (
+ return {
+ openPostbackSecretModal,
+ closePostbackSecretModal,
+ // Return an element (rendered as `{PostbackSecretModal}`) rather than a
+ // per-render component type, which would remount the modal – and reset its
+ // state – on every parent render
+ PostbackSecretModal: (
{
@@ -97,13 +100,7 @@ export function usePostbackSecretModal() {
}}
secret={state.secret}
/>
- );
- }
-
- return {
- openPostbackSecretModal,
- closePostbackSecretModal,
- PostbackSecretModal: PostbackSecretModalWrapper,
+ ),
isPostbackSecretModalOpen: state.show,
};
}