feat: add licensing handling

This commit is contained in:
zhom
2026-01-11 03:58:46 +04:00
parent 75eb2c72a9
commit 2725cf9316
9 changed files with 781 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useState } from "react";
import { LoadingButton } from "@/components/loading-button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { showErrorToast } from "@/lib/toast-utils";
interface CommercialTrialModalProps {
isOpen: boolean;
onClose: () => void;
}
export function CommercialTrialModal({
isOpen,
onClose,
}: CommercialTrialModalProps) {
const [isAcknowledging, setIsAcknowledging] = useState(false);
const handleAcknowledge = useCallback(async () => {
setIsAcknowledging(true);
try {
await invoke("acknowledge_trial_expiration");
onClose();
} catch (error) {
console.error("Failed to acknowledge trial expiration:", error);
showErrorToast("Failed to save acknowledgment", {
description:
error instanceof Error ? error.message : "Please try again",
});
} finally {
setIsAcknowledging(false);
}
}, [onClose]);
return (
<Dialog open={isOpen}>
<DialogContent
className="sm:max-w-md"
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>Commercial Trial Expired</DialogTitle>
<DialogDescription>
Your 2-week commercial trial period has ended.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<p className="text-sm text-muted-foreground">
If you are using Donut Browser for business purposes, you need to
purchase a commercial license to continue.
</p>
<p className="text-sm font-medium">
Personal use remains free and unrestricted.
</p>
<p className="text-xs text-muted-foreground">
Visit our website to learn more about commercial licensing options.
</p>
</div>
<DialogFooter>
<LoadingButton
onClick={handleAcknowledge}
isLoading={isAcknowledging}
>
I Understand
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
+112
View File
@@ -37,8 +37,10 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useCommercialTrial } from "@/hooks/use-commercial-trial";
import type { PermissionType } from "@/hooks/use-permissions";
import { usePermissions } from "@/hooks/use-permissions";
import { useWayfernTerms } from "@/hooks/use-wayfern-terms";
import {
getThemeByColors,
getThemeById,
@@ -115,6 +117,10 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
isMicrophoneAccessGranted,
isCameraAccessGranted,
} = usePermissions();
const { termsAccepted } = useWayfernTerms();
const { trialStatus } = useCommercialTrial();
const [mcpEnabled, setMcpEnabled] = useState(false);
const [isMcpStarting, setIsMcpStarting] = useState(false);
const getPermissionIcon = useCallback((type: PermissionType) => {
switch (type) {
@@ -417,6 +423,16 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
}
}, []);
const loadMcpServerStatus = useCallback(async () => {
try {
const isRunning = await invoke<boolean>("get_mcp_server_status");
setMcpEnabled(isRunning);
} catch (error) {
console.error("Failed to load MCP server status:", error);
setMcpEnabled(false);
}
}, []);
const handleClose = useCallback(() => {
// Restore original theme when closing without saving
if (originalSettings.theme === "custom" && originalSettings.custom_theme) {
@@ -455,6 +471,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
loadSettings().catch(console.error);
checkDefaultBrowserStatus().catch(console.error);
loadApiServerStatus().catch(console.error);
loadMcpServerStatus().catch(console.error);
// Check if we're on macOS
const userAgent = navigator.userAgent;
@@ -481,6 +498,7 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
checkDefaultBrowserStatus,
loadSettings,
loadApiServerStatus,
loadMcpServerStatus,
]);
// Update permissions when the permission states change
@@ -1047,6 +1065,100 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
)}
</div>
{/* Commercial License Section */}
<div className="space-y-4">
<Label className="text-base font-medium">Commercial License</Label>
<div className="flex items-center justify-between p-3 rounded-md border bg-muted/40">
{trialStatus?.type === "Active" ? (
<div className="space-y-1">
<p className="text-sm font-medium">
Trial: {trialStatus.days_remaining} days,{" "}
{trialStatus.hours_remaining} hours remaining
</p>
<p className="text-xs text-muted-foreground">
Commercial use is free during the trial period
</p>
</div>
) : (
<div className="space-y-1">
<p className="text-sm font-medium text-orange-600">
Trial expired
</p>
<p className="text-xs text-muted-foreground">
Personal use remains free. Commercial use requires a
license.
</p>
</div>
)}
</div>
</div>
{/* MCP Server Section */}
<div className="space-y-4">
<Label className="text-base font-medium">MCP Server</Label>
<div className="flex items-center space-x-2">
<Checkbox
id="mcp-enabled"
checked={mcpEnabled}
disabled={!termsAccepted || isMcpStarting}
onCheckedChange={async (checked: boolean) => {
setIsMcpStarting(true);
try {
if (checked) {
await invoke("start_mcp_server");
setMcpEnabled(true);
showSuccessToast("MCP server started");
} else {
await invoke("stop_mcp_server");
setMcpEnabled(false);
showSuccessToast("MCP server stopped");
}
} catch (e) {
console.error("Failed to toggle MCP server:", e);
showErrorToast("Failed to toggle MCP server", {
description:
e instanceof Error ? e.message : "Unknown error",
});
} finally {
setIsMcpStarting(false);
}
}}
/>
<div className="grid gap-1.5 leading-none">
<Label
htmlFor="mcp-enabled"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Enable MCP Server (Model Context Protocol)
</Label>
<p className="text-xs text-muted-foreground">
Allow AI assistants to control Wayfern and Camoufox browsers
via MCP.
{!termsAccepted && (
<span className="ml-1 text-orange-600">
(Accept terms first)
</span>
)}
</p>
</div>
</div>
{mcpEnabled && (
<div className="p-3 space-y-2 text-xs rounded-md border bg-muted/40">
<div className="font-medium">Available MCP Tools</div>
<ul className="list-disc ml-5 space-y-0.5 text-muted-foreground">
<li>list_profiles - List Wayfern/Camoufox profiles</li>
<li>run_profile - Launch a browser profile</li>
<li>kill_profile - Stop a running browser</li>
<li>get_profile - Get profile details</li>
<li>list_proxies - List configured proxies</li>
</ul>
</div>
)}
</div>
{/* Advanced Section */}
<div className="space-y-4">
<Label className="text-base font-medium">Advanced</Label>
+85
View File
@@ -0,0 +1,85 @@
"use client";
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useState } from "react";
import { LoadingButton } from "@/components/loading-button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
interface WayfernTermsDialogProps {
isOpen: boolean;
onAccepted: () => void;
}
export function WayfernTermsDialog({
isOpen,
onAccepted,
}: WayfernTermsDialogProps) {
const [isAccepting, setIsAccepting] = useState(false);
const handleAccept = useCallback(async () => {
setIsAccepting(true);
try {
await invoke("accept_wayfern_terms");
showSuccessToast("Terms accepted successfully");
onAccepted();
} catch (error) {
console.error("Failed to accept terms:", error);
showErrorToast("Failed to accept terms", {
description:
error instanceof Error ? error.message : "Please try again",
});
} finally {
setIsAccepting(false);
}
}, [onAccepted]);
return (
<Dialog open={isOpen}>
<DialogContent
className="sm:max-w-lg"
onEscapeKeyDown={(e) => e.preventDefault()}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>Wayfern Terms and Conditions</DialogTitle>
<DialogDescription>
Before using Donut Browser, you must read and agree to Wayfern's
Terms and Conditions.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
<p className="text-sm text-muted-foreground">
Please review the Terms and Conditions at:
</p>
<a
href="https://wayfern.com/terms-and-conditions"
target="_blank"
rel="noopener noreferrer"
className="text-primary hover:underline text-sm font-medium block"
>
https://wayfern.com/terms-and-conditions
</a>
<p className="text-sm text-muted-foreground">
By clicking "I Accept", you agree to be bound by these terms.
</p>
</div>
<DialogFooter>
<LoadingButton onClick={handleAccept} isLoading={isAccepting}>
I Accept
</LoadingButton>
</DialogFooter>
</DialogContent>
</Dialog>
);
}