Gate replays.
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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, {});
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user