Skip to content

Commit 3f86f21

Browse files
committed
feat: enhance tier-based functionality across components
- Updated LicenseNag to simplify pricing options button text. - Enhanced BatchActionsDropdown to conditionally enable actions based on user tier, integrating upgrade prompts. - Modified LocalSyncDialog to filter available clients based on user tier, ensuring appropriate access. - Improved PersonalMcpSection to reflect cloud sync availability based on tier status. - Refined ServerStatusSwitch to provide upgrade prompts for tier restrictions. - Adjusted LocalTable and LocalTableHeader to conditionally display sync options and upgrade prompts based on user tier. - Updated ClientSelector to enforce tier-based client selection logic, allowing for development mode exceptions. - Enhanced useTier hook to include comprehensive tier access checks and cloud sync availability.
1 parent 535b814 commit 3f86f21

14 files changed

Lines changed: 352 additions & 122 deletions

File tree

tauri-app/src/components/common/LicenseNag.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,7 @@ export function LicenseNag() {
118118
<DialogFooter className="gap-2 sm:justify-between">
119119
<Button variant="outline" onClick={onDismiss}>Maybe later</Button>
120120
<div className="flex gap-2">
121-
<Button onClick={() => openUrl("https://mcp-linker.store/pricing")}>Get Lifetime (One‑Time)</Button>
122-
</div>
123-
<div className="flex gap-2">
124-
<Button onClick={() => openUrl("https://mcp-linker.store/pricing")}>Upgrade to Pro</Button>
121+
<Button onClick={() => openUrl("https://mcp-linker.store/pricing")}>View Pricing Options</Button>
125122
</div>
126123
</DialogFooter>
127124
</DialogContent>

tauri-app/src/components/manage/BatchActionsDropdown.tsx

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import {
55
DropdownMenuItem,
66
DropdownMenuTrigger,
77
} from "@/components/ui/dropdown-menu";
8+
import { useTier } from "@/hooks/useTier";
9+
import { useGlobalDialogStore } from "@/stores/globalDialogStore";
810
import { ChevronDownIcon } from "@radix-ui/react-icons";
911

1012
interface BatchActionsDropdownProps {
@@ -22,6 +24,28 @@ export const BatchActionsDropdown = ({
2224
isDeleting,
2325
hasSelectedRows,
2426
}: BatchActionsDropdownProps) => {
27+
const { hasMinimumTier } = useTier();
28+
const showGlobalDialog = useGlobalDialogStore((s) => s.showDialog);
29+
30+
// Enable/disable servers instantly requires LIFETIME or higher
31+
const canToggle = hasMinimumTier("LIFETIME") || import.meta.env.DEV;
32+
33+
const handleEnableClick = () => {
34+
if (!canToggle) {
35+
showGlobalDialog("upgrade");
36+
return;
37+
}
38+
handleBatchEnable?.();
39+
};
40+
41+
const handleDisableClick = () => {
42+
if (!canToggle) {
43+
showGlobalDialog("upgrade");
44+
return;
45+
}
46+
handleBatchDisable?.();
47+
};
48+
2549
return (
2650
<>
2751
{hasSelectedRows && (
@@ -33,13 +57,21 @@ export const BatchActionsDropdown = ({
3357
</DropdownMenuTrigger>
3458
<DropdownMenuContent>
3559
{handleBatchEnable && (
36-
<DropdownMenuItem onClick={handleBatchEnable}>
37-
Enable Selected
60+
<DropdownMenuItem
61+
onClick={handleEnableClick}
62+
disabled={!canToggle}
63+
className={!canToggle ? "opacity-50" : ""}
64+
>
65+
Enable Selected {!canToggle && "🔒"}
3866
</DropdownMenuItem>
3967
)}
4068
{handleBatchDisable && (
41-
<DropdownMenuItem onClick={handleBatchDisable}>
42-
Disable Selected
69+
<DropdownMenuItem
70+
onClick={handleDisableClick}
71+
disabled={!canToggle}
72+
className={!canToggle ? "opacity-50" : ""}
73+
>
74+
Disable Selected {!canToggle && "🔒"}
4375
</DropdownMenuItem>
4476
)}
4577
<DropdownMenuItem onClick={handleBatchDelete} disabled={isDeleting}>

tauri-app/src/components/manage/LocalSyncDialog.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from "@/components/ui/select";
1616
import { Switch } from "@/components/ui/switch";
1717
import { availableClients } from "@/constants/clients";
18+
import { useTier } from "@/hooks/useTier";
1819
import { needspathClient } from "@/lib/data";
1920
import { useClientPathStore } from "@/stores/clientPathStore";
2021
import { open as openDialog } from "@tauri-apps/plugin-dialog";
@@ -54,6 +55,8 @@ export function LocalSyncDialog({
5455

5556
const { getClientPath, setClientPath } = useClientPathStore();
5657

58+
const { tier, hasMinimumTier } = useTier();
59+
5760
// Effect to save syncFromClient to localStorage
5861
useEffect(() => {
5962
localStorage.setItem("syncFromClient", syncFromClient);
@@ -150,6 +153,21 @@ export function LocalSyncDialog({
150153
const needsFromPath = needspathClient.includes(syncFromClient);
151154
const needsToPath = needspathClient.includes(syncToClient);
152155

156+
const filteredClients = availableClients.filter((client) => {
157+
// Free users: only allow Claude and Windsurf
158+
if (tier === "FREE" || !tier) {
159+
return client.value === "claude" || client.value === "windsurf";
160+
}
161+
162+
// Lifetime users: exclude Codex and Claude Code
163+
if (tier === "LIFETIME") {
164+
return client.value !== "codex" && client.value !== "claude_code";
165+
}
166+
167+
// Other tiers: check minimum required tier
168+
return hasMinimumTier(client.requiredTier);
169+
});
170+
153171
return (
154172
<Dialog
155173
open={open}
@@ -182,7 +200,7 @@ export function LocalSyncDialog({
182200
/>
183201
</SelectTrigger>
184202
<SelectContent>
185-
{availableClients
203+
{filteredClients
186204
.filter((client) => client.value !== currentClient)
187205
.map((client) => (
188206
<SelectItem key={client.value} value={client.value}>
@@ -228,7 +246,7 @@ export function LocalSyncDialog({
228246
/>
229247
</SelectTrigger>
230248
<SelectContent>
231-
{availableClients.map((client) => (
249+
{filteredClients.map((client) => (
232250
<SelectItem key={client.value} value={client.value}>
233251
{client.label}
234252
</SelectItem>

tauri-app/src/components/manage/LocalTable/LocalTableHeader.tsx

Lines changed: 39 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,15 @@ export const LocalTableHeader: React.FC<LocalTableHeaderProps> = ({
3232
}) => {
3333
const key = getEncryptionKey();
3434
const navigate = useNavigate();
35-
const { isFree } = useTier();
35+
const { hasMinimumTier, canUseCloudSync } = useTier();
36+
37+
// Local sync requires LIFETIME or higher
38+
const canUseLocalSync = hasMinimumTier("LIFETIME") || import.meta.env.DEV;
3639

3740
return (
3841
<div className="flex justify-between items-center">
39-
{!isFree ?
40-
<div className="flex gap-3 items-center">
42+
<div className="flex gap-3 items-center">
43+
{canUseLocalSync ? (
4144
<Button
4245
variant="outline"
4346
size="sm"
@@ -48,29 +51,43 @@ export const LocalTableHeader: React.FC<LocalTableHeaderProps> = ({
4851
<Monitor className="h-4 w-4" />
4952
Local Sync
5053
</Button>
51-
<Button
52-
variant="outline"
53-
size="sm"
54-
onClick={onCloudSync}
55-
disabled={isSyncing}
56-
className="flex items-center gap-2 hover:bg-accent hover:border-accent-foreground"
57-
>
58-
<Cloud className="h-4 w-4" />
59-
Cloud Sync
60-
<Key className="h-3 w-3 opacity-60" />
61-
</Button>
54+
) : (
55+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
56+
<Monitor className="h-4 w-4 opacity-50" />
57+
<span>Local Sync 🔒</span>
58+
<UpgradePlanButton />
59+
</div>
60+
)}
6261

63-
{!key && (
64-
<Button onClick={() => navigate("/settings")}>
62+
{canUseCloudSync ? (
63+
<>
64+
<Button
65+
variant="outline"
66+
size="sm"
67+
onClick={onCloudSync}
68+
disabled={isSyncing}
69+
className="flex items-center gap-2 hover:bg-accent hover:border-accent-foreground"
70+
>
6571
<Cloud className="h-4 w-4" />
66-
Go to generate encryption key for end to end sync to cloud
72+
Cloud Sync
73+
<Key className="h-3 w-3 opacity-60" />
6774
</Button>
68-
)}
69-
</div>
70-
: <div className="flex gap-3 items-center">
71-
<UpgradePlanButton /> to sync bewteen multi clients
75+
76+
{!key && (
77+
<Button onClick={() => navigate("/settings")} size="sm" variant="outline">
78+
<Cloud className="h-4 w-4 mr-2" />
79+
Generate Encryption Key
80+
</Button>
81+
)}
82+
</>
83+
) : (
84+
<div className="flex items-center gap-2 text-sm text-muted-foreground">
85+
<Cloud className="h-4 w-4 opacity-50" />
86+
<span>Cloud Sync 🔒</span>
87+
<UpgradePlanButton />
7288
</div>
73-
}
89+
)}
90+
</div>
7491
<BatchActionsDropdown
7592
hasSelectedRows={Object.keys(rowSelection).length > 0}
7693
handleBatchEnable={handleBatchEnable}

tauri-app/src/components/manage/LocalTable/index.tsx

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { DataTable } from "@/components/ui/data-table";
1616
import { useCloudSync } from "@/hooks/useCloudSync";
1717
import { useMcpConfig } from "@/hooks/useMcpConfig";
18+
import { useTier } from "@/hooks/useTier";
1819
import { useClientPathStore } from "@/stores/clientPathStore";
1920
import { useGlobalDialogStore } from "@/stores/globalDialogStore";
2021
import { UserWithTier } from "@/stores/userStore";
@@ -32,7 +33,7 @@ interface LocalTableProps {
3233
user: UserWithTier;
3334
}
3435

35-
export const LocalTable = ({ isAuthenticated, user }: LocalTableProps) => {
36+
export const LocalTable = ({ isAuthenticated, user: _user }: LocalTableProps) => {
3637
const { selectedClient, selectedPath } = useClientPathStore();
3738
const [localSyncDialogOpen, setLocalSyncDialogOpen] = useState(false);
3839
const [cloudSyncDialogOpen, setCloudSyncDialogOpen] = useState(false);
@@ -118,28 +119,20 @@ export const LocalTable = ({ isAuthenticated, user }: LocalTableProps) => {
118119

119120
// Header action handlers
120121
const handleLocalSync = () => setLocalSyncDialogOpen(true);
122+
const { canUseCloudSync } = useTier();
123+
121124
const handleCloudSync = () => {
122125
if (!isAuthenticated) {
123126
showGlobalDialog("login");
124127
return;
125128
}
126-
if (user.tier === "FREE") {
127-
if (user.trialActive) {
128-
if (
129-
!user.trialEndsAt ||
130-
new Date(user.trialEndsAt).getTime() < Date.now()
131-
) {
132-
// Trial expired or no trial end date, show upgrade dialog
133-
showGlobalDialog("upgrade");
134-
return;
135-
}
136-
// Trial is active and not expired, allow sync (do nothing)
137-
} else {
138-
// No trial, show start trial dialog
139-
showGlobalDialog("startTrial");
140-
return;
141-
}
129+
130+
// Check if user has Professional or Team tier for cloud sync
131+
if (!canUseCloudSync) {
132+
showGlobalDialog("upgrade");
133+
return;
142134
}
135+
143136
if (!key) {
144137
setShowMissingKeyDialog(true);
145138
return;

tauri-app/src/components/manage/PersonalMcpSection.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { LocalTable } from "@/components/manage/LocalTable/index";
22
import { PersonalCloudTable } from "@/components/manage/PersonalCloudTable";
33
import { Button } from "@/components/ui/button";
44
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
5+
import { useTier } from "@/hooks/useTier";
56
import { Cloud } from "lucide-react";
67

78
/**
@@ -27,6 +28,8 @@ export function PersonalMcpSection({
2728
encryptionKey,
2829
navigate,
2930
}: PersonalMcpSectionProps) {
31+
const { canUseCloudSync } = useTier();
32+
3033
return (
3134
<Tabs
3235
value={personalTab}
@@ -37,7 +40,7 @@ export function PersonalMcpSection({
3740
<TabsTrigger value="personalLocal">Local</TabsTrigger>
3841
<TabsTrigger value="personalCloud">
3942
<Cloud />
40-
Cloud {user?.tier === "FREE" && "🔒"}
43+
Cloud {!canUseCloudSync && "🔒"}
4144
</TabsTrigger>
4245
</TabsList>
4346
<TabsContent value="personalLocal" className="flex-1 min-h-0">

tauri-app/src/components/manage/ServerStatusSwitch.tsx

Lines changed: 49 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,12 @@
11
import { Switch } from "@/components/ui/switch";
2+
import {
3+
Tooltip,
4+
TooltipContent,
5+
TooltipProvider,
6+
TooltipTrigger,
7+
} from "@/components/ui/tooltip";
8+
import { useTier } from "@/hooks/useTier";
9+
import { useGlobalDialogStore } from "@/stores/globalDialogStore";
210

311
interface ServerStatusSwitchProps {
412
serverName: string;
@@ -13,17 +21,51 @@ export function ServerStatusSwitch({
1321
onEnable,
1422
onDisable,
1523
}: ServerStatusSwitchProps) {
24+
const { hasMinimumTier } = useTier();
25+
const showGlobalDialog = useGlobalDialogStore((s) => s.showDialog);
26+
27+
// Enable/disable servers instantly requires LIFETIME or higher
28+
const canToggle = hasMinimumTier("LIFETIME") || import.meta.env.DEV;
29+
30+
const handleChange = (checked: boolean) => {
31+
if (!canToggle) {
32+
showGlobalDialog("upgrade");
33+
return;
34+
}
35+
36+
if (checked) {
37+
onEnable(serverName);
38+
} else {
39+
onDisable(serverName);
40+
}
41+
};
42+
43+
if (!canToggle) {
44+
return (
45+
<TooltipProvider>
46+
<Tooltip>
47+
<TooltipTrigger asChild>
48+
<div className="flex items-center">
49+
<Switch
50+
checked={isActive}
51+
disabled={true}
52+
className="data-[state=checked]:bg-green-600 opacity-50 cursor-not-allowed"
53+
/>
54+
</div>
55+
</TooltipTrigger>
56+
<TooltipContent>
57+
<p>Upgrade to Lifetime Basic or higher to enable/disable servers instantly</p>
58+
</TooltipContent>
59+
</Tooltip>
60+
</TooltipProvider>
61+
);
62+
}
63+
1664
return (
1765
<div className="flex items-center">
1866
<Switch
1967
checked={isActive}
20-
onCheckedChange={(checked) => {
21-
if (checked) {
22-
onEnable(serverName);
23-
} else {
24-
onDisable(serverName);
25-
}
26-
}}
68+
onCheckedChange={handleChange}
2769
className="data-[state=checked]:bg-green-600"
2870
/>
2971
</div>

0 commit comments

Comments
 (0)