feat(gui, tauri): Save settings in Tauri storage (#102)

- Implemented dual persistence strategy:
  - **User Settings**: Persisted across app restarts using `tauri-plugin-store`.
  - **Transient State**: Persisted across page reloads using `sessionStorage`.
- Added `settingsSlice` reducer for managing persistent user settings.
- Updated Redux store configuration to handle multiple persistence layers.
- Added a new Settings page in the GUI where users can specify custom Electrum RPC URLs for Bitcoin and Monero node URLs.
  - Users can input their preferred Electrum server (`ssl://host:port`) and Monero daemon (`http://host:port`).
  - Input fields include validation to ensure correct URL formats.
  - Settings persist across application restarts using Tauri's storage plugin.
  - A reset option is available to revert to default settings.
- Improved the Daemon Controller in the Help page:
  - Renamed `RpcControlBox` to `DaemonControlBox` for clarity.
  - Users can now start the daemon manually if it isn't running or has failed.
  - Added a "Restart GUI" button to apply new settings immediately.
  - Displayed the daemon's status within the controller.
- Upgraded Tauri and related plugins to stable version `2.0.0`:
  - Updated `tauri`, `tauri-build`, and `tauri-utils` to `2.0.0`.
  - Ensured compatibility with the latest stable release.
- Updated Tauri plugins to version `2.0.0`:
  - `tauri-plugin-clipboard-manager`
  - `tauri-plugin-shell`
  - Added new plugins:
    - `tauri-plugin-store` for settings persistence.
    - `tauri-plugin-process` to enable application relaunch.
- Deferred Context initialization until explicitly triggered from the frontend.
  - Moved Context setup from the `setup` function to a new `initialize_context` Tauri command.
  - Allows the application to start without immediately initializing the backend context.
  - Context initialization now considers user-provided settings for Electrum and Monero nodes.
- Introduced a `ValidatedTextField` component for form inputs with validation logic.
  - Provides immediate feedback on input validity.
  - Used in the Settings page for Electrum and Monero node URLs.
- If the user provides an override Monero remote daemon, we check if it reachable and on the correct network before starting the `monero-wallet-rpc`
- Changed `bitcoin_confirmation_target` type from `usize` to `u16`.
This commit is contained in:
binarybaron 2024-10-08 16:57:01 +06:00 committed by GitHub
parent d4503a6e9c
commit 253e0b0cf6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
40 changed files with 1124 additions and 451 deletions

View file

@ -20,6 +20,10 @@ once_cell = "1"
serde = { version = "1", features = [ "derive" ] }
serde_json = "1"
swap = { path = "../swap", features = [ "tauri" ] }
tauri = { version = "2.0.0-rc.1", features = [ "config-json5" ] }
tauri = { version = "2.0.0", features = [ "config-json5" ] }
tauri-plugin-clipboard-manager = "2.1.0-beta.7"
tauri-plugin-shell = "2.0.0-rc.2"
tauri-plugin-devtools = "2.0.0"
tauri-plugin-process = "2.0.0"
tauri-plugin-shell = "2.0.0"
tauri-plugin-store = "2.0.0"
tracing = "0.1.40"

View file

@ -7,6 +7,8 @@
"core:event:allow-emit",
"core:event:default",
"clipboard-manager:allow-write-text",
"shell:allow-open"
"shell:allow-open",
"store:default",
"process:default"
]
}

View file

@ -1,3 +1,4 @@
use anyhow::Context as AnyhowContext;
use std::result::Result;
use std::sync::Arc;
use swap::cli::{
@ -7,7 +8,7 @@ use swap::cli::{
GetSwapInfosAllArgs, ListSellersArgs, MoneroRecoveryArgs, ResumeSwapArgs,
SuspendCurrentSwapArgs, WithdrawBtcArgs,
},
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle},
tauri_bindings::{TauriContextStatusEvent, TauriEmitter, TauriHandle, TauriSettings},
Context, ContextBuilder,
},
command::{Bitcoin, Monero},
@ -104,13 +105,12 @@ impl State {
fn try_get_context(&self) -> Result<Arc<Context>, String> {
self.context
.clone()
.ok_or("Context not available")
.to_string_result()
.ok_or("Context not available".to_string())
}
}
/// Sets up the Tauri application
/// Initializes the Tauri state and spawns an async task to set up the Context
/// Initializes the Tauri state
fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let app_handle = app.app_handle().to_owned();
@ -118,44 +118,14 @@ fn setup(app: &mut tauri::App) -> Result<(), Box<dyn std::error::Error>> {
// If we don't do this, Tauri commands will panic at runtime if no value is present
app_handle.manage::<RwLock<State>>(RwLock::new(State::new()));
tauri::async_runtime::spawn(async move {
let tauri_handle = TauriHandle::new(app_handle.clone());
let context = ContextBuilder::new(true)
.with_bitcoin(Bitcoin {
bitcoin_electrum_rpc_url: None,
bitcoin_target_block: None,
})
.with_monero(Monero {
monero_daemon_address: None,
})
.with_json(false)
.with_debug(true)
.with_tauri(tauri_handle.clone())
.build()
.await;
match context {
Ok(context) => {
let state = app_handle.state::<RwLock<State>>();
state.write().await.set_context(Arc::new(context));
// To display to the user that the setup is done, we emit an event to the Tauri frontend
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
}
Err(e) => {
println!("Error while initializing context: {:?}", e);
// To display to the user that the setup failed, we emit an event to the Tauri frontend
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Failed);
}
}
});
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_store::Builder::new().build())
.plugin(tauri_plugin_clipboard_manager::init())
.plugin(tauri_plugin_shell::init())
.invoke_handler(tauri::generate_handler![
@ -171,6 +141,7 @@ pub fn run() {
suspend_current_swap,
cancel_and_refund,
is_context_available,
initialize_context,
])
.setup(setup)
.build(tauri::generate_context!())
@ -220,5 +191,56 @@ tauri_command!(get_history, GetHistoryArgs, no_args);
/// Here we define Tauri commands whose implementation is not delegated to the Request trait
#[tauri::command]
async fn is_context_available(context: tauri::State<'_, RwLock<State>>) -> Result<bool, String> {
// TODO: Here we should return more information about status of the context (e.g. initializing, failed)
Ok(context.read().await.try_get_context().is_ok())
}
/// Tauri command to initialize the Context
#[tauri::command]
async fn initialize_context(
settings: TauriSettings,
app_handle: tauri::AppHandle,
state: tauri::State<'_, RwLock<State>>,
) -> Result<(), String> {
// Acquire a write lock on the state
let mut state_write_lock = state
.try_write()
.context("Context is already being initialized")
.to_string_result()?;
// Get app handle and create a Tauri handle
let tauri_handle = TauriHandle::new(app_handle.clone());
let context_result = ContextBuilder::new(true)
.with_bitcoin(Bitcoin {
bitcoin_electrum_rpc_url: settings.electrum_rpc_url,
bitcoin_target_block: settings.bitcoin_confirmation_target.into(),
})
.with_monero(Monero {
monero_daemon_address: settings.monero_node_url,
})
.with_json(false)
.with_debug(true)
.with_tauri(tauri_handle.clone())
.build()
.await;
match context_result {
Ok(context_instance) => {
state_write_lock.set_context(Arc::new(context_instance));
tracing::info!("Context initialized");
// Emit event to frontend
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Available);
Ok(())
}
Err(e) => {
tracing::error!(error = ?e, "Failed to initialize context");
// Emit event to frontend
tauri_handle.emit_context_init_progress_event(TauriContextStatusEvent::Failed);
Err(e.to_string())
}
}
}