Add sharing to pixel and link detail pages

This commit is contained in:
Mike Cao
2026-03-09 01:26:02 -07:00
parent c4226bc63d
commit 64bf4c378c
23 changed files with 582 additions and 43 deletions
+18 -6
View File
@@ -1,19 +1,31 @@
import { Row } from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useLink, useMessages, useSlug } from '@/components/hooks';
import { ExternalLink, Link } from '@/components/icons';
import { LinkShareButton } from './LinkShareButton';
export function LinkHeader() {
const { t, labels } = useMessages();
const { getSlugUrl } = useSlug('link');
export function LinkHeader({ showActions = true }: { showActions?: boolean }) {
const link = useLink();
return (
<PageHeader title={link.name} description={link.url} icon={<Link />}>
<LinkButton href={getSlugUrl(link.slug)} target="_blank" prefetch={false} asAnchor>
<IconLabel icon={<ExternalLink />} label={t(labels.view)} />
</LinkButton>
{showActions && link.id && <LinkHeaderActions linkId={link.id} slug={link.slug} />}
</PageHeader>
);
}
function LinkHeaderActions({ linkId, slug }: { linkId: string; slug: string }) {
const { t, labels } = useMessages();
const { getSlugUrl } = useSlug('link');
return (
<Row alignItems="center" gap="3">
<LinkShareButton linkId={linkId} />
<LinkButton href={getSlugUrl(slug)} target="_blank" prefetch={false} asAnchor>
<IconLabel icon={<ExternalLink />} label={t(labels.view)} />
</LinkButton>
</Row>
);
}
+8 -2
View File
@@ -12,13 +12,19 @@ import { Panel } from '@/components/common/Panel';
const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
export function LinkPage({ linkId }: { linkId: string }) {
export function LinkPage({
linkId,
showHeaderActions = true,
}: {
linkId: string;
showHeaderActions?: boolean;
}) {
return (
<LinkProvider linkId={linkId}>
<Grid width="100%" height="100%">
<Column margin="2">
<PageBody gap>
<LinkHeader />
<LinkHeader showActions={showHeaderActions} />
<LinkControls linkId={linkId} />
<LinkMetricsBar linkId={linkId} showChange={true} />
<Panel>
@@ -0,0 +1,14 @@
import { useMessages } from '@/components/hooks';
import { Share } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { LinkShareDialog } from './LinkShareDialog';
export function LinkShareButton({ linkId }: { linkId: string }) {
const { t, labels } = useMessages();
return (
<DialogButton icon={<Share />} label={t(labels.share)} title={null} width="900px">
<LinkShareDialog linkId={linkId} />
</DialogButton>
);
}
@@ -0,0 +1,62 @@
import { Button, Column, Heading, Row, Text } from '@umami/react-zen';
import { useState } from 'react';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { IconLabel } from '@/components/common/IconLabel';
import { useLinkSharesQuery, useMessages } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm';
import { SimpleSharesTable } from '@/components/share/SimpleSharesTable';
export function LinkShareDialog({ linkId }: { linkId: string }) {
const { data, error, isLoading } = useLinkSharesQuery({ linkId });
const shares = data?.data || [];
const hasShares = shares.length > 0;
return (
<LoadingPanel data={data} isLoading={isLoading} error={error}>
<LinkShareDialogContent linkId={linkId} hasShares={hasShares} shares={shares} />
</LoadingPanel>
);
}
function LinkShareDialogContent({
linkId,
hasShares,
shares,
}: {
linkId: string;
hasShares: boolean;
shares: any[];
}) {
const { t, labels, messages } = useMessages();
const [isCreating, setIsCreating] = useState(false);
const showCreateForm = !hasShares || isCreating;
return (
<Column gap="4">
<Row justifyContent="space-between" alignItems="center">
<Heading>{t(labels.share)}</Heading>
{hasShares && !isCreating && (
<Button variant="primary" onPress={() => setIsCreating(true)}>
<IconLabel icon={<Plus size={16} />} label={t(labels.add)} />
</Button>
)}
</Row>
{showCreateForm && (
<SimpleShareCreateForm
createPath={`/links/${linkId}/shares`}
onSave={() => setIsCreating(false)}
onCancel={hasShares ? () => setIsCreating(false) : undefined}
/>
)}
{hasShares ? (
<>
<Text>{t(messages.shareUrl)}</Text>
<SimpleSharesTable data={shares} />
</>
) : (
!showCreateForm && <Text color="muted">{t(messages.noDataAvailable)}</Text>
)}
</Column>
);
}
@@ -1,19 +1,31 @@
import { Row } from '@umami/react-zen';
import { IconLabel } from '@/components/common/IconLabel';
import { LinkButton } from '@/components/common/LinkButton';
import { PageHeader } from '@/components/common/PageHeader';
import { useMessages, usePixel, useSlug } from '@/components/hooks';
import { ExternalLink, Grid2x2 } from '@/components/icons';
import { PixelShareButton } from './PixelShareButton';
export function PixelHeader() {
const { t, labels } = useMessages();
const { getSlugUrl } = useSlug('pixel');
export function PixelHeader({ showActions = true }: { showActions?: boolean }) {
const pixel = usePixel();
return (
<PageHeader title={pixel.name} icon={<Grid2x2 />}>
<LinkButton href={getSlugUrl(pixel.slug)} target="_blank" prefetch={false} asAnchor>
<IconLabel icon={<ExternalLink />} label={t(labels.view)} />
</LinkButton>
{showActions && pixel.id && <PixelHeaderActions pixelId={pixel.id} slug={pixel.slug} />}
</PageHeader>
);
}
function PixelHeaderActions({ pixelId, slug }: { pixelId: string; slug: string }) {
const { t, labels } = useMessages();
const { getSlugUrl } = useSlug('pixel');
return (
<Row alignItems="center" gap="3">
<PixelShareButton pixelId={pixelId} />
<LinkButton href={getSlugUrl(slug)} target="_blank" prefetch={false} asAnchor>
<IconLabel icon={<ExternalLink />} label={t(labels.view)} />
</LinkButton>
</Row>
);
}
@@ -12,13 +12,19 @@ import { Panel } from '@/components/common/Panel';
const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event'];
export function PixelPage({ pixelId }: { pixelId: string }) {
export function PixelPage({
pixelId,
showHeaderActions = true,
}: {
pixelId: string;
showHeaderActions?: boolean;
}) {
return (
<PixelProvider pixelId={pixelId}>
<Grid width="100%" height="100%">
<Column margin="2">
<PageBody gap>
<PixelHeader />
<PixelHeader showActions={showHeaderActions} />
<PixelControls pixelId={pixelId} />
<PixelMetricsBar pixelId={pixelId} showChange={true} />
<Panel>
@@ -0,0 +1,14 @@
import { useMessages } from '@/components/hooks';
import { Share } from '@/components/icons';
import { DialogButton } from '@/components/input/DialogButton';
import { PixelShareDialog } from './PixelShareDialog';
export function PixelShareButton({ pixelId }: { pixelId: string }) {
const { t, labels } = useMessages();
return (
<DialogButton icon={<Share />} label={t(labels.share)} title={null} width="900px">
<PixelShareDialog pixelId={pixelId} />
</DialogButton>
);
}
@@ -0,0 +1,62 @@
import { Button, Column, Heading, Row, Text } from '@umami/react-zen';
import { useState } from 'react';
import { LoadingPanel } from '@/components/common/LoadingPanel';
import { IconLabel } from '@/components/common/IconLabel';
import { useMessages, usePixelSharesQuery } from '@/components/hooks';
import { Plus } from '@/components/icons';
import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm';
import { SimpleSharesTable } from '@/components/share/SimpleSharesTable';
export function PixelShareDialog({ pixelId }: { pixelId: string }) {
const { data, error, isLoading } = usePixelSharesQuery({ pixelId });
const shares = data?.data || [];
const hasShares = shares.length > 0;
return (
<LoadingPanel data={data} isLoading={isLoading} error={error}>
<PixelShareDialogContent pixelId={pixelId} hasShares={hasShares} shares={shares} />
</LoadingPanel>
);
}
function PixelShareDialogContent({
pixelId,
hasShares,
shares,
}: {
pixelId: string;
hasShares: boolean;
shares: any[];
}) {
const { t, labels, messages } = useMessages();
const [isCreating, setIsCreating] = useState(false);
const showCreateForm = !hasShares || isCreating;
return (
<Column gap="4">
<Row justifyContent="space-between" alignItems="center">
<Heading>{t(labels.share)}</Heading>
{hasShares && !isCreating && (
<Button variant="primary" onPress={() => setIsCreating(true)}>
<IconLabel icon={<Plus size={16} />} label={t(labels.add)} />
</Button>
)}
</Row>
{showCreateForm && (
<SimpleShareCreateForm
createPath={`/pixels/${pixelId}/shares`}
onSave={() => setIsCreating(false)}
onCancel={hasShares ? () => setIsCreating(false) : undefined}
/>
)}
{hasShares ? (
<>
<Text>{t(messages.shareUrl)}</Text>
<SimpleSharesTable data={shares} />
</>
) : (
!showCreateForm && <Text color="muted">{t(messages.noDataAvailable)}</Text>
)}
</Column>
);
}
@@ -0,0 +1,73 @@
import { z } from 'zod';
import { ENTITY_TYPE } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
import { getRandomChars } from '@/lib/generate';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { filterParams, pagingParams } from '@/lib/schema';
import { canUpdateLink, canViewLink } from '@/permissions';
import { createShare, getSharesByEntityId } from '@/queries/prisma';
export async function GET(
request: Request,
{ params }: { params: Promise<{ linkId: string }> },
) {
const schema = z.object({
...filterParams,
...pagingParams,
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { linkId } = await params;
const { page, pageSize, search } = query;
if (!(await canViewLink(auth, linkId))) {
return unauthorized();
}
const data = await getSharesByEntityId(linkId, {
page,
pageSize,
search,
});
return json(data);
}
export async function POST(
request: Request,
{ params }: { params: Promise<{ linkId: string }> },
) {
const schema = z.object({
name: z.string().max(200),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { linkId } = await params;
const { name } = body;
if (!(await canUpdateLink(auth, linkId))) {
return unauthorized();
}
const share = await createShare({
id: uuid(),
entityId: linkId,
shareType: ENTITY_TYPE.link,
name,
slug: getRandomChars(16),
parameters: {},
});
return json(share);
}
@@ -0,0 +1,73 @@
import { z } from 'zod';
import { ENTITY_TYPE } from '@/lib/constants';
import { uuid } from '@/lib/crypto';
import { getRandomChars } from '@/lib/generate';
import { parseRequest } from '@/lib/request';
import { json, unauthorized } from '@/lib/response';
import { filterParams, pagingParams } from '@/lib/schema';
import { canUpdatePixel, canViewPixel } from '@/permissions';
import { createShare, getSharesByEntityId } from '@/queries/prisma';
export async function GET(
request: Request,
{ params }: { params: Promise<{ pixelId: string }> },
) {
const schema = z.object({
...filterParams,
...pagingParams,
});
const { auth, query, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { pixelId } = await params;
const { page, pageSize, search } = query;
if (!(await canViewPixel(auth, pixelId))) {
return unauthorized();
}
const data = await getSharesByEntityId(pixelId, {
page,
pageSize,
search,
});
return json(data);
}
export async function POST(
request: Request,
{ params }: { params: Promise<{ pixelId: string }> },
) {
const schema = z.object({
name: z.string().max(200),
});
const { auth, body, error } = await parseRequest(request, schema);
if (error) {
return error();
}
const { pixelId } = await params;
const { name } = body;
if (!(await canUpdatePixel(auth, pixelId))) {
return unauthorized();
}
const share = await createShare({
id: uuid(),
entityId: pixelId,
shareType: ENTITY_TYPE.pixel,
name,
slug: getRandomChars(16),
parameters: {},
});
return json(share);
}
+38 -14
View File
@@ -6,17 +6,17 @@ import prisma from '@/lib/prisma';
import redis from '@/lib/redis';
import { json, notFound } from '@/lib/response';
import type { WhiteLabel } from '@/lib/types';
import { getBoard, getShareByCode, getWebsite } from '@/queries/prisma';
import { getBoard, getLink, getPixel, getShareByCode, getWebsite } from '@/queries/prisma';
async function getAccountId(website: { userId?: string; teamId?: string }): Promise<string | null> {
if (website.userId) {
return website.userId;
async function getAccountId(entity: { userId?: string; teamId?: string }): Promise<string | null> {
if (entity.userId) {
return entity.userId;
}
if (website.teamId) {
if (entity.teamId) {
const teamOwner = await prisma.client.teamUser.findFirst({
where: {
teamId: website.teamId,
teamId: entity.teamId,
role: ROLES.teamOwner,
},
select: {
@@ -73,22 +73,46 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
return json(data);
}
const website = await getWebsite(share.entityId);
if (!website) {
return notFound();
}
let entity: { userId?: string; teamId?: string } | null = null;
const data: Record<string, any> = {
shareId: share.id,
shareType: share.shareType,
websiteId: share.entityId,
parameters: share.parameters,
};
if (share.shareType === ENTITY_TYPE.website) {
entity = await getWebsite(share.entityId);
if (!entity) {
return notFound();
}
data.websiteId = share.entityId;
} else if (share.shareType === ENTITY_TYPE.pixel) {
entity = await getPixel(share.entityId);
if (!entity) {
return notFound();
}
data.websiteId = share.entityId;
data.pixelId = share.entityId;
} else if (share.shareType === ENTITY_TYPE.link) {
entity = await getLink(share.entityId);
if (!entity) {
return notFound();
}
data.websiteId = share.entityId;
data.linkId = share.entityId;
} else {
return notFound();
}
data.token = createToken(data, secret());
const accountId = await getAccountId(website);
const accountId = await getAccountId(entity);
if (accountId) {
const whiteLabel = await getWhiteLabel(accountId);
+5 -2
View File
@@ -13,6 +13,8 @@ export interface ShareData {
websiteId?: string;
websiteIds?: string[];
boardId?: string;
pixelId?: string;
linkId?: string;
parameters: any;
token: string;
whiteLabel?: WhiteLabel;
@@ -54,12 +56,13 @@ export function ShareProvider({ slug, children }: { slug: string; children: Reac
const pathname = usePathname();
const path = getSharePath(pathname);
const isBoardShare = share?.shareType === ENTITY_TYPE.board;
const isWebsiteShare = share?.shareType === ENTITY_TYPE.website;
const allowedSections = !isBoardShare && share?.parameters
const allowedSections = isWebsiteShare && share?.parameters
? ALL_SECTION_IDS.filter(id => share.parameters[id] !== false)
: [];
const shouldRedirect = !isBoardShare &&
const shouldRedirect = isWebsiteShare &&
allowedSections.length === 1 &&
allowedSections[0] !== 'overview' &&
(path === undefined || path === '' || path === 'overview');
+11 -1
View File
@@ -2,6 +2,8 @@
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import { LinkPage } from '@/app/(main)/links/[linkId]/LinkPage';
import { PixelPage } from '@/app/(main)/pixels/[pixelId]/PixelPage';
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
@@ -59,7 +61,7 @@ export function SharePage() {
const { setTheme } = useTheme();
const pathname = usePathname();
const path = getSharePath(pathname);
const { websiteId, boardId, parameters = {}, shareType } = share;
const { websiteId, boardId, pixelId, linkId, parameters = {}, shareType } = share;
useEffect(() => {
const url = new URL(window?.location?.href);
@@ -81,6 +83,14 @@ export function SharePage() {
);
}
if (shareType === ENTITY_TYPE.pixel && pixelId) {
return <PixelPage pixelId={pixelId} showHeaderActions={false} />;
}
if (shareType === ENTITY_TYPE.link && linkId) {
return <LinkPage linkId={linkId} showHeaderActions={false} />;
}
// Check if the requested path is allowed
const pageKey = path || '';
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
+2
View File
@@ -22,9 +22,11 @@ export * from './queries/useEventDataPropertiesQuery';
export * from './queries/useEventDataQuery';
export * from './queries/useEventDataValuesQuery';
export * from './queries/useLinkQuery';
export * from './queries/useLinkSharesQuery';
export * from './queries/useLinksQuery';
export * from './queries/useLoginQuery';
export * from './queries/usePixelQuery';
export * from './queries/usePixelSharesQuery';
export * from './queries/usePixelsQuery';
export * from './queries/useRealtimeQuery';
export * from './queries/useReplayQuery';
@@ -0,0 +1,20 @@
import type { ReactQueryOptions } from '@/lib/types';
import { useApi } from '../useApi';
import { useModified } from '../useModified';
import { usePagedQuery } from '../usePagedQuery';
export function useLinkSharesQuery(
{ linkId }: { linkId: string },
options?: ReactQueryOptions,
) {
const { modified } = useModified('shares');
const { get } = useApi();
return usePagedQuery({
queryKey: ['linkShares', { linkId, modified }],
queryFn: pageParams => {
return get(`/links/${linkId}/shares`, pageParams);
},
...options,
});
}
@@ -0,0 +1,20 @@
import type { ReactQueryOptions } from '@/lib/types';
import { useApi } from '../useApi';
import { useModified } from '../useModified';
import { usePagedQuery } from '../usePagedQuery';
export function usePixelSharesQuery(
{ pixelId }: { pixelId: string },
options?: ReactQueryOptions,
) {
const { modified } = useModified('shares');
const { get } = useApi();
return usePagedQuery({
queryKey: ['pixelShares', { pixelId, modified }],
queryFn: pageParams => {
return get(`/pixels/${pixelId}/shares`, pageParams);
},
...options,
});
}
+3 -1
View File
@@ -2,7 +2,9 @@ import { useConfig } from '@/components/hooks/useConfig';
import { LINKS_URL, PIXELS_URL } from '@/lib/constants';
export function useSlug(type: 'link' | 'pixel') {
const { linksUrl, pixelsUrl } = useConfig();
const config = useConfig();
const linksUrl = config?.linksUrl;
const pixelsUrl = config?.pixelsUrl;
const hostUrl = type === 'link' ? linksUrl || LINKS_URL : pixelsUrl || PIXELS_URL;
@@ -0,0 +1,57 @@
import { Button, Column, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen';
import { useState } from 'react';
import { useApi, useMessages, useModified } from '@/components/hooks';
export function SimpleShareCreateForm({
createPath,
onSave,
onCancel,
}: {
createPath: string;
onSave?: () => void;
onCancel?: () => void;
}) {
const { post } = useApi();
const { touch } = useModified();
const { t, labels, getErrorMessage } = useMessages();
const [isPending, setIsPending] = useState(false);
const [error, setError] = useState<unknown>(null);
const handleSubmit = async (data: { name: string }) => {
setIsPending(true);
setError(null);
try {
await post(createPath, {
name: data.name,
});
touch('shares');
onSave?.();
} catch (e) {
setError(e);
} finally {
setIsPending(false);
}
};
return (
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ name: '' }}>
<Column gap="4">
<FormField label={t(labels.name)} name="name" rules={{ required: t(labels.required) }}>
<TextField autoComplete="off" autoFocus />
</FormField>
<Row justifyContent="flex-end" gap="3">
{onCancel && (
<Button isDisabled={isPending} onPress={onCancel}>
{t(labels.cancel)}
</Button>
)}
<FormSubmitButton variant="primary" isDisabled={isPending}>
{t(labels.add)}
</FormSubmitButton>
</Row>
</Column>
</Form>
);
}
@@ -0,0 +1,52 @@
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
import { ShareDeleteButton } from '@/app/(main)/websites/[websiteId]/settings/ShareDeleteButton';
import { CopyButton } from '@/components/common/CopyButton';
import { DateDistance } from '@/components/common/DateDistance';
import { ExternalLink } from '@/components/common/ExternalLink';
import { useConfig, useMessages, useMobile } from '@/components/hooks';
export function SimpleSharesTable(props: DataTableProps) {
const { t, labels } = useMessages();
const { cloudMode } = useConfig();
const { isMobile } = useMobile();
const getUrl = (slug: string) => {
if (cloudMode) {
return `${process.env.cloudUrl}/share/${slug}`;
}
return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
};
return (
<DataTable {...props} displayMode={isMobile ? 'cards' : 'table'}>
<DataColumn id="name" label={t(labels.name)}>
{({ name }: any) => name}
</DataColumn>
<DataColumn id="slug" label={t(labels.shareUrl)} width="2fr">
{({ slug }: any) => {
const url = getUrl(slug);
return (
<Row alignItems="center" gap="1" overflow="hidden">
<ExternalLink href={url} prefetch={false}>
{isMobile ? slug : url}
</ExternalLink>
<CopyButton value={url} label="Copy URL" />
</Row>
);
}}
</DataColumn>
<DataColumn id="created" label={t(labels.created)}>
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
</DataColumn>
<DataColumn id="action" align="end" width="60px">
{({ id, slug }: any) => (
<Row>
<ShareDeleteButton shareId={id} slug={slug} />
</Row>
)}
</DataColumn>
</DataTable>
);
}
+2
View File
@@ -23,6 +23,8 @@ export interface Auth {
websiteId?: string;
websiteIds?: string[];
boardId?: string;
pixelId?: string;
linkId?: string;
};
}
+8 -4
View File
@@ -3,15 +3,19 @@ import { PERMISSIONS } from '@/lib/constants';
import type { Auth } from '@/lib/types';
import { getLink, getTeamUser } from '@/queries/prisma';
export async function canViewLink({ user }: Auth, linkId: string) {
if (!user) {
return false;
export async function canViewLink({ user, shareToken }: Auth, linkId: string) {
if (user?.isAdmin) {
return true;
}
if (user.isAdmin) {
if (shareToken?.linkId === linkId || shareToken?.websiteId === linkId) {
return true;
}
if (!user) {
return false;
}
const link = await getLink(linkId);
if (link.userId) {
+8 -4
View File
@@ -3,15 +3,19 @@ import { PERMISSIONS } from '@/lib/constants';
import type { Auth } from '@/lib/types';
import { getPixel, getTeamUser } from '@/queries/prisma';
export async function canViewPixel({ user }: Auth, pixelId: string) {
if (!user) {
return false;
export async function canViewPixel({ user, shareToken }: Auth, pixelId: string) {
if (user?.isAdmin) {
return true;
}
if (user.isAdmin) {
if (shareToken?.pixelId === pixelId || shareToken?.websiteId === pixelId) {
return true;
}
if (!user) {
return false;
}
const pixel = await getPixel(pixelId);
if (pixel.userId) {
+6 -1
View File
@@ -9,7 +9,12 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
return true;
}
if (shareToken?.websiteId === websiteId || shareToken?.websiteIds?.includes(websiteId)) {
if (
shareToken?.websiteId === websiteId ||
shareToken?.pixelId === websiteId ||
shareToken?.linkId === websiteId ||
shareToken?.websiteIds?.includes(websiteId)
) {
return true;
}