Gate replays.

This commit is contained in:
Mike Cao
2026-03-15 18:03:36 -07:00
parent 534af98809
commit 6e1a05f686
8 changed files with 109 additions and 6 deletions
+2
View File
@@ -348,6 +348,7 @@
"saved": "Saved",
"unknown": "Unknown",
"untitled": "Untitled",
"upgrade": "Upgrade",
"update": "Update",
"url": "URL",
"user": "User",
@@ -426,6 +427,7 @@
"unauthorized": "Unauthorized",
"user-deleted": "User deleted.",
"viewed-page": "Viewed page",
"upgrade-required": "This feature requires a {plan} plan subscription.",
"visitor-log": "Visitor from <b>{country}</b> using <b>{browser}</b> on <b>{os}</b> <b>{device}</b>"
}
}
@@ -1,10 +1,10 @@
'use client';
import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
import { Button, Column, Tab, TabList, TabPanel, Tabs, Text } from '@umami/react-zen';
import { type Key, useState } from 'react';
import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
import { Panel } from '@/components/common/Panel';
import { useMessages } from '@/components/hooks';
import { useMessages, useSubscription } from '@/components/hooks';
import { getItem, setItem } from '@/lib/storage';
import { ReplayModal } from './ReplayModal';
import { ReplaysDataTable } from './ReplaysDataTable';
@@ -14,13 +14,29 @@ const KEY_NAME = 'umami.replays.tab';
export function ReplaysPage({ websiteId }: { websiteId: string }) {
const [tab, setTab] = useState(getItem(KEY_NAME) || 'replays');
const { t, labels } = useMessages();
const { t, labels, messages } = useMessages();
const { hasFeature, cloudMode } = useSubscription();
const handleSelect = (value: Key) => {
setItem(KEY_NAME, value);
setTab(value);
};
if (cloudMode && !hasFeature('replays')) {
return (
<Column gap="3">
<Panel>
<Column gap="4" alignItems="center" padding="10">
<Text>{t(messages.upgradeRequired, { plan: 'Business' })}</Text>
<Button variant="primary" onPress={() => window.open(`${process.env.cloudUrl}/settings/billing`, '_blank')}>
{t(labels.upgrade)}
</Button>
</Column>
</Panel>
</Column>
);
}
return (
<Column gap="3">
<WebsiteControls websiteId={websiteId} />
@@ -7,10 +7,11 @@ import {
Select,
Slider,
Switch,
Text,
TextField,
} from '@umami/react-zen';
import { useState } from 'react';
import { useMessages, useUpdateQuery, useWebsite } from '@/components/hooks';
import { useMessages, useSubscription, useUpdateQuery, useWebsite } from '@/components/hooks';
interface ReplayConfig {
sampleRate?: number;
@@ -22,6 +23,7 @@ interface ReplayConfig {
export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
const website = useWebsite();
const { t, labels, messages } = useMessages();
const { hasFeature, cloudMode } = useSubscription();
const { mutateAsync, touch, toast, isPending } = useUpdateQuery(`/websites/${websiteId}`);
const [enabled, setEnabled] = useState(website?.replayEnabled ?? false);
@@ -75,6 +77,20 @@ export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
);
};
if (cloudMode && !hasFeature('replays')) {
return (
<Column gap="4">
<Label>{t(labels.replays)}</Label>
<Column gap="4" alignItems="center" padding="10">
<Text>{t(messages.upgradeRequired, { plan: 'Business' })}</Text>
<Button variant="primary" onPress={() => window.open(`${process.env.cloudUrl}/settings/billing`, '_blank')}>
{t(labels.upgrade)}
</Button>
</Column>
</Column>
);
}
return (
<Column gap="4">
<Label>{t(labels.replays)}</Label>
+16 -2
View File
@@ -1,3 +1,4 @@
import { fetchAccount } from '@/lib/load';
import { parseRequest } from '@/lib/request';
import { json } from '@/lib/response';
import { getAllUserTeams } from '@/queries/prisma';
@@ -9,7 +10,20 @@ export async function POST(request: Request) {
return error();
}
const teams = await getAllUserTeams(auth.user.id);
const user = { ...auth.user };
const teams = await getAllUserTeams(user.id);
return json({ ...auth.user, teams });
if (process.env.CLOUD_MODE) {
const account = await fetchAccount(user.id);
if (account) {
user.subscription = {
isPro: account.isPro || false,
isBusiness: account.isBusiness || false,
hasSubscription: account.hasSubscription || false,
};
}
}
return json({ ...user, teams });
}
+9
View File
@@ -4,6 +4,7 @@ import { z } from 'zod';
import { secret } from '@/lib/crypto';
import { getClientInfo, hasBlockedIp } from '@/lib/detect';
import { parseToken } from '@/lib/jwt';
import { fetchAccount } from '@/lib/load';
import { parseRequest } from '@/lib/request';
import { badRequest, forbidden, json, serverError } from '@/lib/response';
import { getWebsite } from '@/queries/prisma';
@@ -60,6 +61,14 @@ export async function POST(request: Request) {
return json({ ok: false, reason: 'replay_disabled' });
}
if (process.env.CLOUD_MODE) {
const account = await fetchAccount(website.userId);
if (!account?.isBusiness) {
return forbidden({ message: 'Business subscription required.' });
}
}
// Client info for bot/IP checks
const { ip, userAgent } = await getClientInfo(request, {});
+1
View File
@@ -96,5 +96,6 @@ export * from './usePageParameters';
export * from './useRegionNames';
export * from './useSlug';
export * from './useSticky';
export * from './useSubscription';
export * from './useTimezone';
export * from './useWebsiteNavItems';
+43
View File
@@ -0,0 +1,43 @@
import { useApp } from '@/store/app';
import { useConfig } from './useConfig';
export interface Subscription {
isPro: boolean;
isBusiness: boolean;
hasSubscription: boolean;
}
const FEATURES = {
replays: 'isBusiness',
} as const;
export type FeatureName = keyof typeof FEATURES;
const defaultSubscription: Subscription = {
isPro: false,
isBusiness: false,
hasSubscription: false,
};
export function useSubscription() {
const { user } = useApp();
const config = useConfig();
const subscription: Subscription = user?.subscription || defaultSubscription;
const cloudMode = config?.cloudMode || false;
function hasFeature(feature: FeatureName): boolean {
if (!cloudMode) {
return true;
}
const requiredFlag = FEATURES[feature];
return subscription[requiredFlag] || false;
}
return {
...subscription,
cloudMode,
hasFeature,
};
}
+2
View File
@@ -378,6 +378,7 @@ export const labels: Record<string, string> = {
retentionDays: 'label.retention-days',
duration: 'label.duration',
recorded: 'label.recorded',
upgrade: 'label.upgrade',
};
export const messages: Record<string, string> = {
@@ -427,4 +428,5 @@ export const messages: Record<string, string> = {
forbidden: 'message.forbidden',
notFound: 'message.not-found',
serverError: 'message.sever-error',
upgradeRequired: 'message.upgrade-required',
};