feat: windows support

This commit is contained in:
zhom
2026-02-15 11:48:59 +04:00
parent dd5afac951
commit 63453331ff
46 changed files with 2445 additions and 328 deletions
+17 -2
View File
@@ -26,6 +26,7 @@ import { SettingsDialog } from "@/components/settings-dialog";
import { SyncConfigDialog } from "@/components/sync-config-dialog";
import { WayfernTermsDialog } from "@/components/wayfern-terms-dialog";
import { useAppUpdateNotifications } from "@/hooks/use-app-update-notifications";
import { useCloudAuth } from "@/hooks/use-cloud-auth";
import { useCommercialTrial } from "@/hooks/use-commercial-trial";
import { useGroupEvents } from "@/hooks/use-group-events";
import type { PermissionType } from "@/hooks/use-permissions";
@@ -88,6 +89,11 @@ export default function Home() {
checkTrialStatus,
} = useCommercialTrial();
// Cloud auth for cross-OS unlock
const { user: cloudUser } = useCloudAuth();
const crossOsUnlocked =
cloudUser?.plan !== "free" && cloudUser?.subscriptionStatus === "active";
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
const [integrationsDialogOpen, setIntegrationsDialogOpen] = useState(false);
@@ -719,6 +725,13 @@ export default function Home() {
void checkMissingBinaries();
}
// Proactively download Wayfern and Camoufox if not already available
if (!profilesLoading) {
void invoke("ensure_active_browsers_downloaded").catch((err: unknown) => {
console.error("Failed to auto-download browsers:", err);
});
}
return () => {
clearInterval(updateInterval);
if (cleanup) {
@@ -766,7 +779,7 @@ export default function Home() {
}
}, [profiles]);
// Show warning for non-wayfern/camoufox profiles (support ending March 1, 2026)
// Show warning for non-wayfern/camoufox profiles (support ending March 15, 2026)
useEffect(() => {
if (profiles.length === 0) return;
@@ -783,7 +796,7 @@ export default function Home() {
id: "browser-support-ending-warning",
type: "error",
title: "Browser support ending soon",
description: `Support for the following profiles will be removed on March 1, 2026: ${unsupportedNames}. Please migrate to Wayfern or Camoufox profiles.`,
description: `Support for the following profiles will be removed on March 15, 2026: ${unsupportedNames}. Please migrate to Wayfern or Camoufox profiles.`,
duration: 15000,
action: {
label: "Learn more",
@@ -917,6 +930,7 @@ export default function Home() {
}}
onCreateProfile={handleCreateProfile}
selectedGroupId={selectedGroupId}
crossOsUnlocked={crossOsUnlocked}
/>
<SettingsDialog
@@ -988,6 +1002,7 @@ export default function Home() {
? runningProfiles.has(currentProfileForCamoufoxConfig.id)
: false
}
crossOsUnlocked={crossOsUnlocked}
/>
<GroupManagementDialog
@@ -39,6 +39,7 @@ interface CamoufoxConfigDialogProps {
config: CamoufoxConfig,
) => Promise<void>;
isRunning?: boolean;
crossOsUnlocked?: boolean;
}
export function CamoufoxConfigDialog({
@@ -48,6 +49,7 @@ export function CamoufoxConfigDialog({
onSave,
onSaveWayfern,
isRunning = false,
crossOsUnlocked = false,
}: CamoufoxConfigDialogProps) {
// Use union type to support both Camoufox and Wayfern configs
const [config, setConfig] = useState<CamoufoxConfig | WayfernConfig>(() => ({
@@ -160,6 +162,7 @@ export function CamoufoxConfigDialog({
onConfigChange={updateConfig}
forceAdvanced={true}
readOnly={isRunning}
crossOsUnlocked={crossOsUnlocked}
/>
) : (
<SharedCamoufoxConfigForm
@@ -168,6 +171,7 @@ export function CamoufoxConfigDialog({
forceAdvanced={true}
readOnly={isRunning}
browserType="camoufox"
crossOsUnlocked={crossOsUnlocked}
/>
)}
</div>
+24
View File
@@ -71,6 +71,7 @@ interface CreateProfileDialogProps {
groupId?: string;
}) => Promise<void>;
selectedGroupId?: string;
crossOsUnlocked?: boolean;
}
interface BrowserOption {
@@ -106,6 +107,7 @@ export function CreateProfileDialog({
onClose,
onCreateProfile,
selectedGroupId,
crossOsUnlocked = false,
}: CreateProfileDialogProps) {
const [profileName, setProfileName] = useState("");
const [currentStep, setCurrentStep] = useState<
@@ -677,6 +679,16 @@ export function CreateProfileDialog({
</RippleButton>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!getBestAvailableVersion("wayfern") && (
<div className="flex gap-3 items-center p-3 rounded-md border border-yellow-500/50 bg-yellow-500/10">
<p className="text-sm text-yellow-500">
Wayfern is not available on your platform
yet.
</p>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!isBrowserCurrentlyDownloading("wayfern") &&
@@ -732,6 +744,7 @@ export function CreateProfileDialog({
config={wayfernConfig}
onConfigChange={updateWayfernConfig}
isCreating
crossOsUnlocked={crossOsUnlocked}
/>
</div>
) : selectedBrowser === "camoufox" ? (
@@ -763,6 +776,16 @@ export function CreateProfileDialog({
</RippleButton>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!getBestAvailableVersion("camoufox") && (
<div className="flex gap-3 items-center p-3 rounded-md border border-yellow-500/50 bg-yellow-500/10">
<p className="text-sm text-yellow-500">
Camoufox is not available on your platform
yet.
</p>
</div>
)}
{!isLoadingReleaseTypes &&
!releaseTypesError &&
!isBrowserCurrentlyDownloading("camoufox") &&
@@ -819,6 +842,7 @@ export function CreateProfileDialog({
onConfigChange={updateCamoufoxConfig}
isCreating
browserType="camoufox"
crossOsUnlocked={crossOsUnlocked}
/>
</div>
) : (
+41 -22
View File
@@ -1,6 +1,8 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuLock } from "react-icons/lu";
import MultipleSelector, { type Option } from "@/components/multiple-selector";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
@@ -29,6 +31,7 @@ interface SharedCamoufoxConfigFormProps {
forceAdvanced?: boolean; // Force advanced mode (for editing)
readOnly?: boolean; // Flag to indicate if the form should be read-only
browserType?: "camoufox" | "wayfern"; // Browser type to customize form options
crossOsUnlocked?: boolean; // Allow selecting non-current OS (paid feature)
}
// Determine if fingerprint editing should be disabled
@@ -118,7 +121,9 @@ export function SharedCamoufoxConfigForm({
forceAdvanced = false,
readOnly = false,
browserType = "camoufox",
crossOsUnlocked = false,
}: SharedCamoufoxConfigFormProps) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState(
forceAdvanced ? "manual" : "automatic",
);
@@ -128,7 +133,6 @@ export function SharedCamoufoxConfigForm({
// Get selected OS (defaults to current OS)
const selectedOS = config.os || currentOS;
const isOSDifferent = selectedOS !== currentOS;
// Set screen resolution to user's screen size when creating a new profile
useEffect(() => {
@@ -227,18 +231,25 @@ export function SharedCamoufoxConfigForm({
<SelectValue placeholder="Select operating system" />
</SelectTrigger>
<SelectContent>
<SelectItem value="windows">{osLabels.windows}</SelectItem>
<SelectItem value="macos">{osLabels.macos}</SelectItem>
<SelectItem value="linux">{osLabels.linux}</SelectItem>
{(["windows", "macos", "linux"] as CamoufoxOS[]).map((os) => {
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
{osLabels[os]}
{isDisabled && (
<LuLock className="w-3 h-3 text-muted-foreground" />
)}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{isOSDifferent && (
<Alert className="border-yellow-500/50 bg-yellow-500/10">
<AlertDescription className="text-yellow-600 dark:text-yellow-400">
Warning: Selecting an OS different from your current system (
{osLabels[currentOS]}) increases the risk of detection. Websites
can detect mismatches between your fingerprint and actual system
behavior.
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
{t("fingerprint.crossOsWarning")}
</AlertDescription>
</Alert>
)}
@@ -994,19 +1005,27 @@ export function SharedCamoufoxConfigForm({
<SelectValue placeholder="Select operating system" />
</SelectTrigger>
<SelectContent>
<SelectItem value="windows">{osLabels.windows}</SelectItem>
<SelectItem value="macos">{osLabels.macos}</SelectItem>
<SelectItem value="linux">{osLabels.linux}</SelectItem>
{(["windows", "macos", "linux"] as CamoufoxOS[]).map((os) => {
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
{osLabels[os]}
{isDisabled && (
<LuLock className="w-3 h-3 text-muted-foreground" />
)}
</span>
</SelectItem>
);
})}
</SelectContent>
</Select>
{isOSDifferent && (
<Alert className="border-yellow-500/50 bg-yellow-500/10">
<AlertDescription className="text-yellow-600 dark:text-yellow-400">
Warning: Selecting an OS different from your current
system ({osLabels[currentOS]}) increases the risk of
detection. Websites with advanced protections can detect
mismatches between your fingerprint and actual system
behavior.
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
Cross-OS fingerprinting has limitations. System-level APIs
may still reflect your actual operating system, and some
features may have degraded performance.
</AlertDescription>
</Alert>
)}
+297 -82
View File
@@ -2,6 +2,7 @@
import { invoke } from "@tauri-apps/api/core";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuEye, LuEyeOff } from "react-icons/lu";
import { LoadingButton } from "@/components/loading-button";
import { Button } from "@/components/ui/button";
@@ -15,11 +16,13 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useCloudAuth } from "@/hooks/use-cloud-auth";
import { showErrorToast, showSuccessToast } from "@/lib/toast-utils";
import type { SyncSettings } from "@/types";
@@ -29,6 +32,9 @@ interface SyncConfigDialogProps {
}
export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
const { t } = useTranslation();
// Self-hosted state
const [serverUrl, setServerUrl] = useState("");
const [token, setToken] = useState("");
const [isLoading, setIsLoading] = useState(false);
@@ -36,6 +42,24 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
const [isTesting, setIsTesting] = useState(false);
const [showToken, setShowToken] = useState(false);
// Cloud auth state
const {
user,
isLoggedIn,
isLoading: isCloudLoading,
requestOtp,
verifyOtp,
logout,
} = useCloudAuth();
const [email, setEmail] = useState("");
const [otpCode, setOtpCode] = useState("");
const [codeSent, setCodeSent] = useState(false);
const [isSendingCode, setIsSendingCode] = useState(false);
const [isVerifying, setIsVerifying] = useState(false);
// Default to self-hosted tab if self-hosted is configured and not cloud-logged-in
const [activeTab, setActiveTab] = useState<string>("cloud");
const loadSettings = useCallback(async () => {
setIsLoading(true);
try {
@@ -52,9 +76,19 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
useEffect(() => {
if (isOpen) {
void loadSettings();
setCodeSent(false);
setOtpCode("");
setEmail("");
}
}, [isOpen, loadSettings]);
// If self-hosted is configured and not cloud-logged-in, default to self-hosted tab
useEffect(() => {
if (!isCloudLoading && !isLoggedIn && serverUrl && token) {
setActiveTab("self-hosted");
}
}, [isCloudLoading, isLoggedIn, serverUrl, token]);
const handleTestConnection = useCallback(async () => {
if (!serverUrl) {
showErrorToast("Please enter a server URL");
@@ -112,102 +146,283 @@ export function SyncConfigDialog({ isOpen, onClose }: SyncConfigDialogProps) {
}
}, []);
const handleSendCode = useCallback(async () => {
if (!email) return;
setIsSendingCode(true);
try {
await requestOtp(email);
setCodeSent(true);
showSuccessToast(t("sync.cloud.codeSent"));
} catch (error) {
console.error("Failed to send OTP:", error);
showErrorToast(String(error));
} finally {
setIsSendingCode(false);
}
}, [email, requestOtp, t]);
const handleVerifyOtp = useCallback(async () => {
if (!email || !otpCode) return;
setIsVerifying(true);
try {
await verifyOtp(email, otpCode);
showSuccessToast(t("sync.cloud.loginSuccess"));
// Restart sync service with cloud credentials
try {
await invoke("restart_sync_service");
} catch (e) {
console.error("Failed to restart sync service:", e);
}
} catch (error) {
console.error("OTP verification failed:", error);
showErrorToast(String(error));
} finally {
setIsVerifying(false);
}
}, [email, otpCode, verifyOtp, t]);
const handleCloudLogout = useCallback(async () => {
try {
await logout();
showSuccessToast(t("sync.cloud.logoutSuccess"));
// Restart sync service (will fall back to self-hosted or stop)
try {
await invoke("restart_sync_service");
} catch (e) {
console.error("Failed to restart sync service:", e);
}
} catch (error) {
console.error("Failed to logout:", error);
showErrorToast(String(error));
}
}, [logout, t]);
const isConnected = Boolean(serverUrl && token);
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Sync Service</DialogTitle>
<DialogDescription>
Configure connection to a sync server to synchronize your profiles
across devices.
</DialogDescription>
<DialogTitle>{t("sync.title")}</DialogTitle>
<DialogDescription>{t("sync.description")}</DialogDescription>
</DialogHeader>
{isLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="sync-server-url">Server URL</Label>
<Input
id="sync-server-url"
placeholder="https://sync.example.com"
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<TabsList className="w-full">
<TabsTrigger value="cloud" className="flex-1">
{t("sync.cloud.tabLabel")}
</TabsTrigger>
<TabsTrigger value="self-hosted" className="flex-1">
{t("sync.cloud.selfHostedTabLabel")}
</TabsTrigger>
</TabsList>
<div className="space-y-2">
<Label htmlFor="sync-token">Access Token</Label>
<div className="relative">
<Input
id="sync-token"
type={showToken ? "text" : "password"}
placeholder="Enter your sync token"
value={token}
onChange={(e) => setToken(e.target.value)}
className="pr-10"
/>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
aria-label={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
) : (
<LuEye className="w-4 h-4 text-muted-foreground hover:text-foreground" />
)}
</button>
</TooltipTrigger>
<TooltipContent>
{showToken ? "Hide token" : "Show token"}
</TooltipContent>
</Tooltip>
<TabsContent value="cloud">
{isCloudLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
</div>
) : isLoggedIn && user ? (
<div className="grid gap-4 py-4">
<div className="flex gap-2 items-center text-sm">
<div className="w-2 h-2 rounded-full bg-green-500" />
{t("sync.cloud.connected")}
</div>
{isConnected && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-green-500" />
Connected
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.email")}
</span>
<span>{user.email}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.plan")}
</span>
<span className="capitalize">
{user.plan}
{user.planPeriod ? ` (${user.planPeriod})` : ""}
</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">
{t("sync.cloud.profiles")}
</span>
<span>
{t("sync.cloud.profileUsage", {
used: user.cloudProfilesUsed,
limit: user.profileLimit,
})}
</span>
</div>
</div>
<div className="flex gap-2 pt-2">
<Button variant="outline" className="flex-1" asChild>
<a
href="https://donutbrowser.com/account"
target="_blank"
rel="noopener noreferrer"
>
{t("sync.cloud.manageAccount")}
</a>
</Button>
<Button
variant="outline"
className="flex-1"
onClick={handleCloudLogout}
>
{t("sync.cloud.logout")}
</Button>
</div>
</div>
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="cloud-email">{t("sync.cloud.email")}</Label>
<div className="flex gap-2">
<Input
id="cloud-email"
type="email"
placeholder={t("sync.cloud.emailPlaceholder")}
value={email}
onChange={(e) => setEmail(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !codeSent) {
void handleSendCode();
}
}}
/>
<LoadingButton
onClick={handleSendCode}
isLoading={isSendingCode}
disabled={!email || codeSent}
variant="outline"
>
{t("sync.cloud.sendCode")}
</LoadingButton>
</div>
</div>
{codeSent && (
<div className="space-y-2">
<Label htmlFor="cloud-otp">
{t("sync.cloud.verificationCode")}
</Label>
<Input
id="cloud-otp"
placeholder={t("sync.cloud.codePlaceholder")}
value={otpCode}
onChange={(e) => setOtpCode(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
void handleVerifyOtp();
}
}}
/>
<LoadingButton
onClick={handleVerifyOtp}
isLoading={isVerifying}
disabled={!otpCode}
className="w-full"
>
{isVerifying
? t("sync.cloud.loggingIn")
: t("sync.cloud.verifyAndLogin")}
</LoadingButton>
</div>
)}
</div>
)}
</div>
)}
</TabsContent>
<DialogFooter className="flex gap-2">
{isConnected && (
<Button
variant="outline"
onClick={handleDisconnect}
disabled={isSaving}
>
Disconnect
</Button>
)}
<Button
variant="outline"
onClick={handleTestConnection}
disabled={isTesting || !serverUrl}
>
{isTesting ? "Testing..." : "Test Connection"}
</Button>
<LoadingButton
onClick={handleSave}
isLoading={isSaving}
disabled={!serverUrl || !token}
>
Save
</LoadingButton>
</DialogFooter>
<TabsContent value="self-hosted">
{isLoading ? (
<div className="flex justify-center py-8">
<div className="w-6 h-6 rounded-full border-2 border-current animate-spin border-t-transparent" />
</div>
) : (
<div className="grid gap-4 py-4">
<div className="space-y-2">
<Label htmlFor="sync-server-url">{t("sync.serverUrl")}</Label>
<Input
id="sync-server-url"
placeholder={t("sync.serverUrlPlaceholder")}
value={serverUrl}
onChange={(e) => setServerUrl(e.target.value)}
/>
</div>
<div className="space-y-2">
<Label htmlFor="sync-token">{t("sync.token")}</Label>
<div className="relative">
<Input
id="sync-token"
type={showToken ? "text" : "password"}
placeholder={t("sync.tokenPlaceholder")}
value={token}
onChange={(e) => setToken(e.target.value)}
className="pr-10"
/>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={() => setShowToken(!showToken)}
className="absolute right-3 top-1/2 p-1 rounded-sm transition-colors transform -translate-y-1/2 hover:bg-accent"
aria-label={showToken ? "Hide token" : "Show token"}
>
{showToken ? (
<LuEyeOff className="w-4 h-4 text-muted-foreground hover:text-foreground" />
) : (
<LuEye className="w-4 h-4 text-muted-foreground hover:text-foreground" />
)}
</button>
</TooltipTrigger>
<TooltipContent>
{showToken ? "Hide token" : "Show token"}
</TooltipContent>
</Tooltip>
</div>
</div>
{isConnected && (
<div className="flex gap-2 items-center text-sm text-muted-foreground">
<div className="w-2 h-2 rounded-full bg-green-500" />
{t("sync.status.connected")}
</div>
)}
</div>
)}
<DialogFooter className="flex gap-2">
{isConnected && (
<Button
variant="outline"
onClick={handleDisconnect}
disabled={isSaving}
>
Disconnect
</Button>
)}
<Button
variant="outline"
onClick={handleTestConnection}
disabled={isTesting || !serverUrl}
>
{isTesting ? "Testing..." : "Test Connection"}
</Button>
<LoadingButton
onClick={handleSave}
isLoading={isSaving}
disabled={!serverUrl || !token}
>
Save
</LoadingButton>
</DialogFooter>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
+22 -2
View File
@@ -1,6 +1,7 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { LuLock } from "react-icons/lu";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Checkbox } from "@/components/ui/checkbox";
@@ -28,6 +29,7 @@ interface WayfernConfigFormProps {
isCreating?: boolean;
forceAdvanced?: boolean;
readOnly?: boolean;
crossOsUnlocked?: boolean;
}
const isFingerprintEditingDisabled = (config: WayfernConfig): boolean => {
@@ -57,7 +59,9 @@ export function WayfernConfigForm({
isCreating = false,
forceAdvanced = false,
readOnly = false,
crossOsUnlocked = false,
}: WayfernConfigFormProps) {
const { t } = useTranslation();
const [activeTab, setActiveTab] = useState(
forceAdvanced ? "manual" : "automatic",
);
@@ -157,7 +161,7 @@ export function WayfernConfigForm({
{(
["windows", "macos", "linux", "android", "ios"] as WayfernOS[]
).map((os) => {
const isDisabled = os !== currentOS;
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
@@ -171,6 +175,13 @@ export function WayfernConfigForm({
})}
</SelectContent>
</Select>
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
{t("fingerprint.crossOsWarning")}
</AlertDescription>
</Alert>
)}
</div>
{/* Randomize Fingerprint Option */}
@@ -943,7 +954,7 @@ export function WayfernConfigForm({
"ios",
] as WayfernOS[]
).map((os) => {
const isDisabled = os !== currentOS;
const isDisabled = os !== currentOS && !crossOsUnlocked;
return (
<SelectItem key={os} value={os} disabled={isDisabled}>
<span className="flex items-center gap-2">
@@ -957,6 +968,15 @@ export function WayfernConfigForm({
})}
</SelectContent>
</Select>
{selectedOS !== currentOS && crossOsUnlocked && (
<Alert className="mt-2">
<AlertDescription>
Cross-OS fingerprinting has limitations. System-level APIs
may still reflect your actual operating system, and some
features may have degraded performance.
</AlertDescription>
</Alert>
)}
</div>
{/* Randomize Fingerprint Option */}
+98 -19
View File
@@ -3,17 +3,20 @@
import { getCurrentWindow } from "@tauri-apps/api/window";
import { useEffect, useState } from "react";
type Platform = "macos" | "windows" | "linux";
function detectPlatform(): Platform {
const userAgent = navigator.userAgent.toLowerCase();
if (userAgent.includes("mac")) return "macos";
if (userAgent.includes("win")) return "windows";
return "linux";
}
export function WindowDragArea() {
const [isMacOS, setIsMacOS] = useState(false);
const [platform, setPlatform] = useState<Platform | null>(null);
useEffect(() => {
// Check if we're on macOS using user agent detection
const checkPlatform = () => {
const userAgent = navigator.userAgent.toLowerCase();
setIsMacOS(userAgent.includes("mac"));
};
checkPlatform();
setPlatform(detectPlatform());
}, []);
const handlePointerDown = (e: React.PointerEvent) => {
@@ -33,21 +36,97 @@ export function WindowDragArea() {
void startDrag();
};
// Only render on macOS and when no dialogs are open
if (!isMacOS) {
// Linux: system decorations handle everything
if (!platform || platform === "linux") {
return null;
}
// macOS: transparent drag area overlay
if (platform === "macos") {
return (
<button
type="button"
className="fixed top-0 right-0 left-0 h-10 bg-transparent border-0 z-[999999] select-none"
data-window-drag-area="true"
onPointerDown={handlePointerDown}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
}}
/>
);
}
// Windows: custom title bar with drag area + minimize/close buttons
const handleMinimize = async () => {
try {
await getCurrentWindow().minimize();
} catch (error) {
console.error("Failed to minimize window:", error);
}
};
const handleClose = async () => {
try {
await getCurrentWindow().close();
} catch (error) {
console.error("Failed to close window:", error);
}
};
return (
<button
type="button"
className="fixed top-0 right-0 left-0 h-10 bg-transparent border-0 z-[999999] select-none"
<div
className="fixed top-0 right-0 left-0 h-10 z-[999999] flex items-center select-none"
data-window-drag-area="true"
onPointerDown={handlePointerDown}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
}}
/>
>
{/* Draggable area */}
<button
type="button"
className="flex-1 h-full bg-transparent border-0 cursor-default"
onPointerDown={handlePointerDown}
onContextMenu={(e) => {
e.preventDefault();
e.stopPropagation();
}}
/>
{/* Window control buttons */}
<div className="flex items-center h-full">
<button
type="button"
onClick={handleMinimize}
className="flex items-center justify-center w-12 h-full hover:bg-muted/50 transition-colors text-muted-foreground hover:text-foreground"
>
<svg
width="10"
height="1"
viewBox="0 0 10 1"
fill="currentColor"
role="img"
aria-label="Minimize"
>
<rect width="10" height="1" />
</svg>
</button>
<button
type="button"
onClick={handleClose}
className="flex items-center justify-center w-12 h-full hover:bg-destructive/90 transition-colors text-muted-foreground hover:text-destructive-foreground"
>
<svg
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
stroke="currentColor"
strokeWidth="1.2"
role="img"
aria-label="Close"
>
<line x1="1" y1="1" x2="9" y2="9" />
<line x1="9" y1="1" x2="1" y2="9" />
</svg>
</button>
</div>
</div>
);
}
+86
View File
@@ -0,0 +1,86 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import type { CloudAuthState, CloudUser } from "@/types";
interface UseCloudAuthReturn {
user: CloudUser | null;
isLoggedIn: boolean;
isLoading: boolean;
requestOtp: (email: string) => Promise<string>;
verifyOtp: (email: string, code: string) => Promise<CloudAuthState>;
logout: () => Promise<void>;
refreshProfile: () => Promise<CloudUser>;
}
export function useCloudAuth(): UseCloudAuthReturn {
const [authState, setAuthState] = useState<CloudAuthState | null>(null);
const [isLoading, setIsLoading] = useState(true);
const loadUser = useCallback(async () => {
try {
const state = await invoke<CloudAuthState | null>("cloud_get_user");
setAuthState(state);
} catch (error) {
console.error("Failed to load cloud auth state:", error);
setAuthState(null);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
loadUser();
const unlistenPromise = listen("cloud-auth-expired", () => {
setAuthState(null);
});
return () => {
void unlistenPromise.then((unlisten) => {
unlisten();
});
};
}, [loadUser]);
const requestOtp = useCallback(async (email: string): Promise<string> => {
return invoke<string>("cloud_request_otp", { email });
}, []);
const verifyOtp = useCallback(
async (email: string, code: string): Promise<CloudAuthState> => {
const state = await invoke<CloudAuthState>("cloud_verify_otp", {
email,
code,
});
setAuthState(state);
return state;
},
[],
);
const logout = useCallback(async () => {
await invoke("cloud_logout");
setAuthState(null);
}, []);
const refreshProfile = useCallback(async (): Promise<CloudUser> => {
const user = await invoke<CloudUser>("cloud_refresh_profile");
setAuthState((prev) =>
prev
? { ...prev, user }
: { user, logged_in_at: new Date().toISOString() },
);
return user;
}, []);
return {
user: authState?.user ?? null,
isLoggedIn: authState !== null,
isLoading,
requestOtp,
verifyOtp,
logout,
refreshProfile,
};
}
+25 -1
View File
@@ -274,7 +274,28 @@
"syncing": "Syncing...",
"error": "Sync Error"
},
"description": "Connect to a sync server to synchronize your profiles, proxies, and groups across devices."
"description": "Connect to a sync server to synchronize your profiles, proxies, and groups across devices.",
"cloud": {
"tabLabel": "Cloud",
"selfHostedTabLabel": "Self-Hosted",
"email": "Email",
"emailPlaceholder": "you@example.com",
"sendCode": "Send Code",
"codeSent": "Code sent! Check your email.",
"verificationCode": "Verification Code",
"codePlaceholder": "Enter 6-digit code",
"verifyAndLogin": "Verify & Log In",
"loggingIn": "Logging in...",
"connected": "Connected",
"plan": "Plan",
"profiles": "Profiles",
"profileUsage": "{{used}} / {{limit}}",
"manageAccount": "Manage Account",
"logout": "Log Out",
"logoutConfirm": "Are you sure you want to log out? Cloud sync will stop.",
"loginSuccess": "Successfully logged in!",
"logoutSuccess": "Successfully logged out."
}
},
"integrations": {
"title": "Integrations",
@@ -457,5 +478,8 @@
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "Spoofing a different operating system is harder — system-level APIs are more difficult to mask, making it easier for websites to detect inconsistencies. No anti-detect browser can perfectly spoof every detail across operating systems."
}
}
+25 -1
View File
@@ -274,7 +274,28 @@
"syncing": "Sincronizando...",
"error": "Error de Sincronización"
},
"description": "Conéctate a un servidor de sincronización para sincronizar tus perfiles, proxies y grupos entre dispositivos."
"description": "Conéctate a un servidor de sincronización para sincronizar tus perfiles, proxies y grupos entre dispositivos.",
"cloud": {
"tabLabel": "Nube",
"selfHostedTabLabel": "Autoalojado",
"email": "Correo electrónico",
"emailPlaceholder": "tu@ejemplo.com",
"sendCode": "Enviar Código",
"codeSent": "¡Código enviado! Revisa tu correo electrónico.",
"verificationCode": "Código de Verificación",
"codePlaceholder": "Ingresa el código de 6 dígitos",
"verifyAndLogin": "Verificar e Iniciar Sesión",
"loggingIn": "Iniciando sesión...",
"connected": "Conectado",
"plan": "Plan",
"profiles": "Perfiles",
"profileUsage": "{{used}} / {{limit}}",
"manageAccount": "Administrar Cuenta",
"logout": "Cerrar Sesión",
"logoutConfirm": "¿Estás seguro de que deseas cerrar sesión? La sincronización en la nube se detendrá.",
"loginSuccess": "¡Sesión iniciada exitosamente!",
"logoutSuccess": "Sesión cerrada exitosamente."
}
},
"integrations": {
"title": "Integraciones",
@@ -457,5 +478,8 @@
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "Suplantar un sistema operativo diferente es más difícil: las API a nivel de sistema son más difíciles de enmascarar, lo que facilita que los sitios web detecten inconsistencias. Ningún navegador antidetección puede suplantar perfectamente cada detalle entre sistemas operativos."
}
}
+25 -1
View File
@@ -274,7 +274,28 @@
"syncing": "Synchronisation...",
"error": "Erreur de synchronisation"
},
"description": "Connectez-vous à un serveur de synchronisation pour synchroniser vos profils, proxies et groupes entre appareils."
"description": "Connectez-vous à un serveur de synchronisation pour synchroniser vos profils, proxies et groupes entre appareils.",
"cloud": {
"tabLabel": "Cloud",
"selfHostedTabLabel": "Auto-hébergé",
"email": "E-mail",
"emailPlaceholder": "vous@exemple.com",
"sendCode": "Envoyer le Code",
"codeSent": "Code envoyé ! Vérifiez votre e-mail.",
"verificationCode": "Code de Vérification",
"codePlaceholder": "Entrez le code à 6 chiffres",
"verifyAndLogin": "Vérifier et Se Connecter",
"loggingIn": "Connexion en cours...",
"connected": "Connecté",
"plan": "Forfait",
"profiles": "Profils",
"profileUsage": "{{used}} / {{limit}}",
"manageAccount": "Gérer le Compte",
"logout": "Se Déconnecter",
"logoutConfirm": "Êtes-vous sûr de vouloir vous déconnecter ? La synchronisation cloud sera arrêtée.",
"loginSuccess": "Connexion réussie !",
"logoutSuccess": "Déconnexion réussie."
}
},
"integrations": {
"title": "Intégrations",
@@ -457,5 +478,8 @@
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "Usurper un système d'exploitation différent est plus difficile : les API au niveau du système sont plus difficiles à masquer, ce qui permet aux sites web de détecter plus facilement les incohérences. Aucun navigateur anti-détection ne peut parfaitement usurper chaque détail d'un système d'exploitation à l'autre."
}
}
+25 -1
View File
@@ -274,7 +274,28 @@
"syncing": "同期中...",
"error": "同期エラー"
},
"description": "同期サーバーに接続して、デバイス間でプロファイル、プロキシ、グループを同期します。"
"description": "同期サーバーに接続して、デバイス間でプロファイル、プロキシ、グループを同期します。",
"cloud": {
"tabLabel": "クラウド",
"selfHostedTabLabel": "セルフホスト",
"email": "メールアドレス",
"emailPlaceholder": "you@example.com",
"sendCode": "コードを送信",
"codeSent": "コードを送信しました!メールを確認してください。",
"verificationCode": "認証コード",
"codePlaceholder": "6桁のコードを入力",
"verifyAndLogin": "認証してログイン",
"loggingIn": "ログイン中...",
"connected": "接続済み",
"plan": "プラン",
"profiles": "プロファイル",
"profileUsage": "{{used}} / {{limit}}",
"manageAccount": "アカウント管理",
"logout": "ログアウト",
"logoutConfirm": "ログアウトしてもよろしいですか?クラウド同期が停止します。",
"loginSuccess": "ログインに成功しました!",
"logoutSuccess": "ログアウトしました。"
}
},
"integrations": {
"title": "統合",
@@ -457,5 +478,8 @@
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "異なるオペレーティングシステムの偽装はより困難です。システムレベルのAPIはマスクしにくく、ウェブサイトが矛盾を検出しやすくなります。どのアンチディテクトブラウザも、異なるOS間のすべての詳細を完璧に偽装することはできません。"
}
}
+25 -1
View File
@@ -274,7 +274,28 @@
"syncing": "Sincronizando...",
"error": "Erro de Sincronização"
},
"description": "Conecte-se a um servidor de sincronização para sincronizar seus perfis, proxies e grupos entre dispositivos."
"description": "Conecte-se a um servidor de sincronização para sincronizar seus perfis, proxies e grupos entre dispositivos.",
"cloud": {
"tabLabel": "Nuvem",
"selfHostedTabLabel": "Auto-hospedado",
"email": "E-mail",
"emailPlaceholder": "voce@exemplo.com",
"sendCode": "Enviar Código",
"codeSent": "Código enviado! Verifique seu e-mail.",
"verificationCode": "Código de Verificação",
"codePlaceholder": "Digite o código de 6 dígitos",
"verifyAndLogin": "Verificar e Entrar",
"loggingIn": "Entrando...",
"connected": "Conectado",
"plan": "Plano",
"profiles": "Perfis",
"profileUsage": "{{used}} / {{limit}}",
"manageAccount": "Gerenciar Conta",
"logout": "Sair",
"logoutConfirm": "Tem certeza de que deseja sair? A sincronização na nuvem será interrompida.",
"loginSuccess": "Login realizado com sucesso!",
"logoutSuccess": "Logout realizado com sucesso."
}
},
"integrations": {
"title": "Integrações",
@@ -457,5 +478,8 @@
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "Falsificar um sistema operacional diferente é mais difícil: as APIs de nível de sistema são mais difíceis de mascarar, facilitando a detecção de inconsistências pelos sites. Nenhum navegador antidetecção consegue falsificar perfeitamente todos os detalhes entre sistemas operacionais."
}
}
+25 -1
View File
@@ -274,7 +274,28 @@
"syncing": "Синхронизация...",
"error": "Ошибка синхронизации"
},
"description": "Подключитесь к серверу синхронизации для синхронизации профилей, прокси и групп между устройствами."
"description": "Подключитесь к серверу синхронизации для синхронизации профилей, прокси и групп между устройствами.",
"cloud": {
"tabLabel": "Облако",
"selfHostedTabLabel": "Свой сервер",
"email": "Электронная почта",
"emailPlaceholder": "вы@пример.com",
"sendCode": "Отправить код",
"codeSent": "Код отправлен! Проверьте вашу почту.",
"verificationCode": "Код подтверждения",
"codePlaceholder": "Введите 6-значный код",
"verifyAndLogin": "Подтвердить и Войти",
"loggingIn": "Вход...",
"connected": "Подключено",
"plan": "Тариф",
"profiles": "Профили",
"profileUsage": "{{used}} / {{limit}}",
"manageAccount": "Управление аккаунтом",
"logout": "Выйти",
"logoutConfirm": "Вы уверены, что хотите выйти? Облачная синхронизация будет остановлена.",
"loginSuccess": "Вход выполнен успешно!",
"logoutSuccess": "Выход выполнен успешно."
}
},
"integrations": {
"title": "Интеграции",
@@ -457,5 +478,8 @@
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "Подмена другой операционной системы сложнее — системные API труднее замаскировать, что упрощает обнаружение несоответствий веб-сайтами. Ни один антидетект-браузер не может идеально подменить все детали при смене операционной системы."
}
}
+25 -1
View File
@@ -274,7 +274,28 @@
"syncing": "同步中...",
"error": "同步错误"
},
"description": "连接到同步服务器以在设备之间同步您的配置文件、代理和分组。"
"description": "连接到同步服务器以在设备之间同步您的配置文件、代理和分组。",
"cloud": {
"tabLabel": "云端",
"selfHostedTabLabel": "自托管",
"email": "电子邮件",
"emailPlaceholder": "you@example.com",
"sendCode": "发送验证码",
"codeSent": "验证码已发送!请检查您的邮箱。",
"verificationCode": "验证码",
"codePlaceholder": "输入6位验证码",
"verifyAndLogin": "验证并登录",
"loggingIn": "登录中...",
"connected": "已连接",
"plan": "套餐",
"profiles": "配置文件",
"profileUsage": "{{used}} / {{limit}}",
"manageAccount": "管理账户",
"logout": "退出登录",
"logoutConfirm": "您确定要退出登录吗?云同步将会停止。",
"loginSuccess": "登录成功!",
"logoutSuccess": "已成功退出登录。"
}
},
"integrations": {
"title": "集成",
@@ -457,5 +478,8 @@
"zen": "Zen Browser",
"camoufox": "Camoufox",
"wayfern": "Wayfern"
},
"fingerprint": {
"crossOsWarning": "伪装不同的操作系统更加困难——系统级API更难以掩盖,使网站更容易检测到不一致之处。没有任何反检测浏览器能够完美伪装跨操作系统的所有细节。"
}
}
+17
View File
@@ -36,6 +36,23 @@ export interface SyncSettings {
sync_token?: string;
}
export interface CloudUser {
id: string;
email: string;
plan: string;
planPeriod: string;
subscriptionStatus: string;
profileLimit: number;
cloudProfilesUsed: number;
proxyBandwidthLimitMb: number;
proxyBandwidthUsedMb: number;
}
export interface CloudAuthState {
user: CloudUser;
logged_in_at: string;
}
export interface ProfileSyncStatusEvent {
profile_id: string;
status: "disabled" | "syncing" | "synced" | "error" | "pending";