Gate replays.
This commit is contained in:
@@ -348,6 +348,7 @@
|
|||||||
"saved": "Saved",
|
"saved": "Saved",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
"untitled": "Untitled",
|
"untitled": "Untitled",
|
||||||
|
"upgrade": "Upgrade",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
@@ -426,6 +427,7 @@
|
|||||||
"unauthorized": "Unauthorized",
|
"unauthorized": "Unauthorized",
|
||||||
"user-deleted": "User deleted.",
|
"user-deleted": "User deleted.",
|
||||||
"viewed-page": "Viewed page",
|
"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>"
|
"visitor-log": "Visitor from <b>{country}</b> using <b>{browser}</b> on <b>{os}</b> <b>{device}</b>"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
'use client';
|
'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 { type Key, useState } from 'react';
|
||||||
import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
|
import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
|
||||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages, useSubscription } from '@/components/hooks';
|
||||||
import { getItem, setItem } from '@/lib/storage';
|
import { getItem, setItem } from '@/lib/storage';
|
||||||
import { ReplayModal } from './ReplayModal';
|
import { ReplayModal } from './ReplayModal';
|
||||||
import { ReplaysDataTable } from './ReplaysDataTable';
|
import { ReplaysDataTable } from './ReplaysDataTable';
|
||||||
@@ -14,13 +14,29 @@ const KEY_NAME = 'umami.replays.tab';
|
|||||||
|
|
||||||
export function ReplaysPage({ websiteId }: { websiteId: string }) {
|
export function ReplaysPage({ websiteId }: { websiteId: string }) {
|
||||||
const [tab, setTab] = useState(getItem(KEY_NAME) || 'replays');
|
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) => {
|
const handleSelect = (value: Key) => {
|
||||||
setItem(KEY_NAME, value);
|
setItem(KEY_NAME, value);
|
||||||
setTab(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 (
|
return (
|
||||||
<Column gap="3">
|
<Column gap="3">
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Slider,
|
Slider,
|
||||||
Switch,
|
Switch,
|
||||||
|
Text,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useMessages, useUpdateQuery, useWebsite } from '@/components/hooks';
|
import { useMessages, useSubscription, useUpdateQuery, useWebsite } from '@/components/hooks';
|
||||||
|
|
||||||
interface ReplayConfig {
|
interface ReplayConfig {
|
||||||
sampleRate?: number;
|
sampleRate?: number;
|
||||||
@@ -22,6 +23,7 @@ interface ReplayConfig {
|
|||||||
export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
|
export function WebsiteReplaySettings({ websiteId }: { websiteId: string }) {
|
||||||
const website = useWebsite();
|
const website = useWebsite();
|
||||||
const { t, labels, messages } = useMessages();
|
const { t, labels, messages } = useMessages();
|
||||||
|
const { hasFeature, cloudMode } = useSubscription();
|
||||||
const { mutateAsync, touch, toast, isPending } = useUpdateQuery(`/websites/${websiteId}`);
|
const { mutateAsync, touch, toast, isPending } = useUpdateQuery(`/websites/${websiteId}`);
|
||||||
const [enabled, setEnabled] = useState(website?.replayEnabled ?? false);
|
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 (
|
return (
|
||||||
<Column gap="4">
|
<Column gap="4">
|
||||||
<Label>{t(labels.replays)}</Label>
|
<Label>{t(labels.replays)}</Label>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { fetchAccount } from '@/lib/load';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { json } from '@/lib/response';
|
import { json } from '@/lib/response';
|
||||||
import { getAllUserTeams } from '@/queries/prisma';
|
import { getAllUserTeams } from '@/queries/prisma';
|
||||||
@@ -9,7 +10,20 @@ export async function POST(request: Request) {
|
|||||||
return error();
|
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 { secret } from '@/lib/crypto';
|
||||||
import { getClientInfo, hasBlockedIp } from '@/lib/detect';
|
import { getClientInfo, hasBlockedIp } from '@/lib/detect';
|
||||||
import { parseToken } from '@/lib/jwt';
|
import { parseToken } from '@/lib/jwt';
|
||||||
|
import { fetchAccount } from '@/lib/load';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { badRequest, forbidden, json, serverError } from '@/lib/response';
|
import { badRequest, forbidden, json, serverError } from '@/lib/response';
|
||||||
import { getWebsite } from '@/queries/prisma';
|
import { getWebsite } from '@/queries/prisma';
|
||||||
@@ -60,6 +61,14 @@ export async function POST(request: Request) {
|
|||||||
return json({ ok: false, reason: 'replay_disabled' });
|
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
|
// Client info for bot/IP checks
|
||||||
const { ip, userAgent } = await getClientInfo(request, {});
|
const { ip, userAgent } = await getClientInfo(request, {});
|
||||||
|
|
||||||
|
|||||||
@@ -96,5 +96,6 @@ export * from './usePageParameters';
|
|||||||
export * from './useRegionNames';
|
export * from './useRegionNames';
|
||||||
export * from './useSlug';
|
export * from './useSlug';
|
||||||
export * from './useSticky';
|
export * from './useSticky';
|
||||||
|
export * from './useSubscription';
|
||||||
export * from './useTimezone';
|
export * from './useTimezone';
|
||||||
export * from './useWebsiteNavItems';
|
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',
|
retentionDays: 'label.retention-days',
|
||||||
duration: 'label.duration',
|
duration: 'label.duration',
|
||||||
recorded: 'label.recorded',
|
recorded: 'label.recorded',
|
||||||
|
upgrade: 'label.upgrade',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const messages: Record<string, string> = {
|
export const messages: Record<string, string> = {
|
||||||
@@ -427,4 +428,5 @@ export const messages: Record<string, string> = {
|
|||||||
forbidden: 'message.forbidden',
|
forbidden: 'message.forbidden',
|
||||||
notFound: 'message.not-found',
|
notFound: 'message.not-found',
|
||||||
serverError: 'message.sever-error',
|
serverError: 'message.sever-error',
|
||||||
|
upgradeRequired: 'message.upgrade-required',
|
||||||
};
|
};
|
||||||
Reference in New Issue
Block a user