feat(desktop-wallet): add system tray and auto-updater
- Add system tray with menu: Show, Hide, Lock, Check Updates, Quit - Integrate tauri-plugin-updater for seamless auto-updates - Add UpdateBanner component for update notifications - Add useAutoUpdater hook for update state management - Add useTrayEvents hook for tray event handling - Add Updates section to Settings page for manual update checks - Configure updater endpoints in tauri.conf.json - Exclude desktop-wallet from Cargo workspace (uses own Tauri deps)
This commit is contained in:
parent
a6233f285d
commit
ce5c996b35
10 changed files with 614 additions and 4 deletions
|
|
@ -25,6 +25,7 @@ exclude = [
|
|||
"contracts/dex",
|
||||
"contracts/staking",
|
||||
"crates/synor-crypto-wasm",
|
||||
"apps/desktop-wallet/src-tauri",
|
||||
]
|
||||
|
||||
# WASM modules are not part of workspace as they target wasm32
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@
|
|||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.0.0",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"@tauri-apps/plugin-notification": "^2.0.0",
|
||||
"@tauri-apps/plugin-process": "^2.0.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.21.0",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,9 @@ tauri-plugin-store = "2"
|
|||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-clipboard-manager = "2"
|
||||
tauri-plugin-updater = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-process = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
|
@ -36,9 +39,9 @@ hex = "0.4"
|
|||
zeroize = { version = "1", features = ["derive"] }
|
||||
bech32 = "0.11"
|
||||
|
||||
# Local crates from the monorepo
|
||||
# Local crates from the monorepo (optional - for direct integration with core)
|
||||
synor-crypto = { path = "../../../crates/synor-crypto", optional = true }
|
||||
synor-primitives = { path = "../../../crates/synor-primitives", optional = true }
|
||||
synor-types = { path = "../../../crates/synor-types", optional = true }
|
||||
synor-rpc = { path = "../../../crates/synor-rpc", optional = true }
|
||||
|
||||
[features]
|
||||
|
|
|
|||
|
|
@ -5,30 +5,137 @@
|
|||
//! - Direct RPC communication with Synor nodes
|
||||
//! - Transaction signing with Dilithium3 post-quantum signatures
|
||||
//! - File system access for wallet backups
|
||||
//! - System tray for background operation
|
||||
//! - Auto-updates for seamless upgrades
|
||||
|
||||
mod commands;
|
||||
mod crypto;
|
||||
mod error;
|
||||
mod wallet;
|
||||
|
||||
use tauri::Manager;
|
||||
use tauri::{
|
||||
menu::{Menu, MenuItem},
|
||||
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
||||
Emitter, Manager, Runtime,
|
||||
};
|
||||
|
||||
pub use error::{Error, Result};
|
||||
|
||||
/// Build the system tray menu
|
||||
fn build_tray_menu<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<Menu<R>> {
|
||||
let show = MenuItem::with_id(app, "show", "Show Wallet", true, None::<&str>)?;
|
||||
let hide = MenuItem::with_id(app, "hide", "Hide to Tray", true, None::<&str>)?;
|
||||
let separator1 = MenuItem::with_id(app, "sep1", "─────────────", false, None::<&str>)?;
|
||||
let lock = MenuItem::with_id(app, "lock", "Lock Wallet", true, None::<&str>)?;
|
||||
let separator2 = MenuItem::with_id(app, "sep2", "─────────────", false, None::<&str>)?;
|
||||
let check_updates = MenuItem::with_id(app, "check_updates", "Check for Updates", true, None::<&str>)?;
|
||||
let separator3 = MenuItem::with_id(app, "sep3", "─────────────", false, None::<&str>)?;
|
||||
let quit = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
|
||||
|
||||
Menu::with_items(
|
||||
app,
|
||||
&[
|
||||
&show,
|
||||
&hide,
|
||||
&separator1,
|
||||
&lock,
|
||||
&separator2,
|
||||
&check_updates,
|
||||
&separator3,
|
||||
&quit,
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Handle system tray menu events
|
||||
fn handle_tray_event<R: Runtime>(app: &tauri::AppHandle<R>, event: TrayIconEvent) {
|
||||
match event {
|
||||
TrayIconEvent::Click {
|
||||
button: MouseButton::Left,
|
||||
button_state: MouseButtonState::Up,
|
||||
..
|
||||
} => {
|
||||
// Left click: show and focus the window
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle tray menu item clicks
|
||||
fn handle_menu_event<R: Runtime>(app: &tauri::AppHandle<R>, event: tauri::menu::MenuEvent) {
|
||||
match event.id().as_ref() {
|
||||
"show" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
"hide" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.hide();
|
||||
}
|
||||
}
|
||||
"lock" => {
|
||||
// Emit lock event to frontend
|
||||
let _ = app.emit("wallet-lock", ());
|
||||
}
|
||||
"check_updates" => {
|
||||
// Emit update check event to frontend
|
||||
let _ = app.emit("check-updates", ());
|
||||
}
|
||||
"quit" => {
|
||||
// Emit quit event to allow cleanup
|
||||
let _ = app.emit("app-quit", ());
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initialize the Tauri application with all plugins and commands
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
// Core plugins
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.plugin(tauri_plugin_store::Builder::default().build())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
// New plugins for desktop features
|
||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_process::init())
|
||||
.setup(|app| {
|
||||
// Initialize wallet state
|
||||
let wallet_state = wallet::WalletState::new();
|
||||
app.manage(wallet_state);
|
||||
|
||||
// Build and set up system tray
|
||||
let menu = build_tray_menu(app.handle())?;
|
||||
let _tray = TrayIconBuilder::new()
|
||||
.icon(app.default_window_icon().unwrap().clone())
|
||||
.menu(&menu)
|
||||
.menu_on_left_click(false)
|
||||
.on_tray_icon_event(|tray, event| {
|
||||
handle_tray_event(tray.app_handle(), event);
|
||||
})
|
||||
.on_menu_event(|app, event| {
|
||||
handle_menu_event(app, event);
|
||||
})
|
||||
.tooltip("Synor Wallet")
|
||||
.build(app)?;
|
||||
|
||||
// Check for updates on startup (non-blocking)
|
||||
let handle = app.handle().clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
check_for_updates(handle).await;
|
||||
});
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
// Open devtools in development
|
||||
|
|
@ -61,7 +168,96 @@ pub fn run() {
|
|||
commands::connect_node,
|
||||
commands::disconnect_node,
|
||||
commands::get_network_status,
|
||||
// Updates
|
||||
check_update,
|
||||
install_update,
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
/// Check for updates on startup
|
||||
async fn check_for_updates<R: Runtime>(app: tauri::AppHandle<R>) {
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
// Wait a few seconds before checking for updates
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
|
||||
let updater = match app.updater() {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
eprintln!("Updater not available: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match updater.check().await {
|
||||
Ok(Some(update)) => {
|
||||
// Notify frontend about available update
|
||||
let _ = app.emit("update-available", serde_json::json!({
|
||||
"version": update.version,
|
||||
"body": update.body,
|
||||
"date": update.date
|
||||
}));
|
||||
}
|
||||
Ok(None) => {
|
||||
// No update available
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to check for updates: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tauri command: Check for updates
|
||||
#[tauri::command]
|
||||
async fn check_update<R: Runtime>(app: tauri::AppHandle<R>) -> std::result::Result<Option<serde_json::Value>, String> {
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
let updater = app.updater().map_err(|e| format!("Updater not configured: {}", e))?;
|
||||
|
||||
match updater.check().await {
|
||||
Ok(Some(update)) => {
|
||||
Ok(Some(serde_json::json!({
|
||||
"version": update.version,
|
||||
"body": update.body,
|
||||
"date": update.date
|
||||
})))
|
||||
}
|
||||
Ok(None) => Ok(None),
|
||||
Err(e) => Err(format!("Update check failed: {}", e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tauri command: Install update
|
||||
#[tauri::command]
|
||||
async fn install_update<R: Runtime>(app: tauri::AppHandle<R>) -> std::result::Result<(), String> {
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
let updater = app.updater().map_err(|e| format!("Updater not configured: {}", e))?;
|
||||
|
||||
match updater.check().await {
|
||||
Ok(Some(update)) => {
|
||||
// Download and install with progress callbacks
|
||||
let downloaded: u64 = 0;
|
||||
let total_size: u64 = 0;
|
||||
|
||||
update.download_and_install(
|
||||
move |chunk_len, content_len| {
|
||||
// Progress callback - could emit progress events here
|
||||
let _ = (chunk_len, content_len, downloaded, total_size);
|
||||
},
|
||||
|| {
|
||||
// Ready to install callback
|
||||
},
|
||||
).await.map_err(|e| format!("Update installation failed: {}", e))?;
|
||||
|
||||
// Notify user to restart
|
||||
let _ = app.emit("update-installed", ());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Ok(None) => Err("No update available".to_string()),
|
||||
Err(e) => Err(format!("Update check failed: {}", e)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,17 @@
|
|||
"open": true
|
||||
},
|
||||
"dialog": {},
|
||||
"clipboard-manager": {}
|
||||
"clipboard-manager": {},
|
||||
"updater": {
|
||||
"endpoints": [
|
||||
"https://releases.synor.io/wallet/{{target}}/{{arch}}/{{current_version}}"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IFNZTk9SIFdBTExFVApSV1RiRE5QYVdvOHVqeTFGMVdXR0dZR3ROQ0Y4bno1dXl4SWJocjd1V3pvbnZ6Y0xSNjNyNjNUdgo=",
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
}
|
||||
},
|
||||
"notification": {}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import { useWalletStore } from './store/wallet';
|
|||
// Layout
|
||||
import Layout from './components/Layout';
|
||||
import TitleBar from './components/TitleBar';
|
||||
import UpdateBanner from './components/UpdateBanner';
|
||||
|
||||
// Hooks
|
||||
import { useTrayEvents } from './hooks/useTrayEvents';
|
||||
|
||||
// Pages
|
||||
import Welcome from './pages/Welcome';
|
||||
|
|
@ -19,8 +23,14 @@ import Settings from './pages/Settings';
|
|||
function App() {
|
||||
const { isInitialized, isUnlocked } = useWalletStore();
|
||||
|
||||
// Listen for system tray events
|
||||
useTrayEvents();
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col bg-gray-950">
|
||||
{/* Update notification banner */}
|
||||
<UpdateBanner />
|
||||
|
||||
{/* Custom title bar for frameless window (optional) */}
|
||||
<TitleBar />
|
||||
|
||||
|
|
|
|||
138
apps/desktop-wallet/src/components/UpdateBanner.tsx
Normal file
138
apps/desktop-wallet/src/components/UpdateBanner.tsx
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
/**
|
||||
* Update notification banner.
|
||||
*
|
||||
* Displays when a new version is available.
|
||||
*/
|
||||
|
||||
import { useAutoUpdater } from '../hooks/useAutoUpdater';
|
||||
|
||||
export function UpdateBanner() {
|
||||
const {
|
||||
available,
|
||||
downloading,
|
||||
installed,
|
||||
updateInfo,
|
||||
installUpdate,
|
||||
dismissUpdate,
|
||||
} = useAutoUpdater();
|
||||
|
||||
// Don't render if no update available
|
||||
if (!available && !installed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Show restart prompt after install
|
||||
if (installed) {
|
||||
return (
|
||||
<div className="bg-green-900/80 border-b border-green-700 px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5 text-green-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-green-100 text-sm">
|
||||
Update installed! Restart to apply changes.
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="px-3 py-1 text-sm bg-green-600 hover:bg-green-500 text-white rounded transition-colors"
|
||||
>
|
||||
Restart Now
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Show update available banner
|
||||
return (
|
||||
<div className="bg-blue-900/80 border-b border-blue-700 px-4 py-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<svg
|
||||
className="w-5 h-5 text-blue-400"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4"
|
||||
/>
|
||||
</svg>
|
||||
<span className="text-blue-100 text-sm">
|
||||
{updateInfo?.version
|
||||
? `Version ${updateInfo.version} is available`
|
||||
: 'A new update is available'}
|
||||
</span>
|
||||
{updateInfo?.body && (
|
||||
<span className="text-blue-300 text-xs hidden sm:inline">
|
||||
- {updateInfo.body.slice(0, 50)}
|
||||
{updateInfo.body.length > 50 ? '...' : ''}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{downloading ? (
|
||||
<div className="flex items-center gap-2 text-blue-200 text-sm">
|
||||
<svg
|
||||
className="w-4 h-4 animate-spin"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
Downloading...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={installUpdate}
|
||||
className="px-3 py-1 text-sm bg-blue-600 hover:bg-blue-500 text-white rounded transition-colors"
|
||||
>
|
||||
Install Update
|
||||
</button>
|
||||
<button
|
||||
onClick={dismissUpdate}
|
||||
className="p-1 text-blue-300 hover:text-blue-100 transition-colors"
|
||||
aria-label="Dismiss"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default UpdateBanner;
|
||||
133
apps/desktop-wallet/src/hooks/useAutoUpdater.ts
Normal file
133
apps/desktop-wallet/src/hooks/useAutoUpdater.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
/**
|
||||
* Auto-updater hook for desktop wallet.
|
||||
*
|
||||
* Listens to backend events and provides update functionality.
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export interface UpdateInfo {
|
||||
version: string;
|
||||
body?: string;
|
||||
date?: string;
|
||||
downloadUrl?: string;
|
||||
}
|
||||
|
||||
export interface UpdateState {
|
||||
checking: boolean;
|
||||
available: boolean;
|
||||
downloading: boolean;
|
||||
installed: boolean;
|
||||
error: string | null;
|
||||
updateInfo: UpdateInfo | null;
|
||||
}
|
||||
|
||||
export function useAutoUpdater() {
|
||||
const [state, setState] = useState<UpdateState>({
|
||||
checking: false,
|
||||
available: false,
|
||||
downloading: false,
|
||||
installed: false,
|
||||
error: null,
|
||||
updateInfo: null,
|
||||
});
|
||||
|
||||
// Listen for backend events
|
||||
useEffect(() => {
|
||||
const unlisteners: (() => void)[] = [];
|
||||
|
||||
// Update available notification from backend
|
||||
listen<UpdateInfo>('update-available', (event) => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
available: true,
|
||||
updateInfo: event.payload,
|
||||
}));
|
||||
}).then((unlisten) => unlisteners.push(unlisten));
|
||||
|
||||
// Update installed notification
|
||||
listen('update-installed', () => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
downloading: false,
|
||||
installed: true,
|
||||
}));
|
||||
}).then((unlisten) => unlisteners.push(unlisten));
|
||||
|
||||
// Check updates trigger from tray
|
||||
listen('check-updates', () => {
|
||||
checkForUpdates();
|
||||
}).then((unlisten) => unlisteners.push(unlisten));
|
||||
|
||||
return () => {
|
||||
unlisteners.forEach((unlisten) => unlisten());
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Check for updates manually
|
||||
const checkForUpdates = useCallback(async () => {
|
||||
setState((prev) => ({ ...prev, checking: true, error: null }));
|
||||
|
||||
try {
|
||||
const result = await invoke<UpdateInfo | null>('check_update');
|
||||
|
||||
if (result) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
checking: false,
|
||||
available: true,
|
||||
updateInfo: result,
|
||||
}));
|
||||
} else {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
checking: false,
|
||||
available: false,
|
||||
updateInfo: null,
|
||||
}));
|
||||
}
|
||||
} catch (error) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
checking: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Install available update
|
||||
const installUpdate = useCallback(async () => {
|
||||
if (!state.available) return;
|
||||
|
||||
setState((prev) => ({ ...prev, downloading: true, error: null }));
|
||||
|
||||
try {
|
||||
await invoke('install_update');
|
||||
// The 'update-installed' event will set the installed state
|
||||
} catch (error) {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
downloading: false,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
}));
|
||||
}
|
||||
}, [state.available]);
|
||||
|
||||
// Dismiss update notification
|
||||
const dismissUpdate = useCallback(() => {
|
||||
setState((prev) => ({
|
||||
...prev,
|
||||
available: false,
|
||||
updateInfo: null,
|
||||
}));
|
||||
}, []);
|
||||
|
||||
return {
|
||||
...state,
|
||||
checkForUpdates,
|
||||
installUpdate,
|
||||
dismissUpdate,
|
||||
};
|
||||
}
|
||||
40
apps/desktop-wallet/src/hooks/useTrayEvents.ts
Normal file
40
apps/desktop-wallet/src/hooks/useTrayEvents.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
/**
|
||||
* System tray event hook for desktop wallet.
|
||||
*
|
||||
* Handles events triggered from the system tray menu.
|
||||
*/
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useWalletStore } from '../store/wallet';
|
||||
|
||||
export function useTrayEvents() {
|
||||
const navigate = useNavigate();
|
||||
const { lockWallet, isUnlocked } = useWalletStore();
|
||||
|
||||
useEffect(() => {
|
||||
const unlisteners: (() => void)[] = [];
|
||||
|
||||
// Handle wallet lock from tray
|
||||
listen('wallet-lock', async () => {
|
||||
if (isUnlocked) {
|
||||
await lockWallet();
|
||||
navigate('/unlock');
|
||||
}
|
||||
}).then((unlisten) => unlisteners.push(unlisten));
|
||||
|
||||
// Handle app quit from tray
|
||||
listen('app-quit', async () => {
|
||||
// Perform cleanup if wallet is unlocked
|
||||
if (isUnlocked) {
|
||||
await lockWallet();
|
||||
}
|
||||
// The backend will handle the actual quit
|
||||
}).then((unlisten) => unlisteners.push(unlisten));
|
||||
|
||||
return () => {
|
||||
unlisteners.forEach((unlisten) => unlisten());
|
||||
};
|
||||
}, [isUnlocked, lockWallet, navigate]);
|
||||
}
|
||||
|
|
@ -7,11 +7,22 @@ import {
|
|||
AlertTriangle,
|
||||
Check,
|
||||
RefreshCw,
|
||||
Download,
|
||||
} from 'lucide-react';
|
||||
import { useWalletStore } from '../store/wallet';
|
||||
import { useAutoUpdater } from '../hooks/useAutoUpdater';
|
||||
|
||||
export default function Settings() {
|
||||
const { networkStatus, connectNode } = useWalletStore();
|
||||
const {
|
||||
checking,
|
||||
available,
|
||||
downloading,
|
||||
error: updateError,
|
||||
updateInfo,
|
||||
checkForUpdates,
|
||||
installUpdate,
|
||||
} = useAutoUpdater();
|
||||
|
||||
const [rpcUrl, setRpcUrl] = useState(
|
||||
'http://localhost:17110'
|
||||
|
|
@ -247,6 +258,71 @@ export default function Settings() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Updates */}
|
||||
<div className="card">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Download size={20} className="text-gray-400" />
|
||||
<h2 className="text-lg font-semibold text-white">Updates</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{available && updateInfo ? (
|
||||
<div className="p-4 bg-blue-900/20 border border-blue-700/50 rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-blue-200 font-medium">
|
||||
Version {updateInfo.version} available
|
||||
</p>
|
||||
{updateInfo.body && (
|
||||
<p className="text-sm text-blue-300/70 mt-1">
|
||||
{updateInfo.body}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={installUpdate}
|
||||
disabled={downloading}
|
||||
className="btn btn-primary"
|
||||
>
|
||||
{downloading ? (
|
||||
<>
|
||||
<RefreshCw size={18} className="animate-spin" />
|
||||
Downloading...
|
||||
</>
|
||||
) : (
|
||||
'Install Update'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center justify-between">
|
||||
<p className="text-gray-400 text-sm">
|
||||
{checking ? 'Checking for updates...' : 'No updates available'}
|
||||
</p>
|
||||
<button
|
||||
onClick={checkForUpdates}
|
||||
disabled={checking}
|
||||
className="btn btn-secondary"
|
||||
>
|
||||
{checking ? (
|
||||
<RefreshCw size={18} className="animate-spin" />
|
||||
) : (
|
||||
'Check for Updates'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{updateError && (
|
||||
<p className="text-red-400 text-sm flex items-center gap-2">
|
||||
<AlertTriangle size={16} />
|
||||
{updateError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version info */}
|
||||
<div className="text-center text-sm text-gray-600">
|
||||
Synor Wallet v0.1.0
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue