From 2725cf93167bb5c34f08a0a09a233570e71c016e Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sun, 11 Jan 2026 03:58:46 +0400 Subject: [PATCH] feat: add licensing handling --- src-tauri/src/commercial_license.rs | 135 +++++++++++++ src-tauri/src/settings_manager.rs | 12 ++ src-tauri/src/wayfern_terms.rs | 228 ++++++++++++++++++++++ src/app/page.tsx | 33 ++++ src/components/commercial-trial-modal.tsx | 82 ++++++++ src/components/settings-dialog.tsx | 112 +++++++++++ src/components/wayfern-terms-dialog.tsx | 85 ++++++++ src/hooks/use-commercial-trial.ts | 59 ++++++ src/hooks/use-wayfern-terms.ts | 35 ++++ 9 files changed, 781 insertions(+) create mode 100644 src-tauri/src/commercial_license.rs create mode 100644 src-tauri/src/wayfern_terms.rs create mode 100644 src/components/commercial-trial-modal.tsx create mode 100644 src/components/wayfern-terms-dialog.tsx create mode 100644 src/hooks/use-commercial-trial.ts create mode 100644 src/hooks/use-wayfern-terms.ts diff --git a/src-tauri/src/commercial_license.rs b/src-tauri/src/commercial_license.rs new file mode 100644 index 0000000..f0ba5ab --- /dev/null +++ b/src-tauri/src/commercial_license.rs @@ -0,0 +1,135 @@ +use serde::{Deserialize, Serialize}; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::{AppHandle, Emitter}; + +use crate::settings_manager::SettingsManager; + +const TRIAL_DURATION_SECONDS: u64 = 14 * 24 * 60 * 60; // 2 weeks + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum TrialStatus { + Active { + remaining_seconds: u64, + days_remaining: u64, + hours_remaining: u64, + minutes_remaining: u64, + }, + Expired, +} + +pub struct CommercialLicenseManager; + +impl CommercialLicenseManager { + pub fn instance() -> &'static CommercialLicenseManager { + &COMMERCIAL_LICENSE_MANAGER + } + + fn get_current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("System time before UNIX epoch") + .as_secs() + } + + pub async fn get_trial_status(&self, app_handle: &AppHandle) -> Result { + let first_launch = self.get_or_set_first_launch(app_handle).await?; + let now = Self::get_current_timestamp(); + + if now < first_launch { + // Clock was set back, treat as expired + return Ok(TrialStatus::Expired); + } + + let elapsed = now - first_launch; + + if elapsed >= TRIAL_DURATION_SECONDS { + Ok(TrialStatus::Expired) + } else { + let remaining = TRIAL_DURATION_SECONDS - elapsed; + let days = remaining / (24 * 60 * 60); + let hours = (remaining % (24 * 60 * 60)) / (60 * 60); + let minutes = (remaining % (60 * 60)) / 60; + + Ok(TrialStatus::Active { + remaining_seconds: remaining, + days_remaining: days, + hours_remaining: hours, + minutes_remaining: minutes, + }) + } + } + + async fn get_or_set_first_launch(&self, app_handle: &AppHandle) -> Result { + let settings_manager = SettingsManager::instance(); + let mut settings = settings_manager + .load_settings() + .map_err(|e| format!("Failed to load settings: {e}"))?; + + if let Some(timestamp) = settings.first_launch_timestamp { + return Ok(timestamp); + } + + // First launch - record the timestamp + let now = Self::get_current_timestamp(); + settings.first_launch_timestamp = Some(now); + settings_manager + .save_settings(&settings) + .map_err(|e| format!("Failed to save settings: {e}"))?; + + log::info!("First launch timestamp recorded: {now}"); + + // Emit event to notify frontend + if let Err(e) = app_handle.emit("first-launch-recorded", now) { + log::warn!("Failed to emit first-launch-recorded event: {e}"); + } + + Ok(now) + } + + pub async fn acknowledge_expiration(&self, _app_handle: &AppHandle) -> Result<(), String> { + let settings_manager = SettingsManager::instance(); + let mut settings = settings_manager + .load_settings() + .map_err(|e| format!("Failed to load settings: {e}"))?; + + settings.commercial_trial_acknowledged = true; + settings_manager + .save_settings(&settings) + .map_err(|e| format!("Failed to save settings: {e}"))?; + + log::info!("Commercial trial expiration acknowledged"); + Ok(()) + } + + pub fn has_acknowledged(&self, _app_handle: &AppHandle) -> Result { + let settings_manager = SettingsManager::instance(); + let settings = settings_manager + .load_settings() + .map_err(|e| format!("Failed to load settings: {e}"))?; + + Ok(settings.commercial_trial_acknowledged) + } +} + +lazy_static::lazy_static! { + static ref COMMERCIAL_LICENSE_MANAGER: CommercialLicenseManager = CommercialLicenseManager; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_trial_duration() { + // 2 weeks = 14 * 24 * 60 * 60 = 1,209,600 seconds + assert_eq!(TRIAL_DURATION_SECONDS, 1_209_600); + } + + #[test] + fn test_current_timestamp() { + let timestamp = CommercialLicenseManager::get_current_timestamp(); + // Timestamp should be after 2020-01-01 (1577836800) + assert!(timestamp > 1577836800); + } +} diff --git a/src-tauri/src/settings_manager.rs b/src-tauri/src/settings_manager.rs index 20f0f21..0baa16b 100644 --- a/src-tauri/src/settings_manager.rs +++ b/src-tauri/src/settings_manager.rs @@ -40,6 +40,12 @@ pub struct AppSettings { pub api_token: Option, // Displayed token for user to copy #[serde(default)] pub sync_server_url: Option, // URL of the sync server + #[serde(default)] + pub first_launch_timestamp: Option, // Unix epoch seconds when app was first launched + #[serde(default)] + pub commercial_trial_acknowledged: bool, // Has user dismissed the trial expiration modal + #[serde(default)] + pub mcp_enabled: bool, // Enable MCP (Model Context Protocol) server } #[derive(Debug, Serialize, Deserialize, Clone, Default)] @@ -66,6 +72,9 @@ impl Default for AppSettings { api_port: 10108, api_token: None, sync_server_url: None, + first_launch_timestamp: None, + commercial_trial_acknowledged: false, + mcp_enabled: false, } } } @@ -753,6 +762,9 @@ mod tests { api_port: 10108, api_token: None, sync_server_url: None, + first_launch_timestamp: None, + commercial_trial_acknowledged: false, + mcp_enabled: false, }; // Save settings diff --git a/src-tauri/src/wayfern_terms.rs b/src-tauri/src/wayfern_terms.rs new file mode 100644 index 0000000..4af1440 --- /dev/null +++ b/src-tauri/src/wayfern_terms.rs @@ -0,0 +1,228 @@ +use directories::BaseDirs; +use std::path::PathBuf; +use std::process::Stdio; +use tokio::process::Command as TokioCommand; + +use crate::browser::{create_browser, BrowserType}; +use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry; +use crate::profile::ProfileManager; + +const ACCEPT_TERMS_FLAG: &str = "--accept-terms-and-conditions"; +const MIN_VALID_TIMESTAMP: i64 = 1577836800; // 2020-01-01 00:00:00 UTC + +pub struct WayfernTermsManager { + base_dirs: BaseDirs, +} + +impl WayfernTermsManager { + fn new() -> Self { + Self { + base_dirs: BaseDirs::new().expect("Failed to get base directories"), + } + } + + pub fn instance() -> &'static WayfernTermsManager { + &WAYFERN_TERMS_MANAGER + } + + fn get_license_file_path(&self) -> PathBuf { + #[cfg(target_os = "windows")] + { + // Windows: %APPDATA%\Wayfern\license-accepted + if let Some(app_data) = std::env::var_os("APPDATA") { + return PathBuf::from(app_data) + .join("Wayfern") + .join("license-accepted"); + } + // Fallback to home directory + self + .base_dirs + .home_dir() + .join("AppData") + .join("Roaming") + .join("Wayfern") + .join("license-accepted") + } + + #[cfg(target_os = "macos")] + { + // macOS: ~/Library/Application Support/Wayfern/license-accepted + self + .base_dirs + .home_dir() + .join("Library") + .join("Application Support") + .join("Wayfern") + .join("license-accepted") + } + + #[cfg(target_os = "linux")] + { + // Linux: ~/.config/Wayfern/license-accepted or $XDG_CONFIG_HOME/Wayfern/license-accepted + if let Some(xdg_config) = std::env::var_os("XDG_CONFIG_HOME") { + let xdg_path = PathBuf::from(xdg_config); + if !xdg_path.as_os_str().is_empty() { + return xdg_path.join("Wayfern").join("license-accepted"); + } + } + self + .base_dirs + .home_dir() + .join(".config") + .join("Wayfern") + .join("license-accepted") + } + } + + pub fn is_terms_accepted(&self) -> bool { + let license_file = self.get_license_file_path(); + + if !license_file.exists() { + return false; + } + + // Read the timestamp from the file + let contents = match std::fs::read_to_string(&license_file) { + Ok(c) => c, + Err(_) => return false, + }; + + // Parse timestamp (Wayfern stores Unix timestamp as text) + let timestamp: i64 = match contents.trim().parse() { + Ok(t) => t, + Err(_) => return false, + }; + + // Check that timestamp is positive and after 2020-01-01 + timestamp >= MIN_VALID_TIMESTAMP + } + + fn get_any_wayfern_executable(&self) -> Option { + // First try to get executable from any downloaded Wayfern version + let registry = DownloadedBrowsersRegistry::instance(); + let versions = registry.get_downloaded_versions("wayfern"); + + if versions.is_empty() { + return None; + } + + // Get first available version + let version = versions.first()?; + + // Get binaries directory + let binaries_dir = ProfileManager::instance().get_binaries_dir(); + let mut browser_dir = binaries_dir; + browser_dir.push("wayfern"); + browser_dir.push(version); + + let browser = create_browser(BrowserType::Wayfern); + browser.get_executable_path(&browser_dir).ok() + } + + pub async fn accept_terms(&self) -> Result<(), String> { + let executable_path = self.get_any_wayfern_executable().ok_or_else(|| { + "No Wayfern browser downloaded. Please download a Wayfern browser version first.".to_string() + })?; + + log::info!( + "Running Wayfern with {} flag: {:?}", + ACCEPT_TERMS_FLAG, + executable_path + ); + + #[cfg(target_os = "macos")] + { + // On macOS, if it's an app bundle, we need to find the actual executable + let executable_str = executable_path.to_string_lossy(); + if executable_str.ends_with(".app") { + // Navigate to Contents/MacOS and find the executable + let macos_dir = executable_path.join("Contents").join("MacOS"); + if let Ok(entries) = std::fs::read_dir(&macos_dir) { + for entry in entries.flatten() { + let path = entry.path(); + if path.is_file() { + return self.run_accept_command(&path).await; + } + } + } + return Err("Could not find executable in Wayfern app bundle".to_string()); + } + } + + self.run_accept_command(&executable_path).await + } + + async fn run_accept_command(&self, executable_path: &PathBuf) -> Result<(), String> { + let output = TokioCommand::new(executable_path) + .arg(ACCEPT_TERMS_FLAG) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .map_err(|e| format!("Failed to run Wayfern: {e}"))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + log::error!("Wayfern terms acceptance failed: {stderr}"); + return Err(format!( + "Wayfern terms acceptance failed with exit code: {:?}", + output.status.code() + )); + } + + // Verify the license file was created + if !self.is_terms_accepted() { + return Err( + "Terms acceptance command succeeded but license file was not created".to_string(), + ); + } + + log::info!("Wayfern terms and conditions accepted successfully"); + Ok(()) + } +} + +lazy_static::lazy_static! { + static ref WAYFERN_TERMS_MANAGER: WayfernTermsManager = WayfernTermsManager::new(); +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_license_file_path() { + let manager = WayfernTermsManager::new(); + let path = manager.get_license_file_path(); + let path_str = path.to_string_lossy(); + + assert!( + path_str.contains("Wayfern"), + "License file path should contain Wayfern" + ); + assert!( + path_str.ends_with("license-accepted"), + "License file should be named license-accepted" + ); + + #[cfg(target_os = "macos")] + assert!( + path_str.contains("Application Support"), + "macOS path should contain Application Support" + ); + + #[cfg(target_os = "linux")] + assert!( + path_str.contains(".config") || std::env::var_os("XDG_CONFIG_HOME").is_some(), + "Linux path should be in .config or XDG_CONFIG_HOME" + ); + } + + #[test] + fn test_is_terms_accepted_no_file() { + let manager = WayfernTermsManager::new(); + // This test will pass if no license file exists (which is typically the case in test env) + // The actual behavior depends on whether the file exists + let _ = manager.is_terms_accepted(); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index f8bf47d..7308d73 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -5,6 +5,7 @@ import { listen } from "@tauri-apps/api/event"; import { getCurrent } from "@tauri-apps/plugin-deep-link"; import { useCallback, useEffect, useMemo, useState } from "react"; import { CamoufoxConfigDialog } from "@/components/camoufox-config-dialog"; +import { CommercialTrialModal } from "@/components/commercial-trial-modal"; import { CookieCopyDialog } from "@/components/cookie-copy-dialog"; import { CreateProfileDialog } from "@/components/create-profile-dialog"; import { DeleteConfirmationDialog } from "@/components/delete-confirmation-dialog"; @@ -21,7 +22,9 @@ import { ProxyAssignmentDialog } from "@/components/proxy-assignment-dialog"; import { ProxyManagementDialog } from "@/components/proxy-management-dialog"; 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 { useCommercialTrial } from "@/hooks/use-commercial-trial"; import { useGroupEvents } from "@/hooks/use-group-events"; import type { PermissionType } from "@/hooks/use-permissions"; import { usePermissions } from "@/hooks/use-permissions"; @@ -29,6 +32,7 @@ import { useProfileEvents } from "@/hooks/use-profile-events"; import { useProxyEvents } from "@/hooks/use-proxy-events"; import { useUpdateNotifications } from "@/hooks/use-update-notifications"; import { useVersionUpdater } from "@/hooks/use-version-updater"; +import { useWayfernTerms } from "@/hooks/use-wayfern-terms"; import { showErrorToast, showSuccessToast, showToast } from "@/lib/toast-utils"; import type { BrowserProfile, CamoufoxConfig, WayfernConfig } from "@/types"; @@ -70,6 +74,18 @@ export default function Home() { error: proxiesError, } = useProxyEvents(); + // Wayfern terms and commercial trial hooks + const { + termsAccepted, + isLoading: termsLoading, + checkTerms, + } = useWayfernTerms(); + const { + trialStatus, + hasAcknowledged: trialAcknowledged, + checkTrialStatus, + } = useCommercialTrial(); + const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false); const [settingsDialogOpen, setSettingsDialogOpen] = useState(false); const [importProfileDialogOpen, setImportProfileDialogOpen] = useState(false); @@ -961,6 +977,23 @@ export default function Home() { profile={currentProfileForSync} onSyncConfigOpen={() => setSyncConfigDialogOpen(true)} /> + + {/* Wayfern Terms and Conditions Dialog - shown if terms not accepted */} + + + {/* Commercial Trial Modal - shown once when trial expires */} + ); } diff --git a/src/components/commercial-trial-modal.tsx b/src/components/commercial-trial-modal.tsx new file mode 100644 index 0000000..f2a3920 --- /dev/null +++ b/src/components/commercial-trial-modal.tsx @@ -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 ( + + e.preventDefault()} + onPointerDownOutside={(e) => e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + > + + Commercial Trial Expired + + Your 2-week commercial trial period has ended. + + + +
+

+ If you are using Donut Browser for business purposes, you need to + purchase a commercial license to continue. +

+

+ Personal use remains free and unrestricted. +

+

+ Visit our website to learn more about commercial licensing options. +

+
+ + + + I Understand + + +
+
+ ); +} diff --git a/src/components/settings-dialog.tsx b/src/components/settings-dialog.tsx index 2468955..439597b 100644 --- a/src/components/settings-dialog.tsx +++ b/src/components/settings-dialog.tsx @@ -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("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) { )} + {/* Commercial License Section */} +
+ + +
+ {trialStatus?.type === "Active" ? ( +
+

+ Trial: {trialStatus.days_remaining} days,{" "} + {trialStatus.hours_remaining} hours remaining +

+

+ Commercial use is free during the trial period +

+
+ ) : ( +
+

+ Trial expired +

+

+ Personal use remains free. Commercial use requires a + license. +

+
+ )} +
+
+ + {/* MCP Server Section */} +
+ + +
+ { + 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); + } + }} + /> +
+ +

+ Allow AI assistants to control Wayfern and Camoufox browsers + via MCP. + {!termsAccepted && ( + + (Accept terms first) + + )} +

+
+
+ + {mcpEnabled && ( +
+
Available MCP Tools
+
    +
  • list_profiles - List Wayfern/Camoufox profiles
  • +
  • run_profile - Launch a browser profile
  • +
  • kill_profile - Stop a running browser
  • +
  • get_profile - Get profile details
  • +
  • list_proxies - List configured proxies
  • +
+
+ )} +
+ {/* Advanced Section */}
diff --git a/src/components/wayfern-terms-dialog.tsx b/src/components/wayfern-terms-dialog.tsx new file mode 100644 index 0000000..16f965d --- /dev/null +++ b/src/components/wayfern-terms-dialog.tsx @@ -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 ( + + e.preventDefault()} + onPointerDownOutside={(e) => e.preventDefault()} + onInteractOutside={(e) => e.preventDefault()} + > + + Wayfern Terms and Conditions + + Before using Donut Browser, you must read and agree to Wayfern's + Terms and Conditions. + + + +
+

+ Please review the Terms and Conditions at: +

+ + https://wayfern.com/terms-and-conditions + +

+ By clicking "I Accept", you agree to be bound by these terms. +

+
+ + + + I Accept + + +
+
+ ); +} diff --git a/src/hooks/use-commercial-trial.ts b/src/hooks/use-commercial-trial.ts new file mode 100644 index 0000000..e58973f --- /dev/null +++ b/src/hooks/use-commercial-trial.ts @@ -0,0 +1,59 @@ +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; + +export interface TrialStatusActive { + type: "Active"; + remaining_seconds: number; + days_remaining: number; + hours_remaining: number; + minutes_remaining: number; +} + +export interface TrialStatusExpired { + type: "Expired"; +} + +export type TrialStatus = TrialStatusActive | TrialStatusExpired; + +interface UseCommercialTrialReturn { + trialStatus: TrialStatus | null; + hasAcknowledged: boolean; + isLoading: boolean; + checkTrialStatus: () => Promise; +} + +export function useCommercialTrial(): UseCommercialTrialReturn { + const [trialStatus, setTrialStatus] = useState(null); + const [hasAcknowledged, setHasAcknowledged] = useState(true); + const [isLoading, setIsLoading] = useState(true); + + const checkTrialStatus = useCallback(async () => { + try { + const [status, acknowledged] = await Promise.all([ + invoke("get_commercial_trial_status"), + invoke("has_acknowledged_trial_expiration"), + ]); + setTrialStatus(status); + setHasAcknowledged(acknowledged); + } catch (error) { + console.error("Failed to check trial status:", error); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + checkTrialStatus(); + + // Check trial status every minute to update the countdown + const interval = setInterval(checkTrialStatus, 60000); + return () => clearInterval(interval); + }, [checkTrialStatus]); + + return { + trialStatus, + hasAcknowledged, + isLoading, + checkTrialStatus, + }; +} diff --git a/src/hooks/use-wayfern-terms.ts b/src/hooks/use-wayfern-terms.ts new file mode 100644 index 0000000..f829346 --- /dev/null +++ b/src/hooks/use-wayfern-terms.ts @@ -0,0 +1,35 @@ +import { invoke } from "@tauri-apps/api/core"; +import { useCallback, useEffect, useState } from "react"; + +interface UseWayfernTermsReturn { + termsAccepted: boolean | null; + isLoading: boolean; + checkTerms: () => Promise; +} + +export function useWayfernTerms(): UseWayfernTermsReturn { + const [termsAccepted, setTermsAccepted] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const checkTerms = useCallback(async () => { + try { + const accepted = await invoke("check_wayfern_terms_accepted"); + setTermsAccepted(accepted); + } catch (error) { + console.error("Failed to check terms acceptance:", error); + setTermsAccepted(false); + } finally { + setIsLoading(false); + } + }, []); + + useEffect(() => { + checkTerms(); + }, [checkTerms]); + + return { + termsAccepted, + isLoading, + checkTerms, + }; +}