From ed9032df2b4016acb86f5bb496339cb49894ae24 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 13 Aug 2025 19:34:39 +0700 Subject: [PATCH 1/2] feat: implement new ConfirmationDialog component throughout app --- .../contracts_documents_screen.rs | 80 +- src/ui/identities/identities_screen.rs | 15 +- src/ui/identities/withdraw_screen.rs | 149 ++-- src/ui/tokens/burn_tokens_screen.rs | 173 +++-- src/ui/tokens/claim_tokens_screen.rs | 92 +-- src/ui/tokens/destroy_frozen_funds_screen.rs | 191 +++-- .../tokens/destroy_frozen_funds_screen.rs.bak | 661 ++++++++++++++++ src/ui/tokens/direct_token_purchase_screen.rs | 152 ++-- src/ui/tokens/freeze_tokens_screen.rs | 174 ++--- src/ui/tokens/mint_tokens_screen.rs | 202 +++-- src/ui/tokens/mint_tokens_screen.rs.bak | 734 ++++++++++++++++++ src/ui/tokens/pause_tokens_screen.rs | 130 ++-- src/ui/tokens/resume_tokens_screen.rs | 130 ++-- .../data_contract_json_pop_up.rs | 69 +- src/ui/tokens/tokens_screen/mod.rs | 112 +-- src/ui/tokens/tokens_screen/token_creator.rs | 127 +-- src/ui/tokens/transfer_tokens_screen.rs | 175 ++--- src/ui/tokens/transfer_tokens_screen.rs.bak | 587 ++++++++++++++ src/ui/tokens/unfreeze_tokens_screen.rs | 183 ++--- src/ui/tokens/unfreeze_tokens_screen.rs.bak | 694 +++++++++++++++++ 20 files changed, 3779 insertions(+), 1051 deletions(-) create mode 100644 src/ui/tokens/destroy_frozen_funds_screen.rs.bak create mode 100644 src/ui/tokens/mint_tokens_screen.rs.bak create mode 100644 src/ui/tokens/transfer_tokens_screen.rs.bak create mode 100644 src/ui/tokens/unfreeze_tokens_screen.rs.bak diff --git a/src/ui/contracts_documents/contracts_documents_screen.rs b/src/ui/contracts_documents/contracts_documents_screen.rs index 259cc50ef..b164646c5 100644 --- a/src/ui/contracts_documents/contracts_documents_screen.rs +++ b/src/ui/contracts_documents/contracts_documents_screen.rs @@ -4,6 +4,8 @@ use crate::backend_task::contract::ContractTask; use crate::backend_task::document::DocumentTask::{self, FetchDocumentsPage}; // Updated import use crate::context::AppContext; use crate::model::qualified_contract::QualifiedContract; +use crate::ui::components::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::contract_chooser_panel::{ ContractChooserState, add_contract_chooser_panel, }; @@ -57,7 +59,7 @@ pub struct DocumentQueryScreen { selected_index: Option, pub matching_documents: Vec, document_query_status: DocumentQueryStatus, - confirm_remove_contract_popup: bool, + confirmation_dialog: Option, contract_to_remove: Option, pending_document_type: DocumentType, pending_fields_selection: HashMap, @@ -122,7 +124,7 @@ impl DocumentQueryScreen { selected_index: None, matching_documents: vec![], document_query_status: DocumentQueryStatus::NotStarted, - confirm_remove_contract_popup: false, + confirmation_dialog: None, contract_to_remove: None, pending_document_type, pending_fields_selection, @@ -490,53 +492,44 @@ impl DocumentQueryScreen { let contract_to_remove = match &self.contract_to_remove { Some(contract) => *contract, None => { - self.confirm_remove_contract_popup = false; + self.confirmation_dialog = None; return AppAction::None; } }; - let mut app_action = AppAction::None; - let mut is_open = true; - - egui::Window::new("Confirm Remove Contract") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - let contract_alias_or_id = - match self.app_context.get_contract_by_id(&contract_to_remove) { - Ok(Some(contract)) => contract - .alias - .unwrap_or_else(|| contract.contract.id().to_string(Encoding::Base58)), - Ok(None) | Err(_) => contract_to_remove.to_string(Encoding::Base58), - }; - - ui.label(format!( + let contract_alias_or_id = match self.app_context.get_contract_by_id(&contract_to_remove) { + Ok(Some(contract)) => contract + .alias + .unwrap_or_else(|| contract.contract.id().to_string(Encoding::Base58)), + Ok(None) | Err(_) => contract_to_remove.to_string(Encoding::Base58), + }; + + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Remove Contract".to_string(), + format!( "Are you sure you want to remove contract \"{}\"?", contract_alias_or_id - )); - - // Confirm button - if ui.button("Confirm").clicked() { - app_action = AppAction::BackendTask(BackendTask::ContractTask(Box::new( - ContractTask::RemoveContract(contract_to_remove), - ))); - self.confirm_remove_contract_popup = false; - self.contract_to_remove = None; - } - - // Cancel button - if ui.button("Cancel").clicked() { - self.confirm_remove_contract_popup = false; - self.contract_to_remove = None; - } - }); + ), + ) + }); - // If user closes the popup window (the [x] button), also reset state - if !is_open { - self.confirm_remove_contract_popup = false; - self.contract_to_remove = None; + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + let action = AppAction::BackendTask(BackendTask::ContractTask(Box::new( + ContractTask::RemoveContract(contract_to_remove), + ))); + self.confirmation_dialog = None; + self.contract_to_remove = None; + action + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + self.contract_to_remove = None; + AppAction::None + } + None => AppAction::None, } - app_action } } @@ -708,8 +701,9 @@ impl ScreenLike for DocumentQueryScreen { if let AppAction::BackendTask(BackendTask::ContractTask(contract_task)) = &action { if let ContractTask::RemoveContract(contract_id) = **contract_task { action = AppAction::None; - self.confirm_remove_contract_popup = true; self.contract_to_remove = Some(contract_id); + // Clear any existing dialog to create a new one with updated content + self.confirmation_dialog = None; } } @@ -752,7 +746,7 @@ impl ScreenLike for DocumentQueryScreen { ); }); - if self.confirm_remove_contract_popup { + if self.contract_to_remove.is_some() { inner_action |= self.show_remove_contract_popup(ui); } inner_action diff --git a/src/ui/identities/identities_screen.rs b/src/ui/identities/identities_screen.rs index 361d80b7b..dff31771a 100644 --- a/src/ui/identities/identities_screen.rs +++ b/src/ui/identities/identities_screen.rs @@ -836,8 +836,9 @@ impl IdentitiesScreen { action } - fn show_identity_to_remove(&mut self, ctx: &Context) { + fn show_identity_to_remove(&mut self, ctx: &Context) -> AppAction { if let Some(identity_to_remove) = self.identity_to_remove.clone() { + let action = AppAction::None; egui::Window::new("Confirm Removal") .collapsible(false) .resizable(false) @@ -884,6 +885,9 @@ impl IdentitiesScreen { } }); }); + action + } else { + AppAction::None } } @@ -1004,6 +1008,11 @@ impl ScreenLike for IdentitiesScreen { inner_action |= self.render_identities_view(ui, &identities_vec); } + // Handle identity removal confirmation dialog + if self.identity_to_remove.is_some() { + inner_action |= self.show_identity_to_remove(ctx); + } + // Show either refreshing indicator or message, but not both if let IdentitiesRefreshingStatus::Refreshing(start_time) = self.refreshing_status { ui.add_space(25.0); // Space above @@ -1040,10 +1049,6 @@ impl ScreenLike for IdentitiesScreen { inner_action }); - if self.identity_to_remove.is_some() { - self.show_identity_to_remove(ctx); - } - match action { AppAction::BackendTask(BackendTask::IdentityTask(IdentityTask::RefreshIdentity(_))) => { self.refreshing_status = diff --git a/src/ui/identities/withdraw_screen.rs b/src/ui/identities/withdraw_screen.rs index 720b6c404..85159e3ca 100644 --- a/src/ui/identities/withdraw_screen.rs +++ b/src/ui/identities/withdraw_screen.rs @@ -7,6 +7,7 @@ use crate::model::qualified_identity::encrypted_key_storage::PrivateKeyData; use crate::model::qualified_identity::{IdentityType, PrivateKeyTarget, QualifiedIdentity}; use crate::model::wallet::Wallet; use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::top_panel::add_top_panel; @@ -48,7 +49,7 @@ pub struct WithdrawalScreen { withdrawal_amount_input: Option, max_amount: u64, pub app_context: Arc, - confirmation_popup: bool, + confirmation_dialog: Option, withdraw_from_identity_status: WithdrawFromIdentityStatus, selected_wallet: Option>>, wallet_password: String, @@ -77,7 +78,7 @@ impl WithdrawalScreen { withdrawal_amount_input: None, max_amount, app_context: app_context.clone(), - confirmation_popup: false, + confirmation_dialog: None, withdraw_from_identity_status: WithdrawFromIdentityStatus::NotStarted, selected_wallet, wallet_password: String::new(), @@ -153,56 +154,68 @@ impl WithdrawalScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut app_action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Withdrawal") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - let address = if self.withdrawal_address.is_empty() { - None - } else { - match Address::from_str(&self.withdrawal_address) { - Ok(address) => Some(address.assume_checked()), - Err(_) => { - self.withdraw_from_identity_status = - WithdrawFromIdentityStatus::ErrorMessage( - "Invalid withdrawal address".to_string(), - ); - None - } - } - }; - - let message_address = if address.is_some() { - self.withdrawal_address.clone() - } else if let Some(payout_address) = self - .identity - .masternode_payout_address(self.app_context.network) - { - format!("masternode payout address {}", payout_address) - } else if !self.app_context.is_developer_mode() { + let address = if self.withdrawal_address.is_empty() { + None + } else { + match Address::from_str(&self.withdrawal_address) { + Ok(address) => Some(address.assume_checked()), + Err(_) => { self.withdraw_from_identity_status = WithdrawFromIdentityStatus::ErrorMessage( - "No masternode payout address".to_string(), + "Invalid withdrawal address".to_string(), ); - return; - } else { - "to default address".to_string() - }; + self.confirmation_dialog = None; + return AppAction::None; + } + } + }; + + let message_address = if address.is_some() { + self.withdrawal_address.clone() + } else if let Some(payout_address) = self + .identity + .masternode_payout_address(self.app_context.network) + { + format!("masternode payout address {}", payout_address) + } else if !self.app_context.is_developer_mode() { + self.withdraw_from_identity_status = WithdrawFromIdentityStatus::ErrorMessage( + "No masternode payout address".to_string(), + ); + self.confirmation_dialog = None; + return AppAction::None; + } else { + "to default address".to_string() + }; - let Some(selected_key) = self.selected_key.as_ref() else { - self.withdraw_from_identity_status = - WithdrawFromIdentityStatus::ErrorMessage("No selected key".to_string()); - return; - }; + let Some(selected_key) = self.selected_key.as_ref() else { + self.withdraw_from_identity_status = + WithdrawFromIdentityStatus::ErrorMessage("No selected key".to_string()); + self.confirmation_dialog = None; + return AppAction::None; + }; - ui.label(format!( + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Withdrawal".to_string(), + format!( "Are you sure you want to withdraw {} to {}", self.withdrawal_amount .as_ref() .expect("Withdrawal amount should be present"), message_address - )); + ), + ) + .danger_mode(true) // Withdrawal is a destructive operation + }); + + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.withdraw_from_identity_status = + WithdrawFromIdentityStatus::WaitingForResult(now); // Use the amount directly from the stored amount let credits = self @@ -211,31 +224,21 @@ impl WithdrawalScreen { .expect("Withdrawal amount should be present") .value() as u128; - if ui.button("Confirm").clicked() { - self.confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.withdraw_from_identity_status = - WithdrawFromIdentityStatus::WaitingForResult(now); - app_action = AppAction::BackendTask(BackendTask::IdentityTask( - IdentityTask::WithdrawFromIdentity( - self.identity.clone(), - address, - credits as Credits, - Some(selected_key.id()), - ), - )); - } - if ui.button("Cancel").clicked() { - self.confirmation_popup = false; - } - }); - if !is_open { - self.confirmation_popup = false; + AppAction::BackendTask(BackendTask::IdentityTask( + IdentityTask::WithdrawFromIdentity( + self.identity.clone(), + address, + credits as Credits, + Some(selected_key.id()), + ), + )) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - app_action } pub fn show_success(&self, ui: &mut Ui) -> AppAction { @@ -469,11 +472,19 @@ impl ScreenLike for WithdrawalScreen { .add_enabled(ready, button) .on_disabled_hover_text("Please enter a valid amount to withdraw") .clicked() + && self.confirmation_dialog.is_none() { - self.confirmation_popup = true; + // Validation will be done in show_confirmation_popup + self.confirmation_dialog = Some( + ConfirmationDialog::new( + "Confirm Withdrawal".to_string(), + "Loading...".to_string(), // Will be updated in show_confirmation_popup + ) + .danger_mode(true), + ); } - if self.confirmation_popup { + if self.confirmation_dialog.is_some() { inner_action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/burn_tokens_screen.rs b/src/ui/tokens/burn_tokens_screen.rs index 8292cccf9..c766017ad 100644 --- a/src/ui/tokens/burn_tokens_screen.rs +++ b/src/ui/tokens/burn_tokens_screen.rs @@ -1,4 +1,5 @@ use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -68,7 +69,7 @@ pub struct BurnTokensScreen { pub app_context: Arc, // Confirmation popup - show_confirmation_popup: bool, + confirmation_dialog: Option, // For password-based wallet unlocking, if needed selected_wallet: Option>>, @@ -204,7 +205,7 @@ impl BurnTokensScreen { status: BurnTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -233,88 +234,80 @@ impl BurnTokensScreen { /// Renders a confirm popup with the final "Are you sure?" step fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Burn") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - let amount = match self.amount.as_ref() { - Some(amount) if amount.value() > 0 => amount, - _ => { - self.error_message = - Some("Please enter a valid amount greater than 0.".into()); - self.status = BurnTokensStatus::ErrorMessage("Invalid amount".into()); - self.show_confirmation_popup = false; - return; - } - }; - - ui.label(format!("Are you sure you want to burn {}?", amount)); - - ui.add_space(10.0); + let amount = match self.amount.as_ref() { + Some(amount) if amount.value() > 0 => amount, + _ => { + self.error_message = Some("Please enter a valid amount greater than 0.".into()); + self.status = BurnTokensStatus::ErrorMessage("Invalid amount".into()); + self.confirmation_dialog = None; + return AppAction::None; + } + }; - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = BurnTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch the actual backend burn action - action = AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::BurnTokens { - owner_identity: self.identity_token_info.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - amount: amount.value(), - group_info, - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Burn".to_string(), + format!("Are you sure you want to burn {}?", amount), + ) + .danger_mode(true) // Burning tokens is destructive + }); - // Cancel button - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = BurnTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = + Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; - if !is_open { - self.show_confirmation_popup = false; + // Dispatch the actual backend burn action + AppAction::BackendTasks( + vec![ + BackendTask::TokenTask(Box::new(TokenTask::BurnTokens { + owner_identity: self.identity_token_info.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("Expected a key"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + amount: amount.value(), + group_info, + })), + BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), + ], + BackendTasksExecutionMode::Sequential, + ) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } /// Renders a simple "Success!" screen after completion @@ -596,12 +589,26 @@ impl ScreenLike for BurnTokensScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + // Create confirmation dialog on button click + if self.confirmation_dialog.is_none() { + let amount = match self.amount.as_ref() { + Some(amount) if amount.value() > 0 => amount, + _ => return AppAction::None, + }; + + self.confirmation_dialog = Some( + ConfirmationDialog::new( + "Confirm Burn".to_string(), + format!("Are you sure you want to burn {}?", amount), + ) + .danger_mode(true), + ); + } } } - // If user pressed "Burn," show a popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/claim_tokens_screen.rs b/src/ui/tokens/claim_tokens_screen.rs index 918ef055f..25bf04c1b 100644 --- a/src/ui/tokens/claim_tokens_screen.rs +++ b/src/ui/tokens/claim_tokens_screen.rs @@ -1,3 +1,5 @@ +use crate::ui::components::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -54,7 +56,7 @@ pub struct ClaimTokensScreen { status: ClaimTokensStatus, error_message: Option, pub app_context: Arc, - show_confirmation_popup: bool, + confirmation_dialog: Option, selected_wallet: Option>>, wallet_password: String, show_password: bool, @@ -122,7 +124,7 @@ impl ClaimTokensScreen { status: ClaimTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -181,52 +183,47 @@ impl ClaimTokensScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; let distribution_type = self .distribution_type .unwrap_or(TokenDistributionType::Perpetual); - egui::Window::new("Confirm Claim") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label("Are you sure you want to claim tokens for this contract?"); - ui.add_space(10.0); - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = ClaimTokensStatus::WaitingForResult(now); - - action = AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::ClaimTokens { - data_contract: Arc::new(self.token_contract.contract.clone()), - token_position: self.identity_token_basic_info.token_position, - actor_identity: self.identity.clone(), - distribution_type, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: self.public_note.clone(), - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } - - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Claim".to_string(), + "Are you sure you want to claim tokens for this contract?".to_string(), + ) + }); - if !is_open { - self.show_confirmation_popup = false; + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = ClaimTokensStatus::WaitingForResult(now); + + AppAction::BackendTasks( + vec![ + BackendTask::TokenTask(Box::new(TokenTask::ClaimTokens { + data_contract: Arc::new(self.token_contract.contract.clone()), + token_position: self.identity_token_basic_info.token_position, + actor_identity: self.identity.clone(), + distribution_type, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: self.public_note.clone(), + })), + BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), + ], + BackendTasksExecutionMode::Sequential, + ) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } fn show_success_screen(&self, ui: &mut Ui) -> AppAction { @@ -511,13 +508,16 @@ impl ScreenLike for ClaimTokensScreen { "Please select a distribution type.".to_string(), ); return; - } else { - self.show_confirmation_popup = true; + } else if self.confirmation_dialog.is_none() { + self.confirmation_dialog = Some(ConfirmationDialog::new( + "Confirm Claim".to_string(), + "Are you sure you want to claim tokens for this contract?".to_string(), + )); } } - // If user pressed "Claim," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/destroy_frozen_funds_screen.rs b/src/ui/tokens/destroy_frozen_funds_screen.rs index 67da57535..cb4089170 100644 --- a/src/ui/tokens/destroy_frozen_funds_screen.rs +++ b/src/ui/tokens/destroy_frozen_funds_screen.rs @@ -1,10 +1,12 @@ use super::tokens_screen::IdentityTokenInfo; -use crate::app::{AppAction, BackendTasksExecutionMode}; +use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; @@ -76,8 +78,8 @@ pub struct DestroyFrozenFundsScreen { /// Basic references pub app_context: Arc, - /// Confirmation popup - show_confirmation_popup: bool, + /// Confirmation dialog + confirmation_dialog: Option, /// If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -205,7 +207,7 @@ impl DestroyFrozenFundsScreen { status: DestroyFrozenFundsStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -226,100 +228,87 @@ impl DestroyFrozenFundsScreen { /// Confirmation popup fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Destroy Frozen Funds") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Parse the user input into an Identifier - let maybe_frozen_id = Identifier::from_string_try_encodings( - &self.frozen_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - - if maybe_frozen_id.is_err() { - self.error_message = Some("Invalid frozen identity format".into()); - self.status = DestroyFrozenFundsStatus::ErrorMessage("Invalid identity".into()); - self.show_confirmation_popup = false; - return; - } - - let frozen_id = maybe_frozen_id.unwrap(); - - ui.label(format!( - "Are you sure you want to destroy the frozen funds of identity {}?", - self.frozen_identity_id - )); - - ui.add_space(10.0); - - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = DestroyFrozenFundsStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch the actual backend destroy action - action = AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::DestroyFrozenFunds { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - frozen_identity: frozen_id, - group_info, - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } + let msg = format!( + "Are you sure you want to destroy frozen funds for identity {}? This action cannot be undone.", + self.frozen_identity_id + ); - // Cancel - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Destroy Frozen Funds", msg) + .confirm_text(Some("Destroy")) + .cancel_text(Some("Cancel")) + .danger_mode(true) + }); - if !is_open { - self.show_confirmation_popup = false; + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } + fn confirmation_ok(&mut self) -> AppAction { + let maybe_frozen_id = Identifier::from_string_try_encodings( + &self.frozen_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + if maybe_frozen_id.is_err() { + self.error_message = Some("Invalid frozen identity format".into()); + self.status = DestroyFrozenFundsStatus::ErrorMessage("Invalid identity".into()); + return AppAction::None; + } + let frozen_id = maybe_frozen_id.unwrap(); + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = DestroyFrozenFundsStatus::WaitingForResult(now); + + let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::DestroyFrozenFunds { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + frozen_identity: frozen_id, + group_info, + }, + ))) + } /// Simple “Success” screen fn show_success_screen(&self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; @@ -585,12 +574,22 @@ impl ScreenLike for DestroyFrozenFundsScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + // Initialize confirmation dialog when button is clicked + let msg = format!( + "Are you sure you want to destroy frozen funds for identity {}? This action cannot be undone.", + self.frozen_identity_id + ); + self.confirmation_dialog = Some( + ConfirmationDialog::new("Confirm Destroy Frozen Funds", msg) + .confirm_text(Some("Destroy")) + .cancel_text(Some("Cancel")) + .danger_mode(true), + ); } } - // If user pressed "Destroy," show a popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/destroy_frozen_funds_screen.rs.bak b/src/ui/tokens/destroy_frozen_funds_screen.rs.bak new file mode 100644 index 000000000..208565648 --- /dev/null +++ b/src/ui/tokens/destroy_frozen_funds_screen.rs.bak @@ -0,0 +1,661 @@ +use super::tokens_screen::IdentityTokenInfo; +use crate::app::{AppAction, BackendTasksExecutionMode}; +use crate::backend_task::BackendTask; +use crate::backend_task::tokens::TokenTask; +use crate::context::AppContext; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::model::wallet::Wallet; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::styled::island_central_panel; +use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; +use crate::ui::helpers::{TransactionType, add_identity_key_chooser, render_group_action_text}; +use crate::ui::identities::get_selected_wallet; +use crate::ui::identities::keys::add_key_screen::AddKeyScreen; +use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; +use crate::ui::theme::DashColors; +use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike}; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; +use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; +use dash_sdk::platform::{Identifier, IdentityPublicKey}; +use eframe::egui::{self, Color32, Context, Ui}; +use egui::RichText; +use std::collections::HashSet; +use std::sync::{Arc, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Represents possible states in the “destroy frozen funds” flow +#[derive(PartialEq)] +pub enum DestroyFrozenFundsStatus { + NotStarted, + WaitingForResult(u64), + ErrorMessage(String), + Complete, +} + +/// A screen for destroying frozen funds of a particular token contract +pub struct DestroyFrozenFundsScreen { + /// Identity that is authorized to destroy + pub identity: QualifiedIdentity, + + /// Info on which token contract we’re dealing with + pub identity_token_info: IdentityTokenInfo, + + /// The key used to sign the operation + selected_key: Option, + + group: Option<(GroupContractPosition, Group)>, + is_unilateral_group_member: bool, + pub group_action_id: Option, + + /// Optional public note + pub public_note: Option, + + /// The user must specify the identity ID whose frozen funds are to be destroyed + /// Typically some Identity that has been frozen by the system or a group + pub frozen_identity_id: String, + + /// All frozen identities that can be selected + /// TODO: We should filter them by frozen status, right now we just show all known identities + pub frozen_identities: Vec, + + status: DestroyFrozenFundsStatus, + error_message: Option, + + /// Basic references + pub app_context: Arc, + + /// Confirmation dialog + confirmation_dialog: Option, + + /// If password-based wallet unlocking is needed + selected_wallet: Option>>, + wallet_password: String, + show_password: bool, +} + +impl DestroyFrozenFundsScreen { + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); + + let mut error_message = None; + + let group = match identity_token_info + .token_config + .destroy_frozen_funds_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Burning is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to burn this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to burn this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + let mut is_unilateral_group_member = false; + if group.is_some() { + if let Some((_, group)) = group.clone() { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power { + if your_power >= &group.required_power() { + is_unilateral_group_member = true; + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); + + let all_identities = app_context + .load_local_qualified_identities() + .expect("Identities not loaded"); + + Self { + identity: identity_token_info.identity.clone(), + frozen_identity_id: String::new(), + frozen_identities: all_identities, + identity_token_info, + selected_key: possible_key, + group, + is_unilateral_group_member, + group_action_id: None, + public_note: None, + status: DestroyFrozenFundsStatus::NotStarted, + error_message, + app_context: app_context.clone(), + confirmation_dialog: None, + selected_wallet, + wallet_password: String::new(), + show_password: false, + } + } + + /// Renders the text input for specifying the “frozen identity” + fn render_frozen_identity_input(&mut self, ui: &mut Ui) { + ui.add( + IdentitySelector::new( + "frozen_identity_selector", + &mut self.frozen_identity_id, + &self.frozen_identities, + ) + .label("Frozen Identity ID:"), + ); + } + + /// Confirmation popup + fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + let mut is_open = true; + egui::Window::new("Confirm Destroy Frozen Funds") + .collapsible(false) + .open(&mut is_open) + .show(ui.ctx(), |ui| { + // Parse the user input into an Identifier + let maybe_frozen_id = Identifier::from_string_try_encodings( + &self.frozen_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + + if maybe_frozen_id.is_err() { + self.error_message = Some("Invalid frozen identity format".into()); + self.status = DestroyFrozenFundsStatus::ErrorMessage("Invalid identity".into()); + self.show_confirmation_popup = false; + return; + } + + let frozen_id = maybe_frozen_id.unwrap(); + + ui.label(format!( + "Are you sure you want to destroy the frozen funds of identity {}?", + self.frozen_identity_id + )); + + ui.add_space(10.0); + + // Confirm button + if ui.button("Confirm").clicked() { + self.show_confirmation_popup = false; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = DestroyFrozenFundsStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = + Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Dispatch the actual backend destroy action + action = AppAction::BackendTasks( + vec![ + BackendTask::TokenTask(Box::new(TokenTask::DestroyFrozenFunds { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("Expected a key"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + frozen_identity: frozen_id, + group_info, + })), + BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), + ], + BackendTasksExecutionMode::Sequential, + ); + } + + // Cancel + if ui.button("Cancel").clicked() { + self.show_confirmation_popup = false; + } + }); + + if !is_open { + self.show_confirmation_popup = false; + } + action + } + + /// Simple “Success” screen + fn show_success_screen(&self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading("🎉"); + if self.group_action_id.is_some() { + // This destroy is already initiated by the group, we are just signing it + ui.heading("Group Destroy Frozen Funds Signing Successful."); + } else if !self.is_unilateral_group_member && self.group.is_some() { + ui.heading("Group Action to Destroy Frozen Funds Initiated."); + } else { + ui.heading("Frozen Funds Destroyed Successfully."); + } + + ui.add_space(20.0); + + if self.group_action_id.is_some() { + if ui.button("Back to Group Actions").clicked() { + action = AppAction::PopScreenAndRefresh; + } + if ui.button("Back to Tokens").clicked() { + action = AppAction::SetMainScreenThenGoToMainScreen( + RootScreenType::RootScreenMyTokenBalances, + ); + } + } else { + if ui.button("Back to Tokens").clicked() { + action = AppAction::PopScreenAndRefresh; + } + + if !self.is_unilateral_group_member && ui.button("Go to Group Actions").clicked() { + action = AppAction::PopThenAddScreenToMainScreen( + RootScreenType::RootScreenDocumentQuery, + Screen::GroupActionsScreen(GroupActionsScreen::new( + &self.app_context.clone(), + )), + ); + } + } + }); + action + } +} + +impl ScreenLike for DestroyFrozenFundsScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Success => { + // If your backend returns "DestroyFrozenFunds" on success, + // or if there's a more descriptive success message: + if message.contains("Successfully destroyed frozen funds") + || message == "DestroyFrozenFunds" + { + self.status = DestroyFrozenFundsStatus::Complete; + } + } + MessageType::Error => { + self.status = DestroyFrozenFundsStatus::ErrorMessage(message.to_string()); + self.error_message = Some(message.to_string()); + } + MessageType::Info => { + // no-op + } + } + } + + fn refresh(&mut self) { + // Reload the identity data if needed + if let Ok(all_identities) = self.app_context.load_local_user_identities() { + if let Some(updated_identity) = all_identities + .into_iter() + .find(|id| id.identity.id() == self.identity.identity.id()) + { + self.identity = updated_identity; + } + } + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Destroy Frozen Funds", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Destroy Frozen Funds", AppAction::None), + ], + vec![], + ); + } + + // Left panel + action |= add_left_panel( + ctx, + &self.app_context, + crate::ui::RootScreenType::RootScreenMyTokenBalances, + ); + + // Subscreen chooser + action |= add_tokens_subscreen_chooser_panel(ctx, &self.app_context); + + island_central_panel(ctx, |ui| { + let dark_mode = ui.ctx().style().visuals.dark_mode; + + if self.status == DestroyFrozenFundsStatus::Complete { + action |= self.show_success_screen(ui); + return; + } + + ui.heading("Destroy Frozen Funds"); + ui.add_space(10.0); + + // Check if user has any auth keys + let has_keys = if self.app_context.is_developer_mode() { + !self.identity.identity.public_keys().is_empty() + } else { + !self + .identity + .available_authentication_keys_with_critical_security_level() + .is_empty() + }; + + if !has_keys { + ui.colored_label( + DashColors::error_color(dark_mode), + format!( + "No authentication keys with CRITICAL security level found for this {} identity.", + self.identity.identity_type, + ), + ); + ui.add_space(10.0); + + // Show "Add key" or "Check keys" option + let first_key = self.identity.identity.get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ); + + if let Some(key) = first_key { + if ui.button("Check Keys").clicked() { + action |= AppAction::AddScreen(Screen::KeyInfoScreen(KeyInfoScreen::new( + self.identity.clone(), + key.clone(), + None, + &self.app_context, + ))); + } + ui.add_space(5.0); + } + + if ui.button("Add key").clicked() { + action |= AppAction::AddScreen(Screen::AddKeyScreen(AddKeyScreen::new( + self.identity.clone(), + &self.app_context, + ))); + } + } else { + // Possibly handle locked wallet scenario + if self.selected_wallet.is_some() { + let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + if needed_unlock && !just_unlocked { + return; + } + } + + // Key selection + ui.heading("1. Select the key to sign the Destroy operation"); + ui.add_space(10.0); + + let mut selected_identity = Some(self.identity.clone()); + add_identity_key_chooser( + ui, + &self.app_context, + std::iter::once(&self.identity), + &mut selected_identity, + &mut self.selected_key, + TransactionType::TokenAction, + ); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // Frozen identity + ui.heading("2. Frozen identity to destroy funds from"); + ui.add_space(5.0); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Destroy so you are not allowed to choose the identity.", + ); + ui.add_space(5.0); + ui.label(format!("Identity: {}", self.frozen_identity_id)); + } else { + self.render_frozen_identity_input(ui); + } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // Render text input for the public note + ui.heading("3. Public note (optional)"); + ui.add_space(5.0); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Destroy so you are not allowed to put a note.", + ); + ui.add_space(5.0); + ui.label(format!( + "Note: {}", + self.public_note.clone().unwrap_or("None".to_string()) + )); + } else { + ui.horizontal(|ui| { + ui.label("Public note (optional):"); + ui.add_space(10.0); + let mut txt = self.public_note.clone().unwrap_or_default(); + if ui + .text_edit_singleline(&mut txt) + .on_hover_text( + "A note about the transaction that can be seen by the public.", + ) + .changed() + { + self.public_note = if !txt.is_empty() { Some(txt) } else { None }; + } + }); + } + + let button_text = render_group_action_text( + ui, + &self.group, + &self.identity_token_info, + "Destroy Frozen Funds", + &self.group_action_id, + ); + + // Destroy button + if self.app_context.is_developer_mode() || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); + + if ui.add(button).clicked() { + self.show_confirmation_popup = true; + } + } + + // If user pressed "Destroy," show a popup + if self.show_confirmation_popup { + action |= self.show_confirmation_popup(ui); + } + + // Show in-progress or error messages + ui.add_space(10.0); + match &self.status { + DestroyFrozenFundsStatus::NotStarted => { + // no-op + } + DestroyFrozenFundsStatus::WaitingForResult(start_time) => { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + let elapsed = now - start_time; + ui.label(format!( + "Destroying frozen funds... elapsed: {} seconds", + elapsed + )); + } + DestroyFrozenFundsStatus::ErrorMessage(msg) => { + ui.colored_label( + DashColors::error_color(dark_mode), + format!("Error: {}", msg), + ); + } + DestroyFrozenFundsStatus::Complete => { + // handled above + } + } + } + }); + + action + } +} + +impl ScreenWithWalletUnlock for DestroyFrozenFundsScreen { + fn selected_wallet_ref(&self) -> &Option>> { + &self.selected_wallet + } + + fn wallet_password_ref(&self) -> &String { + &self.wallet_password + } + + fn wallet_password_mut(&mut self) -> &mut String { + &mut self.wallet_password + } + + fn show_password(&self) -> bool { + self.show_password + } + + fn show_password_mut(&mut self) -> &mut bool { + &mut self.show_password + } + + fn set_error_message(&mut self, error_message: Option) { + self.error_message = error_message; + } + + fn error_message(&self) -> Option<&String> { + self.error_message.as_ref() + } +} diff --git a/src/ui/tokens/direct_token_purchase_screen.rs b/src/ui/tokens/direct_token_purchase_screen.rs index 8afabb88a..37581abde 100644 --- a/src/ui/tokens/direct_token_purchase_screen.rs +++ b/src/ui/tokens/direct_token_purchase_screen.rs @@ -14,6 +14,8 @@ use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::wallet::Wallet; +use crate::ui::components::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -53,7 +55,7 @@ pub struct PurchaseTokenScreen { pricing_fetch_attempted: bool, /// Screen stuff - show_confirmation_popup: bool, + confirmation_dialog: Option, status: PurchaseTokensStatus, error_message: Option, @@ -97,7 +99,7 @@ impl PurchaseTokenScreen { status: PurchaseTokensStatus::NotStarted, error_message: None, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -188,88 +190,67 @@ impl PurchaseTokenScreen { /// Renders a confirm popup with the final "Are you sure?" step fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Purchase") - .collapsible(false) - .open(&mut is_open) - .frame( - egui::Frame::default() - .fill(egui::Color32::from_rgb(245, 245, 245)) - .stroke(egui::Stroke::new( - 1.0, - egui::Color32::from_rgb(200, 200, 200), - )) - .shadow(egui::epaint::Shadow::default()) - .inner_margin(egui::Margin::same(20)) - .corner_radius(egui::CornerRadius::same(8)), - ) - .show(ui.ctx(), |ui| { - // Validate user input - let amount_ok = self.amount_to_purchase.parse::().ok(); - if amount_ok.is_none() { - self.error_message = Some("Please enter a valid amount.".into()); - self.status = PurchaseTokensStatus::ErrorMessage("Invalid amount".into()); - self.show_confirmation_popup = false; - return; - } + // Validate user input + let amount_ok = self.amount_to_purchase.parse::().ok(); + if amount_ok.is_none() { + self.error_message = Some("Please enter a valid amount.".into()); + self.status = PurchaseTokensStatus::ErrorMessage("Invalid amount".into()); + self.confirmation_dialog = None; + return AppAction::None; + } - let total_agreed_price_ok: Option = - self.total_agreed_price.parse::().ok(); - if total_agreed_price_ok.is_none() { - self.error_message = Some("Please enter a valid total agreed price.".into()); - self.status = - PurchaseTokensStatus::ErrorMessage("Invalid total agreed price".into()); - self.show_confirmation_popup = false; - return; - } + let total_agreed_price_ok: Option = self.total_agreed_price.parse::().ok(); + if total_agreed_price_ok.is_none() { + self.error_message = Some("Please enter a valid total agreed price.".into()); + self.status = PurchaseTokensStatus::ErrorMessage("Invalid total agreed price".into()); + self.confirmation_dialog = None; + return AppAction::None; + } - ui.label(format!( + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Purchase".to_string(), + format!( "Are you sure you want to purchase {} token(s) for {} Credits?", self.amount_to_purchase, self.total_agreed_price - )); - - ui.add_space(10.0); - - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = PurchaseTokensStatus::WaitingForResult(now); - - // Dispatch the actual backend purchase action - action = AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::PurchaseTokens { - identity: self.identity_token_info.identity.clone(), - data_contract: Arc::new( - self.identity_token_info.data_contract.contract.clone(), - ), - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - amount: amount_ok.expect("Expected a valid amount"), - total_agreed_price: total_agreed_price_ok - .expect("Expected a valid total agreed price"), - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } - - // Cancel button - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + ), + ) + }); - if !is_open { - self.show_confirmation_popup = false; + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = PurchaseTokensStatus::WaitingForResult(now); + + // Dispatch the actual backend purchase action + AppAction::BackendTasks( + vec![ + BackendTask::TokenTask(Box::new(TokenTask::PurchaseTokens { + identity: self.identity_token_info.identity.clone(), + data_contract: Arc::new( + self.identity_token_info.data_contract.contract.clone(), + ), + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("Expected a key"), + amount: amount_ok.expect("Expected a valid amount"), + total_agreed_price: total_agreed_price_ok + .expect("Expected a valid total agreed price"), + })), + BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), + ], + BackendTasksExecutionMode::Sequential, + ) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } /// Renders a simple "Success!" screen after completion @@ -505,8 +486,15 @@ impl ScreenLike for PurchaseTokenScreen { .fill(Color32::from_rgb(0, 128, 255)) .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() && self.confirmation_dialog.is_none() { + // Validation will be done in show_confirmation_popup + self.confirmation_dialog = Some(ConfirmationDialog::new( + "Confirm Purchase".to_string(), + format!( + "Are you sure you want to purchase {} token(s) for {} Credits?", + self.amount_to_purchase, self.total_agreed_price + ), + )); } } else { let button = egui::Button::new( @@ -524,8 +512,8 @@ impl ScreenLike for PurchaseTokenScreen { ); } - // If the user pressed "Purchase," show a popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/freeze_tokens_screen.rs b/src/ui/tokens/freeze_tokens_screen.rs index 5ab4b07cf..fbaeab331 100644 --- a/src/ui/tokens/freeze_tokens_screen.rs +++ b/src/ui/tokens/freeze_tokens_screen.rs @@ -5,6 +5,8 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; @@ -64,8 +66,8 @@ pub struct FreezeTokensScreen { // Basic references pub app_context: Arc, - // Confirmation popup - show_confirmation_popup: bool, + // Confirmation dialog + confirmation_dialog: Option, // If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -192,7 +194,7 @@ impl FreezeTokensScreen { status: FreezeTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -215,92 +217,87 @@ impl FreezeTokensScreen { /// Confirmation popup fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Freeze") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Validate user input - let parsed = Identifier::from_string_try_encodings( - &self.freeze_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - if parsed.is_err() { - self.error_message = Some("Please enter a valid identity ID.".into()); - self.status = FreezeTokensStatus::ErrorMessage("Invalid identity".into()); - self.show_confirmation_popup = false; - return; - } - let freeze_id = parsed.unwrap(); - - ui.label(format!( - "Are you sure you want to freeze identity {}?", - self.freeze_identity_id - )); - - ui.add_space(10.0); + let msg = format!( + "Are you sure you want to freeze identity {}?", + self.freeze_identity_id + ); - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = FreezeTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch to backend - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::FreezeTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - freeze_identity: freeze_id, - group_info, - }, - ))); - } + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Freeze", msg) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + }); - // Cancel - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, + } + } - if !is_open { - self.show_confirmation_popup = false; + /// Handle confirmation OK action + fn confirmation_ok(&mut self) -> AppAction { + // Validate user input + let parsed = Identifier::from_string_try_encodings( + &self.freeze_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + if parsed.is_err() { + self.error_message = Some("Please enter a valid identity ID.".into()); + self.status = FreezeTokensStatus::ErrorMessage("Invalid identity".into()); + return AppAction::None; } - action + let freeze_id = parsed.unwrap(); + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = FreezeTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Dispatch to backend + AppAction::BackendTask(BackendTask::TokenTask(Box::new(TokenTask::FreezeTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + freeze_identity: freeze_id, + group_info, + }))) } /// Success screen @@ -561,12 +558,13 @@ impl ScreenLike for FreezeTokensScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + // Initialize confirmation dialog when button is clicked + self.confirmation_dialog = None; // Reset for fresh dialog } } - // If user pressed "Freeze," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/mint_tokens_screen.rs b/src/ui/tokens/mint_tokens_screen.rs index 4a90e8aff..d7c93053f 100644 --- a/src/ui/tokens/mint_tokens_screen.rs +++ b/src/ui/tokens/mint_tokens_screen.rs @@ -7,13 +7,14 @@ use crate::model::amount::Amount; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::component_trait::{Component, ComponentResponse}; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; -use crate::ui::components::{Component, ComponentResponse}; use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; use crate::ui::helpers::{TransactionType, add_identity_key_chooser, render_group_action_text}; use crate::ui::identities::get_selected_wallet; @@ -32,7 +33,6 @@ use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; -use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::platform::{Identifier, IdentityPublicKey}; use eframe::egui::{self, Color32, Context, Ui}; use egui::RichText; @@ -70,7 +70,7 @@ pub struct MintTokensScreen { pub app_context: Arc, /// Confirmation popup - show_confirmation_popup: bool, + confirmation_dialog: Option, // If needed for password-based wallet unlocking: selected_wallet: Option>>, @@ -199,7 +199,7 @@ impl MintTokensScreen { status: MintTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -245,114 +245,93 @@ impl MintTokensScreen { /// Renders a confirm popup with the final "Are you sure?" step fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Mint") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Validate user input - let Some(amount) = &self.amount else { - self.error_message = Some("Please enter a valid amount.".into()); - self.status = MintTokensStatus::ErrorMessage("Invalid amount".into()); - self.show_confirmation_popup = false; - return; - }; - - let maybe_identifier = if self.recipient_identity_id.trim().is_empty() { - None - } else { - // Attempt to parse from base58 or hex - match Identifier::from_string_try_encodings( - &self.recipient_identity_id, - &[Encoding::Base58, Encoding::Hex], - ) { - Ok(id) => Some(id), - Err(_) => { - self.error_message = Some("Invalid recipient identity format.".into()); - self.status = - MintTokensStatus::ErrorMessage("Invalid recipient identity".into()); - self.show_confirmation_popup = false; - return; - } - } - }; - - ui.label(format!( - "Are you sure you want to mint {} token(s)?", - amount - )); + let msg = format!( + "Are you sure you want to mint {} tokens to {}?", + self.amount.clone().unwrap_or(Amount::new(0, 0)), + self.recipient_identity_id + ); - // If user provided a recipient: - if let Some(ref recipient_id) = maybe_identifier { - ui.label(format!( - "Recipient: {}", - recipient_id.to_string(Encoding::Base58) - )); - } else { - ui.label("No recipient specified; tokens will be minted to default identity."); - } + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Mint", msg) + .confirm_text(Some("Mint")) + .cancel_text(Some("Cancel")) + }); - ui.add_space(10.0); + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, + } + } - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = MintTokensStatus::WaitingForResult(now); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch the actual backend mint action - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::MintTokens { - sending_identity: self.identity_token_info.identity.clone(), - data_contract: Arc::new( - self.identity_token_info.data_contract.contract.clone(), - ), - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - amount: amount.value(), - recipient_id: maybe_identifier, - group_info, - }, - ))); - } + fn confirmation_ok(&mut self) -> AppAction { + if self.amount.is_none() || self.amount == Some(Amount::new(0, 0)) { + self.status = MintTokensStatus::ErrorMessage("Invalid amount".into()); + self.error_message = Some("Invalid amount".into()); + return AppAction::None; + } - // Cancel button - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let parsed_receiver_id = Identifier::from_string_try_encodings( + &self.recipient_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); - if !is_open { - self.show_confirmation_popup = false; + if parsed_receiver_id.is_err() { + self.status = MintTokensStatus::ErrorMessage("Invalid receiver".into()); + self.error_message = Some("Invalid receiver".into()); + return AppAction::None; } - action - } + let receiver_id = parsed_receiver_id.unwrap(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = MintTokensStatus::WaitingForResult(now); + + let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + AppAction::BackendTask(BackendTask::TokenTask(Box::new(TokenTask::MintTokens { + sending_identity: self.identity_token_info.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + recipient_id: Some(receiver_id), + amount: self.amount.clone().unwrap_or(Amount::new(0, 0)).value(), + group_info, + }))) + } /// Renders a simple "Success!" screen after completion fn show_success_screen(&self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; @@ -658,12 +637,21 @@ impl ScreenLike for MintTokensScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + let msg = format!( + "Are you sure you want to mint {} tokens to {}?", + self.amount.clone().unwrap_or(Amount::new(0, 0)), + self.recipient_identity_id + ); + self.confirmation_dialog = Some( + ConfirmationDialog::new("Confirm Mint", msg) + .confirm_text(Some("Mint")) + .cancel_text(Some("Cancel")), + ); } } // If the user pressed "Mint," show a popup - if self.show_confirmation_popup { + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/mint_tokens_screen.rs.bak b/src/ui/tokens/mint_tokens_screen.rs.bak new file mode 100644 index 000000000..ef2401817 --- /dev/null +++ b/src/ui/tokens/mint_tokens_screen.rs.bak @@ -0,0 +1,734 @@ +use super::tokens_screen::IdentityTokenInfo; +use crate::app::AppAction; +use crate::backend_task::BackendTask; +use crate::backend_task::tokens::TokenTask; +use crate::context::AppContext; +use crate::model::amount::Amount; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::styled::island_central_panel; +use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::components::{Component, ComponentResponse}; +use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; +use crate::ui::helpers::{TransactionType, add_identity_key_chooser, render_group_action_text}; +use crate::ui::identities::get_selected_wallet; +use crate::ui::identities::keys::add_key_screen::AddKeyScreen; +use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; +use crate::ui::theme::DashColors; +use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike}; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; +use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::platform::{Identifier, IdentityPublicKey}; +use eframe::egui::{self, Color32, Context, Ui}; +use egui::RichText; +use std::collections::HashSet; +use std::sync::{Arc, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Internal states for the mint process. +#[derive(PartialEq)] +pub enum MintTokensStatus { + NotStarted, + WaitingForResult(u64), // Use seconds or millis + ErrorMessage(String), + Complete, +} + +/// A UI Screen for minting tokens from an existing token contract +pub struct MintTokensScreen { + pub identity_token_info: IdentityTokenInfo, + selected_key: Option, + pub public_note: Option, + group: Option<(GroupContractPosition, Group)>, + is_unilateral_group_member: bool, + pub group_action_id: Option, + known_identities: Vec, + + pub recipient_identity_id: String, + + pub amount: Option, + pub amount_input: Option, + status: MintTokensStatus, + error_message: Option, + + /// Basic references + pub app_context: Arc, + + /// Confirmation popup + confirmation_dialog: Option, + + // If needed for password-based wallet unlocking: + selected_wallet: Option>>, + wallet_password: String, + show_password: bool, +} + +impl MintTokensScreen { + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + let known_identities = app_context + .load_local_qualified_identities() + .expect("Identities not loaded"); + + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); + + let mut error_message = None; + + let group = match identity_token_info + .token_config + .manual_minting_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Minting is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to mint this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to mint this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + let mut is_unilateral_group_member = false; + if group.is_some() { + if let Some((_, group)) = group.clone() { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power { + if your_power >= &group.required_power() { + is_unilateral_group_member = true; + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); + + Self { + identity_token_info, + selected_key: possible_key, + public_note: None, + group, + is_unilateral_group_member, + group_action_id: None, + known_identities, + recipient_identity_id: "".to_string(), + amount: None, + amount_input: None, + status: MintTokensStatus::NotStarted, + error_message, + app_context: app_context.clone(), + confirmation_dialog: None, + selected_wallet, + wallet_password: String::new(), + show_password: false, + } + } + + /// Renders an amount input for the user to specify an amount to mint + fn render_amount_input(&mut self, ui: &mut Ui) { + // Lazy initialization with proper token configuration + let amount_input = self.amount_input.get_or_insert_with(|| { + // Create appropriate Amount based on token configuration + let token_amount = Amount::from_token(&self.identity_token_info, 0); + AmountInput::new(token_amount).with_label("Amount to Mint:") + }); + + // Check if input should be disabled when operation is in progress + let enabled = match self.status { + MintTokensStatus::WaitingForResult(_) | MintTokensStatus::Complete => false, + MintTokensStatus::NotStarted | MintTokensStatus::ErrorMessage(_) => true, + }; + + let response = ui.add_enabled_ui(enabled, |ui| amount_input.show(ui)).inner; + + response.inner.update(&mut self.amount); + // errors are handled inside AmountInput + } + + /// Renders an optional text input for the user to specify a "Recipient Identity" + fn render_recipient_input(&mut self, ui: &mut Ui) { + let _response = ui.add( + IdentitySelector::new( + "mint_recipient_selector", + &mut self.recipient_identity_id, + &self.known_identities, + ) + .width(300.0) + .label("Recipient:") + .exclude(&[self.identity_token_info.identity.identity.id()]), + ); + + // If empty, minted tokens go to the 'issuer' identity (self.identity). + } + + /// Renders a confirm popup with the final "Are you sure?" step + fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + let mut is_open = true; + egui::Window::new("Confirm Mint") + .collapsible(false) + .open(&mut is_open) + .show(ui.ctx(), |ui| { + // Validate user input + let Some(amount) = &self.amount else { + self.error_message = Some("Please enter a valid amount.".into()); + self.status = MintTokensStatus::ErrorMessage("Invalid amount".into()); + self.show_confirmation_popup = false; + return; + }; + + let maybe_identifier = if self.recipient_identity_id.trim().is_empty() { + None + } else { + // Attempt to parse from base58 or hex + match Identifier::from_string_try_encodings( + &self.recipient_identity_id, + &[Encoding::Base58, Encoding::Hex], + ) { + Ok(id) => Some(id), + Err(_) => { + self.error_message = Some("Invalid recipient identity format.".into()); + self.status = + MintTokensStatus::ErrorMessage("Invalid recipient identity".into()); + self.show_confirmation_popup = false; + return; + } + } + }; + + ui.label(format!( + "Are you sure you want to mint {} token(s)?", + amount + )); + + // If user provided a recipient: + if let Some(ref recipient_id) = maybe_identifier { + ui.label(format!( + "Recipient: {}", + recipient_id.to_string(Encoding::Base58) + )); + } else { + ui.label("No recipient specified; tokens will be minted to default identity."); + } + + ui.add_space(10.0); + + // Confirm button + if ui.button("Confirm").clicked() { + self.show_confirmation_popup = false; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = MintTokensStatus::WaitingForResult(now); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Dispatch the actual backend mint action + action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::MintTokens { + sending_identity: self.identity_token_info.identity.clone(), + data_contract: Arc::new( + self.identity_token_info.data_contract.contract.clone(), + ), + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("Expected a key"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + amount: amount.value(), + recipient_id: maybe_identifier, + group_info, + }, + ))); + } + + // Cancel button + if ui.button("Cancel").clicked() { + self.show_confirmation_popup = false; + } + }); + + if !is_open { + self.show_confirmation_popup = false; + } + action + } + + /// Renders a simple "Success!" screen after completion + fn show_success_screen(&self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading("🎉"); + if self.group_action_id.is_some() { + // This mint is already initiated by the group, we are just signing it + ui.heading("Group Mint Signing Successful."); + } else if !self.is_unilateral_group_member && self.group.is_some() { + ui.heading("Group Mint Initiated."); + } else { + ui.heading("Mint Successful."); + } + + ui.add_space(20.0); + + if self.group_action_id.is_some() { + if ui.button("Back to Group Actions").clicked() { + action = AppAction::PopScreenAndRefresh; + } + if ui.button("Back to Tokens").clicked() { + action = AppAction::SetMainScreenThenGoToMainScreen( + RootScreenType::RootScreenMyTokenBalances, + ); + } + } else { + if ui.button("Back to Tokens").clicked() { + action = AppAction::PopScreenAndRefresh; + } + + if !self.is_unilateral_group_member && ui.button("Go to Group Actions").clicked() { + action = AppAction::PopThenAddScreenToMainScreen( + RootScreenType::RootScreenDocumentQuery, + Screen::GroupActionsScreen(GroupActionsScreen::new( + &self.app_context.clone(), + )), + ); + } + } + }); + action + } +} + +impl ScreenLike for MintTokensScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Success => { + if message.contains("Successfully minted tokens") || message == "MintTokens" { + self.status = MintTokensStatus::Complete; + } + } + MessageType::Error => { + self.status = MintTokensStatus::ErrorMessage(message.to_string()); + self.error_message = Some(message.to_string()); + } + MessageType::Info => { + // no-op + } + } + } + + fn refresh(&mut self) { + // If you need to reload local identity data or re-check keys: + if let Ok(all_identities) = self.app_context.load_local_user_identities() { + if let Some(updated_identity) = all_identities + .into_iter() + .find(|id| id.identity.id() == self.identity_token_info.identity.identity.id()) + { + self.identity_token_info.identity = updated_identity; + } + } + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Mint", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Mint", AppAction::None), + ], + vec![], + ); + } + + // Left panel + action |= add_left_panel( + ctx, + &self.app_context, + crate::ui::RootScreenType::RootScreenMyTokenBalances, + ); + + // Subscreen chooser + action |= add_tokens_subscreen_chooser_panel(ctx, &self.app_context); + + let central_panel_action = island_central_panel(ctx, |ui| { + let dark_mode = ui.ctx().style().visuals.dark_mode; + + // If we are in the "Complete" status, just show success screen + if self.status == MintTokensStatus::Complete { + return self.show_success_screen(ui); + } + + ui.heading("Mint Tokens"); + ui.add_space(10.0); + + // Check if user has any auth keys + let has_keys = if self.app_context.is_developer_mode() { + !self + .identity_token_info + .identity + .identity + .public_keys() + .is_empty() + } else { + !self + .identity_token_info + .identity + .available_authentication_keys_with_critical_security_level() + .is_empty() + }; + + if !has_keys { + ui.colored_label( + DashColors::error_color(dark_mode), + format!( + "No authentication keys with CRITICAL security level found for this {} identity.", + self.identity_token_info.identity.identity_type, + ), + ); + ui.add_space(10.0); + + // Show "Add key" or "Check keys" option + let first_key = self + .identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ); + + if let Some(key) = first_key { + if ui.button("Check Keys").clicked() { + action |= AppAction::AddScreen(Screen::KeyInfoScreen(KeyInfoScreen::new( + self.identity_token_info.identity.clone(), + key.clone(), + None, + &self.app_context, + ))); + } + ui.add_space(5.0); + } + + if ui.button("Add key").clicked() { + action |= AppAction::AddScreen(Screen::AddKeyScreen(AddKeyScreen::new( + self.identity_token_info.identity.clone(), + &self.app_context, + ))); + } + } else { + // Possibly handle locked wallet scenario (similar to TransferTokens) + if self.selected_wallet.is_some() { + let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + + if needed_unlock && !just_unlocked { + // Must unlock before we can proceed + return AppAction::None; + } + } + + // 1) Key selection + ui.heading("1. Select the key to sign the Mint transaction"); + ui.add_space(10.0); + + let mut selected_identity = Some(self.identity_token_info.identity.clone()); + add_identity_key_chooser( + ui, + &self.app_context, + std::iter::once(&self.identity_token_info.identity), + &mut selected_identity, + &mut self.selected_key, + TransactionType::TokenAction, + ); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // 2) Amount to mint + ui.heading("2. Amount to mint"); + ui.add_space(5.0); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Mint so you are not allowed to choose the amount.", + ); + ui.add_space(5.0); + ui.label(format!( + "Amount: {}", + self.amount + .as_ref() + .map(|a| a.to_string()) + .unwrap_or_default() + )); + } else { + self.render_amount_input(ui); + } + + if self + .identity_token_info + .token_config + .distribution_rules() + .minting_allow_choosing_destination() + || self.app_context.is_developer_mode() + { + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + if self + .identity_token_info + .token_config + .distribution_rules() + .new_tokens_destination_identity() + .is_some() + { + ui.heading("3. Recipient identity (optional)"); + } else { + ui.heading("3. Recipient identity (required)"); + } + ui.add_space(5.0); + self.render_recipient_input(ui); + } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // Render text input for the public note + ui.heading("4. Public note (optional)"); + ui.add_space(5.0); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Mint so you are not allowed to put a note.", + ); + ui.add_space(5.0); + ui.label(format!( + "Note: {}", + self.public_note.clone().unwrap_or("None".to_string()) + )); + } else { + ui.horizontal(|ui| { + ui.label("Public note (optional):"); + ui.add_space(10.0); + let mut txt = self.public_note.clone().unwrap_or_default(); + if ui + .text_edit_singleline(&mut txt) + .on_hover_text( + "A note about the transaction that can be seen by the public.", + ) + .changed() + { + self.public_note = if !txt.is_empty() { Some(txt) } else { None }; + } + }); + } + + let button_text = render_group_action_text( + ui, + &self.group, + &self.identity_token_info, + "Mint", + &self.group_action_id, + ); + + // Mint button + if self.app_context.is_developer_mode() || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); + + if ui.add(button).clicked() { + self.show_confirmation_popup = true; + } + } + + // If the user pressed "Mint," show a popup + if self.show_confirmation_popup { + action |= self.show_confirmation_popup(ui); + } + + // Show in-progress or error messages + ui.add_space(10.0); + match &self.status { + MintTokensStatus::NotStarted => { + // no-op + } + MintTokensStatus::WaitingForResult(start_time) => { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + let elapsed = now - start_time; + ui.label(format!("Minting... elapsed: {} seconds", elapsed)); + } + MintTokensStatus::ErrorMessage(msg) => { + ui.colored_label( + DashColors::error_color(dark_mode), + format!("Error: {}", msg), + ); + } + MintTokensStatus::Complete => { + // handled above + } + } + } + + AppAction::None + }); + + action |= central_panel_action; + action + } +} + +impl ScreenWithWalletUnlock for MintTokensScreen { + fn selected_wallet_ref(&self) -> &Option>> { + &self.selected_wallet + } + + fn wallet_password_ref(&self) -> &String { + &self.wallet_password + } + + fn wallet_password_mut(&mut self) -> &mut String { + &mut self.wallet_password + } + + fn show_password(&self) -> bool { + self.show_password + } + + fn show_password_mut(&mut self) -> &mut bool { + &mut self.show_password + } + + fn set_error_message(&mut self, error_message: Option) { + self.error_message = error_message; + } + + fn error_message(&self) -> Option<&String> { + self.error_message.as_ref() + } +} diff --git a/src/ui/tokens/pause_tokens_screen.rs b/src/ui/tokens/pause_tokens_screen.rs index f6ed54f7d..95e43b4f8 100644 --- a/src/ui/tokens/pause_tokens_screen.rs +++ b/src/ui/tokens/pause_tokens_screen.rs @@ -5,6 +5,8 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -59,7 +61,7 @@ pub struct PauseTokensScreen { pub app_context: Arc, // Confirmation popup - show_confirmation_popup: bool, + confirmation_dialog: Option, // If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -181,7 +183,7 @@ impl PauseTokensScreen { status: PauseTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -189,69 +191,61 @@ impl PauseTokensScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Pause") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label("Are you sure you want to pause token transfers for this contract?"); - ui.add_space(10.0); + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Pause".to_string(), + "Are you sure you want to pause token transfers for this contract?".to_string(), + ) + }); - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = PauseTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::PauseTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = PauseTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = + Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, }, - group_info, - }, - ))); - } - - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); - - if !is_open { - self.show_confirmation_popup = false; + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + AppAction::BackendTask(BackendTask::TokenTask(Box::new(TokenTask::PauseTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + group_info, + }))) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } fn show_success_screen(&self, ui: &mut Ui) -> AppAction { @@ -491,13 +485,17 @@ impl ScreenLike for PauseTokensScreen { .fill(Color32::from_rgb(0, 128, 255)) .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() && self.confirmation_dialog.is_none() { + self.confirmation_dialog = Some(ConfirmationDialog::new( + "Confirm Pause".to_string(), + "Are you sure you want to pause token transfers for this contract?" + .to_string(), + )); } } - // If user pressed "Pause," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/resume_tokens_screen.rs b/src/ui/tokens/resume_tokens_screen.rs index 5155985ab..70b4b2acd 100644 --- a/src/ui/tokens/resume_tokens_screen.rs +++ b/src/ui/tokens/resume_tokens_screen.rs @@ -5,6 +5,8 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -58,7 +60,7 @@ pub struct ResumeTokensScreen { pub app_context: Arc, // Confirmation popup - show_confirmation_popup: bool, + confirmation_dialog: Option, // If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -180,7 +182,7 @@ impl ResumeTokensScreen { status: ResumeTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -188,69 +190,62 @@ impl ResumeTokensScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Resume") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label("Are you sure you want to resume normal token actions for this contract?"); - ui.add_space(10.0); + let dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Resume".to_string(), + "Are you sure you want to resume normal token actions for this contract?" + .to_string(), + ) + }); - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = ResumeTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::ResumeTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() + match dialog.show(ui).inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = ResumeTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = + Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, }, - group_info, - }, - ))); - } - - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); - - if !is_open { - self.show_confirmation_popup = false; + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + AppAction::BackendTask(BackendTask::TokenTask(Box::new(TokenTask::ResumeTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + group_info, + }))) + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - action } fn show_success_screen(&self, ui: &mut Ui) -> AppAction { @@ -491,13 +486,16 @@ impl ScreenLike for ResumeTokensScreen { .fill(Color32::from_rgb(0, 128, 255)) .corner_radius(3.0); - if ui.add(button).clicked() { - self.show_confirmation_popup = true; + if ui.add(button).clicked() && self.confirmation_dialog.is_none() { + self.confirmation_dialog = Some(ConfirmationDialog::new( + "Confirm Resume".to_string(), + "Are you sure you want to resume normal token actions for this contract?".to_string(), + )); } } - // If user pressed "Resume," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/tokens_screen/data_contract_json_pop_up.rs b/src/ui/tokens/tokens_screen/data_contract_json_pop_up.rs index 00ed9d364..80d5521ac 100644 --- a/src/ui/tokens/tokens_screen/data_contract_json_pop_up.rs +++ b/src/ui/tokens/tokens_screen/data_contract_json_pop_up.rs @@ -1,3 +1,4 @@ +use crate::ui::theme::{ComponentStyles, DashColors, Shape}; use crate::ui::tokens::tokens_screen::TokensScreen; use egui::Ui; @@ -6,18 +7,53 @@ impl TokensScreen { pub(super) fn render_data_contract_json_popup(&mut self, ui: &mut Ui) { if self.show_json_popup { let mut is_open = true; + + // Draw dark overlay behind the dialog for better visibility + let screen_rect = ui.ctx().screen_rect(); + let painter = ui.ctx().layer_painter(egui::LayerId::new( + egui::Order::Background, + egui::Id::new("json_popup_overlay"), + )); + painter.rect_filled( + screen_rect, + 0.0, + egui::Color32::from_rgba_unmultiplied(0, 0, 0, 120), // Semi-transparent black overlay + ); + egui::Window::new("Data Contract JSON") .collapsible(false) .resizable(true) .max_height(600.0) .max_width(800.0) .scroll(true) + .anchor(egui::Align2::CENTER_CENTER, egui::Vec2::ZERO) .open(&mut is_open) + .frame(egui::Frame { + inner_margin: egui::Margin::same(16), + outer_margin: egui::Margin::same(0), + corner_radius: egui::CornerRadius::same(8), + shadow: egui::epaint::Shadow { + offset: [0, 8], + blur: 16, + spread: 0, + color: egui::Color32::from_rgba_unmultiplied(0, 0, 0, 100), + }, + fill: ui.style().visuals.window_fill, + stroke: egui::Stroke::new( + 1.0, + egui::Color32::from_rgba_unmultiplied(255, 255, 255, 30), + ), + }) .show(ui.ctx(), |ui| { // Display the JSON in a multiline text box - ui.add_space(4.0); - ui.label("Below is the data contract JSON:"); - ui.add_space(4.0); + let dark_mode = ui.ctx().style().visuals.dark_mode; + ui.add_space(10.0); + ui.label( + egui::RichText::new("Below is the data contract JSON:") + .strong() + .color(DashColors::text_primary(dark_mode)), + ); + ui.add_space(10.0); egui::Resize::default() .id_salt("json_resize_area_for_contract") @@ -32,12 +68,29 @@ impl TokensScreen { }); }); - ui.add_space(10.0); + ui.add_space(20.0); + + // Close button styled like ConfirmationDialog + ui.horizontal(|ui| { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let close_button = egui::Button::new( + egui::RichText::new("Close") + .color(ComponentStyles::secondary_button_text()), + ) + .fill(ComponentStyles::secondary_button_fill()) + .stroke(ComponentStyles::secondary_button_stroke()) + .corner_radius(egui::CornerRadius::same(Shape::RADIUS_SM)) + .min_size(egui::Vec2::new(80.0, 32.0)); - // A button to close - if ui.button("Close").clicked() { - self.show_json_popup = false; - } + if ui + .add(close_button) + .on_hover_cursor(egui::CursorIcon::PointingHand) + .clicked() + { + self.show_json_popup = false; + } + }); + }); }); // If the user closed the window via the "x" in the corner diff --git a/src/ui/tokens/tokens_screen/mod.rs b/src/ui/tokens/tokens_screen/mod.rs index 9a85b6072..05b0d6946 100644 --- a/src/ui/tokens/tokens_screen/mod.rs +++ b/src/ui/tokens/tokens_screen/mod.rs @@ -60,6 +60,7 @@ use crate::model::amount::Amount; use crate::model::qualified_identity::{IdentityType, QualifiedIdentity}; use crate::model::wallet::Wallet; use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; @@ -981,8 +982,10 @@ pub struct TokensScreen { // Remove token confirm_remove_identity_token_balance_popup: bool, identity_token_balance_to_remove: Option, + remove_identity_token_balance_confirmation_dialog: Option, confirm_remove_token_popup: bool, token_to_remove: Option, + remove_token_confirmation_dialog: Option, // Reward explanations reward_explanations: IndexMap, @@ -1013,6 +1016,7 @@ pub struct TokensScreen { start_as_paused_input: bool, main_control_group_input: String, show_token_creator_confirmation_popup: bool, + token_creator_confirmation_dialog: Option, token_creator_status: TokenCreatorStatus, token_creator_error_message: Option, show_advanced_keeps_history: bool, @@ -1334,8 +1338,10 @@ impl TokensScreen { // Remove token confirm_remove_identity_token_balance_popup: false, identity_token_balance_to_remove: None, + remove_identity_token_balance_confirmation_dialog: None, confirm_remove_token_popup: false, token_to_remove: None, + remove_token_confirmation_dialog: None, // Reward explanations reward_explanations: IndexMap::new(), @@ -1351,6 +1357,7 @@ impl TokensScreen { wallet_password: String::new(), show_password: false, show_token_creator_confirmation_popup: false, + token_creator_confirmation_dialog: None, token_creator_status: TokenCreatorStatus::NotStarted, token_creator_error_message: None, token_names_input: vec![( @@ -2299,20 +2306,28 @@ impl TokensScreen { } }; - let mut is_open = true; - - egui::Window::new("Confirm Stop Tracking Balance") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label(format!( + // Lazy initialization of the confirmation dialog + let confirmation_dialog = self + .remove_identity_token_balance_confirmation_dialog + .get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Stop Tracking Balance", + format!( "Are you sure you want to stop tracking the token \"{}\" for identity \"{}\"?", token_to_remove.token_alias, token_to_remove.identity_id.to_string(Encoding::Base58) - )); + ), + ) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + }); + + // Show the dialog and handle the response + let response = confirmation_dialog.show(ui).inner; - // Confirm button - if ui.button("Confirm").clicked() { + if let Some(status) = response.dialog_response { + match status { + ConfirmationStatus::Confirmed => { if let Err(e) = self .app_context .remove_token_balance(token_to_remove.token_id, token_to_remove.identity_id) @@ -2322,26 +2337,19 @@ impl TokensScreen { MessageType::Error, Utc::now(), )); - self.confirm_remove_identity_token_balance_popup = false; - self.identity_token_balance_to_remove = None; } else { - self.confirm_remove_identity_token_balance_popup = false; - self.identity_token_balance_to_remove = None; self.refresh(); - }; + } + self.confirm_remove_identity_token_balance_popup = false; + self.identity_token_balance_to_remove = None; + self.remove_identity_token_balance_confirmation_dialog = None; } - - // Cancel button - if ui.button("Cancel").clicked() { + ConfirmationStatus::Canceled => { self.confirm_remove_identity_token_balance_popup = false; self.identity_token_balance_to_remove = None; + self.remove_identity_token_balance_confirmation_dialog = None; } - }); - - // If user closes the popup window (the [x] button), also reset state - if !is_open { - self.confirm_remove_identity_token_balance_popup = false; - self.identity_token_balance_to_remove = None; + } } } @@ -2362,48 +2370,48 @@ impl TokensScreen { .map(|t| t.token_name.clone()) .unwrap_or_else(|| token_to_remove.to_string(Encoding::Base58)); - let mut is_open = true; - - egui::Window::new("Confirm Remove Token") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label(format!( + // Lazy initialization of the confirmation dialog + let confirmation_dialog = self.remove_token_confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new( + "Confirm Remove Token", + format!( "Are you sure you want to stop tracking the token \"{}\"? You can re-add it later. Your actual token balance will not change with this action.", token_name, - )); - - // Confirm button - if ui.button("Confirm").clicked() { - if let Err(e) = self.app_context.db.remove_token( - &token_to_remove, - &self.app_context, - ) { + ), + ) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + }); + + // Show the dialog and handle the response + let response = confirmation_dialog.show(ui).inner; + + if let Some(status) = response.dialog_response { + match status { + ConfirmationStatus::Confirmed => { + if let Err(e) = self + .app_context + .db + .remove_token(&token_to_remove, &self.app_context) + { self.backend_message = Some(( format!("Error removing token balance: {}", e), MessageType::Error, Utc::now(), )); - self.confirm_remove_token_popup = false; - self.token_to_remove = None; } else { - self.confirm_remove_token_popup = false; - self.token_to_remove = None; self.refresh(); } + self.confirm_remove_token_popup = false; + self.token_to_remove = None; + self.remove_token_confirmation_dialog = None; } - - // Cancel button - if ui.button("Cancel").clicked() { + ConfirmationStatus::Canceled => { self.confirm_remove_token_popup = false; self.token_to_remove = None; + self.remove_token_confirmation_dialog = None; } - }); - - // If user closes the popup window (the [x] button), also reset state - if !is_open { - self.confirm_remove_token_popup = false; - self.token_to_remove = None; + } } } diff --git a/src/ui/tokens/tokens_screen/token_creator.rs b/src/ui/tokens/tokens_screen/token_creator.rs index e10ef0e25..aa2887efa 100644 --- a/src/ui/tokens/tokens_screen/token_creator.rs +++ b/src/ui/tokens/tokens_screen/token_creator.rs @@ -17,6 +17,8 @@ use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::ui::components::styled::{StyledCheckbox}; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::Component; use crate::ui::helpers::{add_identity_key_chooser, TransactionType}; use crate::ui::tokens::tokens_screen::{TokenBuildArgs, TokenCreatorStatus, TokenNameLanguage, TokensScreen, ChangeControlRulesUI}; @@ -1009,65 +1011,66 @@ impl TokensScreen { /// Shows a popup "Are you sure?" for creating the token contract fn render_token_creator_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; - let mut is_open = true; - - egui::Window::new("Confirm Token Contract Registration") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - ui.label( - "Are you sure you want to register a new token contract with these settings?\n", - ); - let base_supply_display = self - .base_supply_amount - .as_ref() - .map(|amount| amount.to_string_opts(true, false)) - .unwrap_or_else(|| "0".to_string()); - let max_supply_display = self - .max_supply_amount - .as_ref() - .filter(|amount| amount.value() > 0) - .map(|amount| amount.to_string_opts(true, false)) - .unwrap_or_else(|| "None".to_string()); - ui.label(format!( - "Name: {}\nBase Supply: {}\nMax Supply: {}", - self.token_names_input[0].0, base_supply_display, max_supply_display, - )); - - ui.add_space(10.0); - ui.label(format!( - "Estimated cost to register this token is {} Dash", - self.estimate_registration_cost() as f64 / 100_000_000_000.0 - )); + // Prepare the confirmation message + let mut confirmation_message = + "Are you sure you want to register a new token contract with these settings?\n\n" + .to_string(); + let base_supply_display = self + .base_supply_amount + .as_ref() + .map(|amount| amount.to_string_opts(true, false)) + .unwrap_or_else(|| "0".to_string()); + let max_supply_display = self + .max_supply_amount + .as_ref() + .filter(|amount| amount.value() > 0) + .map(|amount| amount.to_string_opts(true, false)) + .unwrap_or_else(|| "None".to_string()); + + confirmation_message.push_str(&format!( + "Name: {}\nBase Supply: {}\nMax Supply: {}\n\n", + self.token_names_input[0].0, base_supply_display, max_supply_display, + )); + + confirmation_message.push_str(&format!( + "Estimated cost to register this token is {} Dash", + self.estimate_registration_cost() as f64 / 100_000_000_000.0 + )); + + // Check if marketplace is locked to NotTradeable forever + let mut is_danger_mode = false; + if let Some(args) = &self.cached_build_args { + let is_not_tradeable = args.marketplace_trade_mode == 0; + let marketplace_rules_locked = matches!( + args.marketplace_rules, + ChangeControlRules::V0(ChangeControlRulesV0 { + authorized_to_make_change: AuthorizedActionTakers::NoOne, + admin_action_takers: AuthorizedActionTakers::NoOne, + .. + }) + ); - ui.add_space(10.0); - - // Check if marketplace is locked to NotTradeable forever - if let Some(args) = &self.cached_build_args { - let is_not_tradeable = args.marketplace_trade_mode == 0; - let marketplace_rules_locked = matches!( - args.marketplace_rules, - ChangeControlRules::V0(ChangeControlRulesV0 { - authorized_to_make_change: AuthorizedActionTakers::NoOne, - admin_action_takers: AuthorizedActionTakers::NoOne, - .. - }) - ); + if is_not_tradeable && marketplace_rules_locked { + confirmation_message.push_str("\n\nWARNING: This token will be permanently set to NotTradeable and can NEVER be made tradeable in the future!"); + is_danger_mode = true; + } + } - if is_not_tradeable && marketplace_rules_locked { - ui.colored_label( - Color32::DARK_RED, - "WARNING: This token will be permanently set to NotTradeable and can NEVER be made tradeable in the future!" - ); - ui.add_space(10.0); - } - } + // Always create a fresh confirmation dialog to ensure current state is reflected + let confirmation_dialog = self.token_creator_confirmation_dialog.insert( + ConfirmationDialog::new("Confirm Token Contract Registration", confirmation_message) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + .danger_mode(is_danger_mode), + ); - ui.add_space(10.0); + // Show the dialog and handle the response + let response = confirmation_dialog.show(ui).inner; - // Confirm - if ui.button("Confirm").clicked() { + if let Some(status) = response.dialog_response { + match status { + ConfirmationStatus::Confirmed => { let args = match &self.cached_build_args { Some(args) => args.clone(), None => { @@ -1077,8 +1080,8 @@ impl TokensScreen { Err(err) => { self.token_creator_error_message = Some(err); self.show_token_creator_confirmation_popup = false; - action = AppAction::None; - return; + self.token_creator_confirmation_dialog = None; + return AppAction::None; } } } @@ -1126,17 +1129,15 @@ impl TokensScreen { self.show_token_creator_confirmation_popup = false; let now = Utc::now().timestamp() as u64; self.token_creator_status = TokenCreatorStatus::WaitingForResult(now); + self.show_token_creator_confirmation_popup = false; + self.token_creator_confirmation_dialog = None; } - - // Cancel - if ui.button("Cancel").clicked() { + ConfirmationStatus::Canceled => { self.show_token_creator_confirmation_popup = false; + self.token_creator_confirmation_dialog = None; action = AppAction::None; } - }); - - if !is_open { - self.show_token_creator_confirmation_popup = false; + } } action diff --git a/src/ui/tokens/transfer_tokens_screen.rs b/src/ui/tokens/transfer_tokens_screen.rs index 3426ce36e..41effdd87 100644 --- a/src/ui/tokens/transfer_tokens_screen.rs +++ b/src/ui/tokens/transfer_tokens_screen.rs @@ -1,4 +1,4 @@ -use crate::app::{AppAction, BackendTasksExecutionMode}; +use crate::app::AppAction; use crate::backend_task::BackendTask; use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; @@ -6,13 +6,14 @@ use crate::model::amount::Amount; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::component_trait::{Component, ComponentResponse}; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; use crate::ui::components::top_panel::add_top_panel; use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; -use crate::ui::components::{Component, ComponentResponse}; use crate::ui::helpers::{TransactionType, add_identity_key_chooser}; use crate::ui::identities::keys::add_key_screen::AddKeyScreen; use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; @@ -20,7 +21,6 @@ use crate::ui::theme::DashColors; use crate::ui::{MessageType, Screen, ScreenLike}; use dash_sdk::dpp::identity::accessors::IdentityGettersV0; use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; -use dash_sdk::dpp::platform_value::string_encoding::Encoding; use dash_sdk::dpp::prelude::TimestampMillis; use dash_sdk::platform::{Identifier, IdentityPublicKey}; use eframe::egui::{self, Context, Ui}; @@ -53,7 +53,7 @@ pub struct TransferTokensScreen { transfer_tokens_status: TransferTokensStatus, max_amount: Amount, pub app_context: Arc, - confirmation_popup: bool, + confirmation_dialog: Option, selected_wallet: Option>>, wallet_password: String, show_password: bool, @@ -99,7 +99,7 @@ impl TransferTokensScreen { transfer_tokens_status: TransferTokensStatus::NotStarted, max_amount, app_context: app_context.clone(), - confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -159,91 +159,79 @@ impl TransferTokensScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut app_action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Transfer") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - let identifier = if self.receiver_identity_id.is_empty() { - self.transfer_tokens_status = - TransferTokensStatus::ErrorMessage("Invalid identifier".to_string()); - self.confirmation_popup = false; - return; - } else { - match Identifier::from_string_try_encodings( - &self.receiver_identity_id, - &[Encoding::Base58, Encoding::Hex], - ) { - Ok(identifier) => identifier, - Err(_) => { - self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( - "Invalid identifier".to_string(), - ); - self.confirmation_popup = false; - return; - } - } - }; - - if self.selected_key.is_none() { - self.transfer_tokens_status = - TransferTokensStatus::ErrorMessage("No selected key".to_string()); - self.confirmation_popup = false; - return; - }; - - ui.label(format!( - "Are you sure you want to transfer {} {} to {}?", - self.amount.as_ref().expect("Amount should be set"), - self.identity_token_balance.token_alias, - self.receiver_identity_id - )); - - if ui.button("Confirm").clicked() { - self.confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.transfer_tokens_status = TransferTokensStatus::WaitingForResult(now); - let data_contract = Arc::new( - self.app_context - .get_unqualified_contract_by_id( - &self.identity_token_balance.data_contract_id, - ) - .expect("Contracts not loaded") - .expect("Data contract not found"), - ); - app_action |= AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::TransferTokens { - sending_identity: self.identity.clone(), - recipient_id: identifier, - amount: { - // Use the amount value directly - self.amount.as_ref().expect("Amount should be set").value() - }, - data_contract, - token_position: self.identity_token_balance.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: self.public_note.clone(), - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } - if ui.button("Cancel").clicked() { - self.confirmation_popup = false; - } - }); - if !is_open { - self.confirmation_popup = false; + let msg = format!( + "Are you sure you want to transfer {} tokens to {}?", + self.amount.clone().unwrap_or(Amount::new(0, 0)), + self.receiver_identity_id + ); + + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Transfer", msg) + .confirm_text(Some("Transfer")) + .cancel_text(Some("Cancel")) + }); + + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, } - app_action } + fn confirmation_ok(&mut self) -> AppAction { + if self.amount.is_none() || self.amount == Some(Amount::new(0, 0)) { + self.transfer_tokens_status = + TransferTokensStatus::ErrorMessage("Invalid amount".into()); + return AppAction::None; + } + + let parsed_receiver_id = Identifier::from_string_try_encodings( + &self.receiver_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + + if parsed_receiver_id.is_err() { + self.transfer_tokens_status = + TransferTokensStatus::ErrorMessage("Invalid receiver".into()); + return AppAction::None; + } + + let receiver_id = parsed_receiver_id.unwrap(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.transfer_tokens_status = TransferTokensStatus::WaitingForResult(now); + + let data_contract = Arc::new( + self.app_context + .get_unqualified_contract_by_id(&self.identity_token_balance.data_contract_id) + .expect("Failed to get data contract") + .expect("Data contract not found"), + ); + + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::TransferTokens { + sending_identity: self.identity.clone(), + recipient_id: receiver_id, + amount: self.amount.clone().unwrap_or(Amount::new(0, 0)).value(), + data_contract, + token_position: self.identity_token_balance.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: self.public_note.clone(), + }, + ))) + } pub fn show_success(&self, ui: &mut Ui) -> AppAction { let mut action = AppAction::None; @@ -481,11 +469,20 @@ impl ScreenLike for TransferTokensScreen { "Amount must be greater than zero".to_string(), ); } else { - self.confirmation_popup = true; + let msg = format!( + "Are you sure you want to transfer {} tokens to {}?", + self.amount.clone().unwrap_or(Amount::new(0, 0)), + self.receiver_identity_id + ); + self.confirmation_dialog = Some( + ConfirmationDialog::new("Confirm Transfer", msg) + .confirm_text(Some("Transfer")) + .cancel_text(Some("Cancel")), + ); } } - if self.confirmation_popup { + if self.confirmation_dialog.is_some() { return self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/transfer_tokens_screen.rs.bak b/src/ui/tokens/transfer_tokens_screen.rs.bak new file mode 100644 index 000000000..6af8044fc --- /dev/null +++ b/src/ui/tokens/transfer_tokens_screen.rs.bak @@ -0,0 +1,587 @@ +use crate::app::{AppAction, BackendTasksExecutionMode}; +use crate::backend_task::BackendTask; +use crate::backend_task::tokens::TokenTask; +use crate::context::AppContext; +use crate::model::amount::Amount; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::model::wallet::Wallet; +use crate::ui::components::amount_input::AmountInput; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::styled::island_central_panel; +use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::components::{Component, ComponentResponse}; +use crate::ui::helpers::{TransactionType, add_identity_key_chooser}; +use crate::ui::identities::keys::add_key_screen::AddKeyScreen; +use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; +use crate::ui::theme::DashColors; +use crate::ui::{MessageType, Screen, ScreenLike}; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; +use dash_sdk::dpp::platform_value::string_encoding::Encoding; +use dash_sdk::dpp::prelude::TimestampMillis; +use dash_sdk::platform::{Identifier, IdentityPublicKey}; +use eframe::egui::{self, Context, Ui}; +use egui::{Color32, RichText}; +use std::collections::HashSet; +use std::sync::{Arc, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use crate::ui::identities::get_selected_wallet; + +use super::tokens_screen::IdentityTokenBalance; + +#[derive(PartialEq)] +pub enum TransferTokensStatus { + NotStarted, + WaitingForResult(TimestampMillis), + ErrorMessage(String), + Complete, +} + +pub struct TransferTokensScreen { + pub identity: QualifiedIdentity, + pub identity_token_balance: IdentityTokenBalance, + known_identities: Vec, + selected_key: Option, + pub public_note: Option, + pub receiver_identity_id: String, + pub amount: Option, + pub amount_input: Option, + transfer_tokens_status: TransferTokensStatus, + max_amount: Amount, + pub app_context: Arc, + confirmation_dialog: Option, + selected_wallet: Option>>, + wallet_password: String, + show_password: bool, +} + +impl TransferTokensScreen { + pub fn new( + identity_token_balance: IdentityTokenBalance, + app_context: &Arc, + ) -> Self { + let known_identities = app_context + .load_local_qualified_identities() + .expect("Identities not loaded"); + + let identity = known_identities + .iter() + .find(|identity| identity.identity.id() == identity_token_balance.identity_id) + .expect("Identity not found") + .clone(); + let max_amount = Amount::from(&identity_token_balance); + let identity_clone = identity.identity.clone(); + let selected_key = identity_clone.get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ); + let mut error_message = None; + let selected_wallet = + get_selected_wallet(&identity, None, selected_key, &mut error_message); + + let amount = Some(Amount::from(&identity_token_balance).with_value(0)); + + Self { + identity, + identity_token_balance, + known_identities, + selected_key: selected_key.cloned(), + public_note: None, + receiver_identity_id: String::new(), + amount, + amount_input: None, + transfer_tokens_status: TransferTokensStatus::NotStarted, + max_amount, + app_context: app_context.clone(), + confirmation_dialog: None, + selected_wallet, + wallet_password: String::new(), + show_password: false, + } + } + + fn render_amount_input(&mut self, ui: &mut Ui) { + ui.label(format!("Available balance: {}", self.max_amount)); + ui.add_space(5.0); + + // Lazy initialization with proper decimal places + let amount_input = match self.amount_input.as_mut() { + Some(input) => input, + _ => { + self.amount_input = Some( + AmountInput::new( + self.amount + .as_ref() + .unwrap_or(&Amount::from(&self.identity_token_balance)), + ) + .with_label("Amount:") + .with_max_button(true), + ); + + self.amount_input + .as_mut() + .expect("AmountInput should be initialized above") + } + }; + + // Check if input should be disabled when operation is in progress + let enabled = match self.transfer_tokens_status { + TransferTokensStatus::WaitingForResult(_) | TransferTokensStatus::Complete => false, + TransferTokensStatus::NotStarted | TransferTokensStatus::ErrorMessage(_) => { + amount_input.set_max_amount(Some(self.max_amount.value())); + true + } + }; + + let response = ui.add_enabled_ui(enabled, |ui| amount_input.show(ui)).inner; + + response.inner.update(&mut self.amount); + // errors are handled inside AmountInput + } + + fn render_to_identity_input(&mut self, ui: &mut Ui) { + let _response = ui.add( + IdentitySelector::new( + "transfer_recipient_selector", + &mut self.receiver_identity_id, + &self.known_identities, + ) + .width(300.0) + .label("Recipient:") + .exclude(&[self.identity.identity.id()]), + ); + } + + fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { + let msg = format!( + "Are you sure you want to transfer {} tokens to {}?", + self.amount.unwrap_or(0), + self.receiver_identity_id + ); + + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Transfer", msg) + .confirm_text(Some("Transfer")) + .cancel_text(Some("Cancel")) + }); + + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + }, + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + }, + None => AppAction::None, + } + } + + fn confirmation_ok(&mut self) -> AppAction { + if self.amount.is_none() || self.amount == Some(0) { + self.status = TransferTokensStatus::ErrorMessage("Invalid amount".into()); + self.error_message = Some("Invalid amount".into()); + return AppAction::None; + } + + let parsed_receiver_id = Identifier::from_string_try_encodings( + &self.receiver_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + + if parsed_receiver_id.is_err() { + self.status = TransferTokensStatus::ErrorMessage("Invalid receiver".into()); + self.error_message = Some("Invalid receiver".into()); + return AppAction::None; + } + + let receiver_id = parsed_receiver_id.unwrap(); + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = TransferTokensStatus::WaitingForResult(now); + + let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::TransferTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + receiver_identity: receiver_id, + amount: self.amount.unwrap_or(0), + group_info, + }, + ))) + } + pub fn show_success(&self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + + // Center the content vertically and horizontally + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading("🎉"); + ui.heading("Success!"); + + ui.add_space(20.0); + + // Display the "Back to Identities" button + if ui.button("Back to Tokens").clicked() { + // Handle navigation back to the identities screen + action |= AppAction::PopScreenAndRefresh; + } + }); + + action + } +} + +impl ScreenLike for TransferTokensScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Success => { + if message == "TransferTokens" { + self.transfer_tokens_status = TransferTokensStatus::Complete; + } + } + MessageType::Info => {} + MessageType::Error => { + // It's not great because the error message can be coming from somewhere else if there are other processes happening + self.transfer_tokens_status = + TransferTokensStatus::ErrorMessage(message.to_string()); + } + } + } + + fn refresh(&mut self) { + // Refresh the identity because there might be new keys + self.identity = self + .app_context + .load_local_qualified_identities() + .unwrap() + .into_iter() + .find(|identity| identity.identity.id() == self.identity.identity.id()) + .unwrap(); + let token_balances = self + .app_context + .db + .get_identity_token_balances(&self.app_context) + .expect("Token balances not loaded"); + self.max_amount = token_balances + .values() + .find(|balance| balance.identity_id == self.identity.identity.id()) + .map(Amount::from) + .unwrap_or_default(); + } + + /// Renders the UI components for the withdrawal screen + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + ( + &self.identity_token_balance.token_alias, + AppAction::PopScreen, + ), + ("Transfer", AppAction::None), + ], + vec![], + ); + + // Left panel + action |= add_left_panel( + ctx, + &self.app_context, + crate::ui::RootScreenType::RootScreenMyTokenBalances, + ); + + // Subscreen chooser + action |= add_tokens_subscreen_chooser_panel(ctx, &self.app_context); + + let central_panel_action = island_central_panel(ctx, |ui| { + let dark_mode = ui.ctx().style().visuals.dark_mode; + + // Show the success screen if the transfer was successful + if self.transfer_tokens_status == TransferTokensStatus::Complete { + return self.show_success(ui); + } + + ui.heading(format!( + "Transfer {}", + self.identity_token_balance.token_alias + )); + ui.add_space(10.0); + + let has_keys = if self.app_context.is_developer_mode() { + !self.identity.identity.public_keys().is_empty() + } else { + !self + .identity + .available_authentication_keys_with_critical_security_level() + .is_empty() + }; + + if !has_keys { + ui.colored_label( + DashColors::error_color(dark_mode), + format!( + "You do not have any authentication keys with CRITICAL security level loaded for this {} identity.", + self.identity.identity_type + ), + ); + ui.add_space(10.0); + + let key = self.identity.identity.get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ); + + if let Some(key) = key { + if ui.button("Check Keys").clicked() { + return AppAction::AddScreen(Screen::KeyInfoScreen(KeyInfoScreen::new( + self.identity.clone(), + key.clone(), + None, + &self.app_context, + ))); + } + ui.add_space(5.0); + } + + if ui.button("Add key").clicked() { + return AppAction::AddScreen(Screen::AddKeyScreen(AddKeyScreen::new( + self.identity.clone(), + &self.app_context, + ))); + } + } else { + if self.selected_wallet.is_some() { + let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + + if needed_unlock && !just_unlocked { + return AppAction::None; + } + } + + // Select the key to sign with + ui.heading("1. Select the key to sign the transaction with"); + ui.add_space(10.0); + + let mut selected_identity = Some(self.identity.clone()); + add_identity_key_chooser( + ui, + &self.app_context, + std::iter::once(&self.identity), + &mut selected_identity, + &mut self.selected_key, + TransactionType::TokenTransfer, + ); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // Input the amount to transfer + ui.heading("2. Input the amount to transfer"); + ui.add_space(5.0); + + self.render_amount_input(ui); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // Input the ID of the identity to transfer to + ui.heading("3. ID of the identity to transfer to"); + ui.add_space(5.0); + self.render_to_identity_input(ui); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // Render text input for the public note + ui.heading("4. Public note (optional)"); + ui.add_space(5.0); + ui.horizontal(|ui| { + ui.label("Public note (optional):"); + ui.add_space(10.0); + let mut txt = self.public_note.clone().unwrap_or_default(); + if ui + .text_edit_singleline(&mut txt) + .on_hover_text( + "A note about the transaction that can be seen by the public.", + ) + .changed() + { + self.public_note = Some(txt); + } + }); + ui.add_space(10.0); + + // Transfer button + + let ready = self.amount.is_some() + && !self.receiver_identity_id.is_empty() + && self.selected_key.is_some(); + let mut new_style = (**ui.style()).clone(); + new_style.spacing.button_padding = egui::vec2(10.0, 5.0); + ui.set_style(new_style); + let button = egui::Button::new(RichText::new("Transfer").color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .frame(true) + .corner_radius(3.0); + if ui + .add_enabled(ready, button) + .on_disabled_hover_text("Please ensure all fields are filled correctly") + .clicked() + { + // Use the amount value directly since it's already parsed + if self.amount.as_ref().is_some_and(|v| v > &self.max_amount) { + self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( + "Amount exceeds available balance".to_string(), + ); + } else if self.amount.as_ref().is_none_or(|a| a.value() == 0) { + self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( + "Amount must be greater than zero".to_string(), + ); + } else { + self.confirmation_popup = true; + } + } + + if self.confirmation_popup { + return self.show_confirmation_popup(ui); + } + + // Handle transfer status messages + ui.add_space(5.0); + match &self.transfer_tokens_status { + TransferTokensStatus::NotStarted => { + // Do nothing + } + TransferTokensStatus::WaitingForResult(start_time) => { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + let elapsed_seconds = now - start_time; + + let display_time = if elapsed_seconds < 60 { + format!( + "{} second{}", + elapsed_seconds, + if elapsed_seconds == 1 { "" } else { "s" } + ) + } else { + let minutes = elapsed_seconds / 60; + let seconds = elapsed_seconds % 60; + format!( + "{} minute{} and {} second{}", + minutes, + if minutes == 1 { "" } else { "s" }, + seconds, + if seconds == 1 { "" } else { "s" } + ) + }; + + ui.label(format!( + "Transferring... Time taken so far: {}", + display_time + )); + } + TransferTokensStatus::ErrorMessage(msg) => { + ui.colored_label( + DashColors::error_color(dark_mode), + format!("Error: {}", msg), + ); + } + TransferTokensStatus::Complete => { + // Handled above + } + } + } + + AppAction::None + }); + action |= central_panel_action; + action + } +} + +impl ScreenWithWalletUnlock for TransferTokensScreen { + fn selected_wallet_ref(&self) -> &Option>> { + &self.selected_wallet + } + + fn wallet_password_ref(&self) -> &String { + &self.wallet_password + } + + fn wallet_password_mut(&mut self) -> &mut String { + &mut self.wallet_password + } + + fn show_password(&self) -> bool { + self.show_password + } + + fn show_password_mut(&mut self) -> &mut bool { + &mut self.show_password + } + + fn set_error_message(&mut self, error_message: Option) { + if let Some(error_message) = error_message { + self.transfer_tokens_status = TransferTokensStatus::ErrorMessage(error_message); + } + } + + fn error_message(&self) -> Option<&String> { + if let TransferTokensStatus::ErrorMessage(error_message) = &self.transfer_tokens_status { + Some(error_message) + } else { + None + } + } +} diff --git a/src/ui/tokens/unfreeze_tokens_screen.rs b/src/ui/tokens/unfreeze_tokens_screen.rs index 9f0d12138..7e29ae721 100644 --- a/src/ui/tokens/unfreeze_tokens_screen.rs +++ b/src/ui/tokens/unfreeze_tokens_screen.rs @@ -5,6 +5,8 @@ use crate::backend_task::tokens::TokenTask; use crate::context::AppContext; use crate::model::qualified_identity::QualifiedIdentity; use crate::model::wallet::Wallet; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; use crate::ui::components::identity_selector::IdentitySelector; use crate::ui::components::left_panel::add_left_panel; use crate::ui::components::styled::island_central_panel; @@ -68,8 +70,8 @@ pub struct UnfreezeTokensScreen { // Basic references pub app_context: Arc, - // Confirmation popup - show_confirmation_popup: bool, + // Confirmation dialog + confirmation_dialog: Option, // If password-based wallet unlocking is needed selected_wallet: Option>>, @@ -197,7 +199,7 @@ impl UnfreezeTokensScreen { status: UnfreezeTokensStatus::NotStarted, error_message, app_context: app_context.clone(), - show_confirmation_popup: false, + confirmation_dialog: None, selected_wallet, wallet_password: String::new(), show_password: false, @@ -218,92 +220,88 @@ impl UnfreezeTokensScreen { } fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Unfreeze") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Validate user input - let parsed = Identifier::from_string_try_encodings( - &self.unfreeze_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - if parsed.is_err() { - self.error_message = Some("Please enter a valid identity ID.".into()); - self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); - self.show_confirmation_popup = false; - return; - } - let unfreeze_id = parsed.unwrap(); - - ui.label(format!( - "Are you sure you want to unfreeze identity {}?", - self.unfreeze_identity_id - )); - - ui.add_space(10.0); + let msg = format!( + "Are you sure you want to unfreeze identity {}?", + self.unfreeze_identity_id + ); - // Confirm - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = UnfreezeTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch to backend - action |= AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::UnfreezeTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - unfreeze_identity: unfreeze_id, - group_info, - }, - ))); - } + let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { + ConfirmationDialog::new("Confirm Unfreeze", msg) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")) + }); - // Cancel - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); + let response = confirmation_dialog.show(ui); + match response.inner.dialog_response { + Some(ConfirmationStatus::Confirmed) => { + self.confirmation_dialog = None; + self.confirmation_ok() + } + Some(ConfirmationStatus::Canceled) => { + self.confirmation_dialog = None; + AppAction::None + } + None => AppAction::None, + } + } - if !is_open { - self.show_confirmation_popup = false; + fn confirmation_ok(&mut self) -> AppAction { + // Validate user input + let parsed = Identifier::from_string_try_encodings( + &self.unfreeze_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + if parsed.is_err() { + self.error_message = Some("Please enter a valid identity ID.".into()); + self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); + return AppAction::None; } - action + let unfreeze_id = parsed.unwrap(); + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = UnfreezeTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Dispatch to backend + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::UnfreezeTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + unfreeze_identity: unfreeze_id, + group_info, + }, + ))) } fn show_success_screen(&self, ui: &mut Ui) -> AppAction { @@ -564,12 +562,21 @@ impl ScreenLike for UnfreezeTokensScreen { .corner_radius(3.0); if ui.add(button).clicked() { - self.show_confirmation_popup = true; + // Initialize confirmation dialog when button is clicked + let msg = format!( + "Are you sure you want to unfreeze identity {}?", + self.unfreeze_identity_id + ); + self.confirmation_dialog = Some( + ConfirmationDialog::new("Confirm Unfreeze", msg) + .confirm_text(Some("Confirm")) + .cancel_text(Some("Cancel")), + ); } } - // If user pressed "Unfreeze," show popup - if self.show_confirmation_popup { + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { action |= self.show_confirmation_popup(ui); } diff --git a/src/ui/tokens/unfreeze_tokens_screen.rs.bak b/src/ui/tokens/unfreeze_tokens_screen.rs.bak new file mode 100644 index 000000000..4b312a2cd --- /dev/null +++ b/src/ui/tokens/unfreeze_tokens_screen.rs.bak @@ -0,0 +1,694 @@ +use super::tokens_screen::IdentityTokenInfo; +use crate::app::AppAction; +use crate::backend_task::BackendTask; +use crate::backend_task::tokens::TokenTask; +use crate::context::AppContext; +use crate::model::qualified_identity::QualifiedIdentity; +use crate::model::wallet::Wallet; +use crate::ui::components::component_trait::Component; +use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; +use crate::ui::components::identity_selector::IdentitySelector; +use crate::ui::components::left_panel::add_left_panel; +use crate::ui::components::styled::island_central_panel; +use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; +use crate::ui::components::top_panel::add_top_panel; +use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; +use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; +use crate::ui::helpers::{TransactionType, add_identity_key_chooser, render_group_action_text}; +use crate::ui::identities::get_selected_wallet; +use crate::ui::identities::keys::add_key_screen::AddKeyScreen; +use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; +use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike}; +use dash_sdk::dpp::data_contract::GroupContractPosition; +use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; +use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; +use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; +use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; +use dash_sdk::dpp::data_contract::group::Group; +use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; +use dash_sdk::dpp::group::GroupStateTransitionInfo; +use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; +use dash_sdk::dpp::identity::accessors::IdentityGettersV0; +use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; +use dash_sdk::platform::{Identifier, IdentityPublicKey}; +use eframe::egui::{self, Color32, Context, Ui}; +use egui::RichText; +use std::collections::HashSet; +use std::sync::{Arc, RwLock}; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// The states for the unfreeze flow +#[derive(PartialEq)] +pub enum UnfreezeTokensStatus { + NotStarted, + WaitingForResult(u64), + ErrorMessage(String), + Complete, +} + +/// A screen that allows unfreezing a previously frozen identity's tokens for a specific contract +pub struct UnfreezeTokensScreen { + pub identity: QualifiedIdentity, + pub identity_token_info: IdentityTokenInfo, + selected_key: Option, + pub public_note: Option, + + group: Option<(GroupContractPosition, Group)>, + is_unilateral_group_member: bool, + pub group_action_id: Option, + /// A list of identities that are frozen and can be unfrozen. + /// + /// TODO: Right now it is just a list of all identities, but it should be filtered to only show frozen ones. + frozen_identities: Vec, + + /// The identity we want to freeze + pub unfreeze_identity_id: String, + + status: UnfreezeTokensStatus, + error_message: Option, + + // Basic references + pub app_context: Arc, + + // Confirmation dialog + confirmation_dialog: Option, + + // If password-based wallet unlocking is needed + selected_wallet: Option>>, + wallet_password: String, + show_password: bool, +} + +impl UnfreezeTokensScreen { + pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { + // TODO: filter to include only frozen identities + let frozen_identities = app_context + .load_local_qualified_identities() + .expect("Identities not loaded"); + + let possible_key = identity_token_info + .identity + .identity + .get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ) + .cloned(); + + let mut error_message = None; + + let group = match identity_token_info + .token_config + .unfreeze_rules() + .authorized_to_make_change_action_takers() + { + AuthorizedActionTakers::NoOne => { + error_message = Some("Burning is not allowed on this token".to_string()); + None + } + AuthorizedActionTakers::ContractOwner => { + if identity_token_info.data_contract.contract.owner_id() + != identity_token_info.identity.identity.id() + { + error_message = Some( + "You are not allowed to burn this token. Only the contract owner is." + .to_string(), + ); + } + None + } + AuthorizedActionTakers::Identity(identifier) => { + if identifier != &identity_token_info.identity.identity.id() { + error_message = Some("You are not allowed to burn this token".to_string()); + } + None + } + AuthorizedActionTakers::MainGroup => { + match identity_token_info.token_config.main_control_group() { + None => { + error_message = Some( + "Invalid contract: No main control group, though one should exist" + .to_string(), + ); + None + } + Some(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(group_pos) + { + Ok(group) => Some((group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + } + } + AuthorizedActionTakers::Group(group_pos) => { + match identity_token_info + .data_contract + .contract + .expected_group(*group_pos) + { + Ok(group) => Some((*group_pos, group.clone())), + Err(e) => { + error_message = Some(format!("Invalid contract: {}", e)); + None + } + } + } + }; + + let mut is_unilateral_group_member = false; + if group.is_some() { + if let Some((_, group)) = group.clone() { + let your_power = group + .members() + .get(&identity_token_info.identity.identity.id()); + + if let Some(your_power) = your_power { + if your_power >= &group.required_power() { + is_unilateral_group_member = true; + } + } + } + }; + + // Attempt to get an unlocked wallet reference + let selected_wallet = get_selected_wallet( + &identity_token_info.identity, + None, + possible_key.as_ref(), + &mut error_message, + ); + + Self { + identity: identity_token_info.identity.clone(), + identity_token_info, + selected_key: possible_key, + group, + is_unilateral_group_member, + group_action_id: None, + public_note: None, + unfreeze_identity_id: String::new(), + status: UnfreezeTokensStatus::NotStarted, + error_message, + app_context: app_context.clone(), + confirmation_dialog: None, + selected_wallet, + wallet_password: String::new(), + show_password: false, + frozen_identities, + } + } + + fn render_unfreeze_identity_input(&mut self, ui: &mut Ui) { + let _response = ui.add( + IdentitySelector::new( + "unfreeze_identity_selector", + &mut self.unfreeze_identity_id, + &self.frozen_identities, + ) + .width(300.0) + .label("Identity ID to unfreeze:"), + ); + } + + fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + let mut is_open = true; + egui::Window::new("Confirm Unfreeze") + .collapsible(false) + .open(&mut is_open) + .show(ui.ctx(), |ui| { + // Validate user input + let parsed = Identifier::from_string_try_encodings( + &self.unfreeze_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + if parsed.is_err() { + self.error_message = Some("Please enter a valid identity ID.".into()); + self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); + self.show_confirmation_popup = false; + return; + } + let unfreeze_id = parsed.unwrap(); + + ui.label(format!( + "Are you sure you want to unfreeze identity {}?", + self.unfreeze_identity_id + )); + + ui.add_space(10.0); + + // Confirm + if ui.button("Confirm").clicked() { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = UnfreezeTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = + Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Dispatch to backend + action |= AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::UnfreezeTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + unfreeze_identity: unfreeze_id, + group_info, + }, + ))); + } + + // Cancel + if ui.button("Cancel").clicked() { + self.show_confirmation_popup = false; + } + }); + + if !is_open { + self.show_confirmation_popup = false; + } + action + } + + fn confirmation_ok(&mut self) -> AppAction { + // Validate user input + let parsed = Identifier::from_string_try_encodings( + &self.unfreeze_identity_id, + &[ + dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, + dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, + ], + ); + if parsed.is_err() { + self.error_message = Some("Please enter a valid identity ID.".into()); + self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); + return AppAction::None; + } + let unfreeze_id = parsed.unwrap(); + + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + self.status = UnfreezeTokensStatus::WaitingForResult(now); + + // Grab the data contract for this token from the app context + let data_contract = + Arc::new(self.identity_token_info.data_contract.contract.clone()); + + let group_info = if self.group_action_id.is_some() { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( + GroupStateTransitionInfo { + group_contract_position: *pos, + action_id: self.group_action_id.unwrap(), + action_is_proposer: false, + }, + ) + }) + } else { + self.group.as_ref().map(|(pos, _)| { + GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) + }) + }; + + // Dispatch to backend + AppAction::BackendTask(BackendTask::TokenTask(Box::new( + TokenTask::UnfreezeTokens { + actor_identity: self.identity.clone(), + data_contract, + token_position: self.identity_token_info.token_position, + signing_key: self.selected_key.clone().expect("No key selected"), + public_note: if self.group_action_id.is_some() { + None + } else { + self.public_note.clone() + }, + unfreeze_identity: unfreeze_id, + group_info, + }, + ))) + } + + fn show_success_screen(&self, ui: &mut Ui) -> AppAction { + let mut action = AppAction::None; + ui.vertical_centered(|ui| { + ui.add_space(50.0); + + ui.heading("🎉"); + if self.group_action_id.is_some() { + // This is already initiated by the group, we are just signing it + ui.heading("Group Unfreeze Signing Successful."); + } else if !self.is_unilateral_group_member && self.group.is_some() { + ui.heading("Group Unfreeze Initiated."); + } else { + ui.heading("Unfroze Identity Successfully."); + } + + ui.add_space(20.0); + + if self.group_action_id.is_some() { + if ui.button("Back to Group Actions").clicked() { + action |= AppAction::PopScreenAndRefresh; + } + if ui.button("Back to Tokens").clicked() { + action |= AppAction::SetMainScreenThenGoToMainScreen( + RootScreenType::RootScreenMyTokenBalances, + ); + } + } else { + if ui.button("Back to Tokens").clicked() { + action |= AppAction::PopScreenAndRefresh; + } + + if !self.is_unilateral_group_member && ui.button("Go to Group Actions").clicked() { + action |= AppAction::PopThenAddScreenToMainScreen( + RootScreenType::RootScreenDocumentQuery, + Screen::GroupActionsScreen(GroupActionsScreen::new( + &self.app_context.clone(), + )), + ); + } + } + }); + action + } +} + +impl ScreenLike for UnfreezeTokensScreen { + fn display_message(&mut self, message: &str, message_type: MessageType) { + match message_type { + MessageType::Success => { + // Possibly "UnfreezeTokens" or something else from your backend + if message.contains("Successfully unfroze identity") || message == "UnfreezeTokens" + { + self.status = UnfreezeTokensStatus::Complete; + } + } + MessageType::Error => { + self.status = UnfreezeTokensStatus::ErrorMessage(message.to_string()); + self.error_message = Some(message.to_string()); + } + MessageType::Info => {} + } + } + + fn refresh(&mut self) { + if let Ok(all_identities) = self.app_context.load_local_user_identities() { + if let Some(updated_identity) = all_identities + .into_iter() + .find(|id| id.identity.id() == self.identity.identity.id()) + { + self.identity = updated_identity; + } + } + } + + fn ui(&mut self, ctx: &Context) -> AppAction { + let mut action; + + // Build a top panel + if self.group_action_id.is_some() { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Contracts", AppAction::GoToMainScreen), + ("Group Actions", AppAction::PopScreen), + ("Unfreeze", AppAction::None), + ], + vec![], + ); + } else { + action = add_top_panel( + ctx, + &self.app_context, + vec![ + ("Tokens", AppAction::GoToMainScreen), + (&self.identity_token_info.token_alias, AppAction::PopScreen), + ("Unfreeze", AppAction::None), + ], + vec![], + ); + } + + // Left panel + action |= add_left_panel( + ctx, + &self.app_context, + crate::ui::RootScreenType::RootScreenMyTokenBalances, + ); + + // Subscreen chooser + action |= add_tokens_subscreen_chooser_panel(ctx, &self.app_context); + + island_central_panel(ctx, |ui| { + if self.status == UnfreezeTokensStatus::Complete { + action |= self.show_success_screen(ui); + return; + } + + ui.heading("Unfreeze a Frozen Identity’s Tokens"); + ui.add_space(10.0); + + // Check if user has any auth keys + let has_keys = if self.app_context.is_developer_mode() { + !self.identity.identity.public_keys().is_empty() + } else { + !self + .identity + .available_authentication_keys_with_critical_security_level() + .is_empty() + }; + + if !has_keys { + ui.colored_label( + Color32::RED, + format!( + "No authentication keys with CRITICAL security level found for this {} identity.", + self.identity.identity_type, + ), + ); + ui.add_space(10.0); + + // Show "Add key" or "Check keys" option + let first_key = self.identity.identity.get_first_public_key_matching( + Purpose::AUTHENTICATION, + HashSet::from([SecurityLevel::CRITICAL]), + KeyType::all_key_types().into(), + false, + ); + + if let Some(key) = first_key { + if ui.button("Check Keys").clicked() { + action |= AppAction::AddScreen(Screen::KeyInfoScreen(KeyInfoScreen::new( + self.identity.clone(), + key.clone(), + None, + &self.app_context, + ))); + } + ui.add_space(5.0); + } + + if ui.button("Add key").clicked() { + action |= AppAction::AddScreen(Screen::AddKeyScreen(AddKeyScreen::new( + self.identity.clone(), + &self.app_context, + ))); + } + } else { + // Possibly handle locked wallet scenario + if self.selected_wallet.is_some() { + let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); + + if needed_unlock && !just_unlocked { + return; + } + } + + // 1) Key selection + ui.heading("1. Select the key to sign the Unfreeze transition"); + ui.add_space(10.0); + + let mut selected_identity = Some(self.identity.clone()); + add_identity_key_chooser( + ui, + &self.app_context, + std::iter::once(&self.identity), + &mut selected_identity, + &mut self.selected_key, + TransactionType::TokenAction, + ); + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // 2) Identity to unfreeze + ui.heading("2. Enter the identity ID to unfreeze"); + ui.add_space(5.0); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Unfreeze so you are not allowed to choose the identity.", + ); + ui.add_space(5.0); + ui.label(format!("Identity: {}", self.unfreeze_identity_id)); + } else { + self.render_unfreeze_identity_input(ui); + } + + ui.add_space(10.0); + ui.separator(); + ui.add_space(10.0); + + // Render text input for the public note + ui.heading("3. Public note (optional)"); + ui.add_space(5.0); + if self.group_action_id.is_some() { + ui.label( + "You are signing an existing group Mint so you are not allowed to put a note.", + ); + ui.add_space(5.0); + ui.label(format!( + "Note: {}", + self.public_note.clone().unwrap_or("None".to_string()) + )); + } else { + ui.horizontal(|ui| { + ui.label("Public note (optional):"); + ui.add_space(10.0); + let mut txt = self.public_note.clone().unwrap_or_default(); + if ui + .text_edit_singleline(&mut txt) + .on_hover_text( + "A note about the transaction that can be seen by the public.", + ) + .changed() + { + self.public_note = if !txt.is_empty() { Some(txt) } else { None }; + } + }); + } + + let button_text = render_group_action_text( + ui, + &self.group, + &self.identity_token_info, + "Unfreeze", + &self.group_action_id, + ); + + // Unfreeze button + if self.app_context.is_developer_mode() || !button_text.contains("Test") { + ui.add_space(10.0); + let button = + egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) + .fill(Color32::from_rgb(0, 128, 255)) + .corner_radius(3.0); + + if ui.add(button).clicked() { + // Initialize confirmation dialog when button is clicked + self.confirmation_dialog = None; // Reset for fresh dialog + } + } + + // Show confirmation dialog if it exists + if self.confirmation_dialog.is_some() { + action |= self.show_confirmation_popup(ui); + } + + // Show in-progress or error messages + ui.add_space(10.0); + match &self.status { + UnfreezeTokensStatus::NotStarted => { + // no-op + } + UnfreezeTokensStatus::WaitingForResult(start_time) => { + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("Time went backwards") + .as_secs(); + let elapsed = now - start_time; + ui.label(format!("Unfreezing... elapsed: {}s", elapsed)); + } + UnfreezeTokensStatus::ErrorMessage(msg) => { + ui.colored_label(Color32::RED, format!("Error: {}", msg)); + } + UnfreezeTokensStatus::Complete => { + // handled above + } + } + } + }); + + action + } +} + +impl ScreenWithWalletUnlock for UnfreezeTokensScreen { + fn selected_wallet_ref(&self) -> &Option>> { + &self.selected_wallet + } + + fn wallet_password_ref(&self) -> &String { + &self.wallet_password + } + + fn wallet_password_mut(&mut self) -> &mut String { + &mut self.wallet_password + } + + fn show_password(&self) -> bool { + self.show_password + } + + fn show_password_mut(&mut self) -> &mut bool { + &mut self.show_password + } + + fn set_error_message(&mut self, error_message: Option) { + self.error_message = error_message; + } + + fn error_message(&self) -> Option<&String> { + self.error_message.as_ref() + } +} From 792e3044d10e308515d6c29db0b1f9fd7af59a18 Mon Sep 17 00:00:00 2001 From: pauldelucia Date: Wed, 13 Aug 2025 19:35:18 +0700 Subject: [PATCH 2/2] fix: remove backup files --- .../tokens/destroy_frozen_funds_screen.rs.bak | 661 ---------------- src/ui/tokens/mint_tokens_screen.rs.bak | 734 ------------------ src/ui/tokens/transfer_tokens_screen.rs.bak | 587 -------------- src/ui/tokens/unfreeze_tokens_screen.rs.bak | 694 ----------------- 4 files changed, 2676 deletions(-) delete mode 100644 src/ui/tokens/destroy_frozen_funds_screen.rs.bak delete mode 100644 src/ui/tokens/mint_tokens_screen.rs.bak delete mode 100644 src/ui/tokens/transfer_tokens_screen.rs.bak delete mode 100644 src/ui/tokens/unfreeze_tokens_screen.rs.bak diff --git a/src/ui/tokens/destroy_frozen_funds_screen.rs.bak b/src/ui/tokens/destroy_frozen_funds_screen.rs.bak deleted file mode 100644 index 208565648..000000000 --- a/src/ui/tokens/destroy_frozen_funds_screen.rs.bak +++ /dev/null @@ -1,661 +0,0 @@ -use super::tokens_screen::IdentityTokenInfo; -use crate::app::{AppAction, BackendTasksExecutionMode}; -use crate::backend_task::BackendTask; -use crate::backend_task::tokens::TokenTask; -use crate::context::AppContext; -use crate::model::qualified_identity::QualifiedIdentity; -use crate::model::wallet::Wallet; -use crate::ui::components::component_trait::Component; -use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; -use crate::ui::components::identity_selector::IdentitySelector; -use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::island_central_panel; -use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; -use crate::ui::components::top_panel::add_top_panel; -use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; -use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; -use crate::ui::helpers::{TransactionType, add_identity_key_chooser, render_group_action_text}; -use crate::ui::identities::get_selected_wallet; -use crate::ui::identities::keys::add_key_screen::AddKeyScreen; -use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; -use crate::ui::theme::DashColors; -use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike}; -use dash_sdk::dpp::data_contract::GroupContractPosition; -use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; -use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; -use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; -use dash_sdk::dpp::data_contract::group::Group; -use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; -use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; -use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; -use dash_sdk::platform::{Identifier, IdentityPublicKey}; -use eframe::egui::{self, Color32, Context, Ui}; -use egui::RichText; -use std::collections::HashSet; -use std::sync::{Arc, RwLock}; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// Represents possible states in the “destroy frozen funds” flow -#[derive(PartialEq)] -pub enum DestroyFrozenFundsStatus { - NotStarted, - WaitingForResult(u64), - ErrorMessage(String), - Complete, -} - -/// A screen for destroying frozen funds of a particular token contract -pub struct DestroyFrozenFundsScreen { - /// Identity that is authorized to destroy - pub identity: QualifiedIdentity, - - /// Info on which token contract we’re dealing with - pub identity_token_info: IdentityTokenInfo, - - /// The key used to sign the operation - selected_key: Option, - - group: Option<(GroupContractPosition, Group)>, - is_unilateral_group_member: bool, - pub group_action_id: Option, - - /// Optional public note - pub public_note: Option, - - /// The user must specify the identity ID whose frozen funds are to be destroyed - /// Typically some Identity that has been frozen by the system or a group - pub frozen_identity_id: String, - - /// All frozen identities that can be selected - /// TODO: We should filter them by frozen status, right now we just show all known identities - pub frozen_identities: Vec, - - status: DestroyFrozenFundsStatus, - error_message: Option, - - /// Basic references - pub app_context: Arc, - - /// Confirmation dialog - confirmation_dialog: Option, - - /// If password-based wallet unlocking is needed - selected_wallet: Option>>, - wallet_password: String, - show_password: bool, -} - -impl DestroyFrozenFundsScreen { - pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { - let possible_key = identity_token_info - .identity - .identity - .get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ) - .cloned(); - - let mut error_message = None; - - let group = match identity_token_info - .token_config - .destroy_frozen_funds_rules() - .authorized_to_make_change_action_takers() - { - AuthorizedActionTakers::NoOne => { - error_message = Some("Burning is not allowed on this token".to_string()); - None - } - AuthorizedActionTakers::ContractOwner => { - if identity_token_info.data_contract.contract.owner_id() - != identity_token_info.identity.identity.id() - { - error_message = Some( - "You are not allowed to burn this token. Only the contract owner is." - .to_string(), - ); - } - None - } - AuthorizedActionTakers::Identity(identifier) => { - if identifier != &identity_token_info.identity.identity.id() { - error_message = Some("You are not allowed to burn this token".to_string()); - } - None - } - AuthorizedActionTakers::MainGroup => { - match identity_token_info.token_config.main_control_group() { - None => { - error_message = Some( - "Invalid contract: No main control group, though one should exist" - .to_string(), - ); - None - } - Some(group_pos) => { - match identity_token_info - .data_contract - .contract - .expected_group(group_pos) - { - Ok(group) => Some((group_pos, group.clone())), - Err(e) => { - error_message = Some(format!("Invalid contract: {}", e)); - None - } - } - } - } - } - AuthorizedActionTakers::Group(group_pos) => { - match identity_token_info - .data_contract - .contract - .expected_group(*group_pos) - { - Ok(group) => Some((*group_pos, group.clone())), - Err(e) => { - error_message = Some(format!("Invalid contract: {}", e)); - None - } - } - } - }; - - let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } - } - }; - - // Attempt to get an unlocked wallet reference - let selected_wallet = get_selected_wallet( - &identity_token_info.identity, - None, - possible_key.as_ref(), - &mut error_message, - ); - - let all_identities = app_context - .load_local_qualified_identities() - .expect("Identities not loaded"); - - Self { - identity: identity_token_info.identity.clone(), - frozen_identity_id: String::new(), - frozen_identities: all_identities, - identity_token_info, - selected_key: possible_key, - group, - is_unilateral_group_member, - group_action_id: None, - public_note: None, - status: DestroyFrozenFundsStatus::NotStarted, - error_message, - app_context: app_context.clone(), - confirmation_dialog: None, - selected_wallet, - wallet_password: String::new(), - show_password: false, - } - } - - /// Renders the text input for specifying the “frozen identity” - fn render_frozen_identity_input(&mut self, ui: &mut Ui) { - ui.add( - IdentitySelector::new( - "frozen_identity_selector", - &mut self.frozen_identity_id, - &self.frozen_identities, - ) - .label("Frozen Identity ID:"), - ); - } - - /// Confirmation popup - fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Destroy Frozen Funds") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Parse the user input into an Identifier - let maybe_frozen_id = Identifier::from_string_try_encodings( - &self.frozen_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - - if maybe_frozen_id.is_err() { - self.error_message = Some("Invalid frozen identity format".into()); - self.status = DestroyFrozenFundsStatus::ErrorMessage("Invalid identity".into()); - self.show_confirmation_popup = false; - return; - } - - let frozen_id = maybe_frozen_id.unwrap(); - - ui.label(format!( - "Are you sure you want to destroy the frozen funds of identity {}?", - self.frozen_identity_id - )); - - ui.add_space(10.0); - - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = DestroyFrozenFundsStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch the actual backend destroy action - action = AppAction::BackendTasks( - vec![ - BackendTask::TokenTask(Box::new(TokenTask::DestroyFrozenFunds { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - frozen_identity: frozen_id, - group_info, - })), - BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)), - ], - BackendTasksExecutionMode::Sequential, - ); - } - - // Cancel - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); - - if !is_open { - self.show_confirmation_popup = false; - } - action - } - - /// Simple “Success” screen - fn show_success_screen(&self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - ui.vertical_centered(|ui| { - ui.add_space(50.0); - - ui.heading("🎉"); - if self.group_action_id.is_some() { - // This destroy is already initiated by the group, we are just signing it - ui.heading("Group Destroy Frozen Funds Signing Successful."); - } else if !self.is_unilateral_group_member && self.group.is_some() { - ui.heading("Group Action to Destroy Frozen Funds Initiated."); - } else { - ui.heading("Frozen Funds Destroyed Successfully."); - } - - ui.add_space(20.0); - - if self.group_action_id.is_some() { - if ui.button("Back to Group Actions").clicked() { - action = AppAction::PopScreenAndRefresh; - } - if ui.button("Back to Tokens").clicked() { - action = AppAction::SetMainScreenThenGoToMainScreen( - RootScreenType::RootScreenMyTokenBalances, - ); - } - } else { - if ui.button("Back to Tokens").clicked() { - action = AppAction::PopScreenAndRefresh; - } - - if !self.is_unilateral_group_member && ui.button("Go to Group Actions").clicked() { - action = AppAction::PopThenAddScreenToMainScreen( - RootScreenType::RootScreenDocumentQuery, - Screen::GroupActionsScreen(GroupActionsScreen::new( - &self.app_context.clone(), - )), - ); - } - } - }); - action - } -} - -impl ScreenLike for DestroyFrozenFundsScreen { - fn display_message(&mut self, message: &str, message_type: MessageType) { - match message_type { - MessageType::Success => { - // If your backend returns "DestroyFrozenFunds" on success, - // or if there's a more descriptive success message: - if message.contains("Successfully destroyed frozen funds") - || message == "DestroyFrozenFunds" - { - self.status = DestroyFrozenFundsStatus::Complete; - } - } - MessageType::Error => { - self.status = DestroyFrozenFundsStatus::ErrorMessage(message.to_string()); - self.error_message = Some(message.to_string()); - } - MessageType::Info => { - // no-op - } - } - } - - fn refresh(&mut self) { - // Reload the identity data if needed - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities - .into_iter() - .find(|id| id.identity.id() == self.identity.identity.id()) - { - self.identity = updated_identity; - } - } - } - - fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action; - - // Build a top panel - if self.group_action_id.is_some() { - action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Contracts", AppAction::GoToMainScreen), - ("Group Actions", AppAction::PopScreen), - ("Destroy Frozen Funds", AppAction::None), - ], - vec![], - ); - } else { - action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - (&self.identity_token_info.token_alias, AppAction::PopScreen), - ("Destroy Frozen Funds", AppAction::None), - ], - vec![], - ); - } - - // Left panel - action |= add_left_panel( - ctx, - &self.app_context, - crate::ui::RootScreenType::RootScreenMyTokenBalances, - ); - - // Subscreen chooser - action |= add_tokens_subscreen_chooser_panel(ctx, &self.app_context); - - island_central_panel(ctx, |ui| { - let dark_mode = ui.ctx().style().visuals.dark_mode; - - if self.status == DestroyFrozenFundsStatus::Complete { - action |= self.show_success_screen(ui); - return; - } - - ui.heading("Destroy Frozen Funds"); - ui.add_space(10.0); - - // Check if user has any auth keys - let has_keys = if self.app_context.is_developer_mode() { - !self.identity.identity.public_keys().is_empty() - } else { - !self - .identity - .available_authentication_keys_with_critical_security_level() - .is_empty() - }; - - if !has_keys { - ui.colored_label( - DashColors::error_color(dark_mode), - format!( - "No authentication keys with CRITICAL security level found for this {} identity.", - self.identity.identity_type, - ), - ); - ui.add_space(10.0); - - // Show "Add key" or "Check keys" option - let first_key = self.identity.identity.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); - - if let Some(key) = first_key { - if ui.button("Check Keys").clicked() { - action |= AppAction::AddScreen(Screen::KeyInfoScreen(KeyInfoScreen::new( - self.identity.clone(), - key.clone(), - None, - &self.app_context, - ))); - } - ui.add_space(5.0); - } - - if ui.button("Add key").clicked() { - action |= AppAction::AddScreen(Screen::AddKeyScreen(AddKeyScreen::new( - self.identity.clone(), - &self.app_context, - ))); - } - } else { - // Possibly handle locked wallet scenario - if self.selected_wallet.is_some() { - let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); - if needed_unlock && !just_unlocked { - return; - } - } - - // Key selection - ui.heading("1. Select the key to sign the Destroy operation"); - ui.add_space(10.0); - - let mut selected_identity = Some(self.identity.clone()); - add_identity_key_chooser( - ui, - &self.app_context, - std::iter::once(&self.identity), - &mut selected_identity, - &mut self.selected_key, - TransactionType::TokenAction, - ); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - // Frozen identity - ui.heading("2. Frozen identity to destroy funds from"); - ui.add_space(5.0); - if self.group_action_id.is_some() { - ui.label( - "You are signing an existing group Destroy so you are not allowed to choose the identity.", - ); - ui.add_space(5.0); - ui.label(format!("Identity: {}", self.frozen_identity_id)); - } else { - self.render_frozen_identity_input(ui); - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - // Render text input for the public note - ui.heading("3. Public note (optional)"); - ui.add_space(5.0); - if self.group_action_id.is_some() { - ui.label( - "You are signing an existing group Destroy so you are not allowed to put a note.", - ); - ui.add_space(5.0); - ui.label(format!( - "Note: {}", - self.public_note.clone().unwrap_or("None".to_string()) - )); - } else { - ui.horizontal(|ui| { - ui.label("Public note (optional):"); - ui.add_space(10.0); - let mut txt = self.public_note.clone().unwrap_or_default(); - if ui - .text_edit_singleline(&mut txt) - .on_hover_text( - "A note about the transaction that can be seen by the public.", - ) - .changed() - { - self.public_note = if !txt.is_empty() { Some(txt) } else { None }; - } - }); - } - - let button_text = render_group_action_text( - ui, - &self.group, - &self.identity_token_info, - "Destroy Frozen Funds", - &self.group_action_id, - ); - - // Destroy button - if self.app_context.is_developer_mode() || !button_text.contains("Test") { - ui.add_space(10.0); - let button = - egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .corner_radius(3.0); - - if ui.add(button).clicked() { - self.show_confirmation_popup = true; - } - } - - // If user pressed "Destroy," show a popup - if self.show_confirmation_popup { - action |= self.show_confirmation_popup(ui); - } - - // Show in-progress or error messages - ui.add_space(10.0); - match &self.status { - DestroyFrozenFundsStatus::NotStarted => { - // no-op - } - DestroyFrozenFundsStatus::WaitingForResult(start_time) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - let elapsed = now - start_time; - ui.label(format!( - "Destroying frozen funds... elapsed: {} seconds", - elapsed - )); - } - DestroyFrozenFundsStatus::ErrorMessage(msg) => { - ui.colored_label( - DashColors::error_color(dark_mode), - format!("Error: {}", msg), - ); - } - DestroyFrozenFundsStatus::Complete => { - // handled above - } - } - } - }); - - action - } -} - -impl ScreenWithWalletUnlock for DestroyFrozenFundsScreen { - fn selected_wallet_ref(&self) -> &Option>> { - &self.selected_wallet - } - - fn wallet_password_ref(&self) -> &String { - &self.wallet_password - } - - fn wallet_password_mut(&mut self) -> &mut String { - &mut self.wallet_password - } - - fn show_password(&self) -> bool { - self.show_password - } - - fn show_password_mut(&mut self) -> &mut bool { - &mut self.show_password - } - - fn set_error_message(&mut self, error_message: Option) { - self.error_message = error_message; - } - - fn error_message(&self) -> Option<&String> { - self.error_message.as_ref() - } -} diff --git a/src/ui/tokens/mint_tokens_screen.rs.bak b/src/ui/tokens/mint_tokens_screen.rs.bak deleted file mode 100644 index ef2401817..000000000 --- a/src/ui/tokens/mint_tokens_screen.rs.bak +++ /dev/null @@ -1,734 +0,0 @@ -use super::tokens_screen::IdentityTokenInfo; -use crate::app::AppAction; -use crate::backend_task::BackendTask; -use crate::backend_task::tokens::TokenTask; -use crate::context::AppContext; -use crate::model::amount::Amount; -use crate::model::qualified_identity::QualifiedIdentity; -use crate::model::wallet::Wallet; -use crate::ui::components::amount_input::AmountInput; -use crate::ui::components::component_trait::Component; -use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; -use crate::ui::components::identity_selector::IdentitySelector; -use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::island_central_panel; -use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; -use crate::ui::components::top_panel::add_top_panel; -use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; -use crate::ui::components::{Component, ComponentResponse}; -use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; -use crate::ui::helpers::{TransactionType, add_identity_key_chooser, render_group_action_text}; -use crate::ui::identities::get_selected_wallet; -use crate::ui::identities::keys::add_key_screen::AddKeyScreen; -use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; -use crate::ui::theme::DashColors; -use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike}; -use dash_sdk::dpp::data_contract::GroupContractPosition; -use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; -use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; -use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use dash_sdk::dpp::data_contract::associated_token::token_distribution_rules::accessors::v0::TokenDistributionRulesV0Getters; -use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; -use dash_sdk::dpp::data_contract::group::Group; -use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; -use dash_sdk::dpp::group::{GroupStateTransitionInfo, GroupStateTransitionInfoStatus}; -use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; -use dash_sdk::dpp::platform_value::string_encoding::Encoding; -use dash_sdk::platform::{Identifier, IdentityPublicKey}; -use eframe::egui::{self, Color32, Context, Ui}; -use egui::RichText; -use std::collections::HashSet; -use std::sync::{Arc, RwLock}; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// Internal states for the mint process. -#[derive(PartialEq)] -pub enum MintTokensStatus { - NotStarted, - WaitingForResult(u64), // Use seconds or millis - ErrorMessage(String), - Complete, -} - -/// A UI Screen for minting tokens from an existing token contract -pub struct MintTokensScreen { - pub identity_token_info: IdentityTokenInfo, - selected_key: Option, - pub public_note: Option, - group: Option<(GroupContractPosition, Group)>, - is_unilateral_group_member: bool, - pub group_action_id: Option, - known_identities: Vec, - - pub recipient_identity_id: String, - - pub amount: Option, - pub amount_input: Option, - status: MintTokensStatus, - error_message: Option, - - /// Basic references - pub app_context: Arc, - - /// Confirmation popup - confirmation_dialog: Option, - - // If needed for password-based wallet unlocking: - selected_wallet: Option>>, - wallet_password: String, - show_password: bool, -} - -impl MintTokensScreen { - pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { - let known_identities = app_context - .load_local_qualified_identities() - .expect("Identities not loaded"); - - let possible_key = identity_token_info - .identity - .identity - .get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ) - .cloned(); - - let mut error_message = None; - - let group = match identity_token_info - .token_config - .manual_minting_rules() - .authorized_to_make_change_action_takers() - { - AuthorizedActionTakers::NoOne => { - error_message = Some("Minting is not allowed on this token".to_string()); - None - } - AuthorizedActionTakers::ContractOwner => { - if identity_token_info.data_contract.contract.owner_id() - != identity_token_info.identity.identity.id() - { - error_message = Some( - "You are not allowed to mint this token. Only the contract owner is." - .to_string(), - ); - } - None - } - AuthorizedActionTakers::Identity(identifier) => { - if identifier != &identity_token_info.identity.identity.id() { - error_message = Some("You are not allowed to mint this token".to_string()); - } - None - } - AuthorizedActionTakers::MainGroup => { - match identity_token_info.token_config.main_control_group() { - None => { - error_message = Some( - "Invalid contract: No main control group, though one should exist" - .to_string(), - ); - None - } - Some(group_pos) => { - match identity_token_info - .data_contract - .contract - .expected_group(group_pos) - { - Ok(group) => Some((group_pos, group.clone())), - Err(e) => { - error_message = Some(format!("Invalid contract: {}", e)); - None - } - } - } - } - } - AuthorizedActionTakers::Group(group_pos) => { - match identity_token_info - .data_contract - .contract - .expected_group(*group_pos) - { - Ok(group) => Some((*group_pos, group.clone())), - Err(e) => { - error_message = Some(format!("Invalid contract: {}", e)); - None - } - } - } - }; - - let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } - } - }; - - // Attempt to get an unlocked wallet reference - let selected_wallet = get_selected_wallet( - &identity_token_info.identity, - None, - possible_key.as_ref(), - &mut error_message, - ); - - Self { - identity_token_info, - selected_key: possible_key, - public_note: None, - group, - is_unilateral_group_member, - group_action_id: None, - known_identities, - recipient_identity_id: "".to_string(), - amount: None, - amount_input: None, - status: MintTokensStatus::NotStarted, - error_message, - app_context: app_context.clone(), - confirmation_dialog: None, - selected_wallet, - wallet_password: String::new(), - show_password: false, - } - } - - /// Renders an amount input for the user to specify an amount to mint - fn render_amount_input(&mut self, ui: &mut Ui) { - // Lazy initialization with proper token configuration - let amount_input = self.amount_input.get_or_insert_with(|| { - // Create appropriate Amount based on token configuration - let token_amount = Amount::from_token(&self.identity_token_info, 0); - AmountInput::new(token_amount).with_label("Amount to Mint:") - }); - - // Check if input should be disabled when operation is in progress - let enabled = match self.status { - MintTokensStatus::WaitingForResult(_) | MintTokensStatus::Complete => false, - MintTokensStatus::NotStarted | MintTokensStatus::ErrorMessage(_) => true, - }; - - let response = ui.add_enabled_ui(enabled, |ui| amount_input.show(ui)).inner; - - response.inner.update(&mut self.amount); - // errors are handled inside AmountInput - } - - /// Renders an optional text input for the user to specify a "Recipient Identity" - fn render_recipient_input(&mut self, ui: &mut Ui) { - let _response = ui.add( - IdentitySelector::new( - "mint_recipient_selector", - &mut self.recipient_identity_id, - &self.known_identities, - ) - .width(300.0) - .label("Recipient:") - .exclude(&[self.identity_token_info.identity.identity.id()]), - ); - - // If empty, minted tokens go to the 'issuer' identity (self.identity). - } - - /// Renders a confirm popup with the final "Are you sure?" step - fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Mint") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Validate user input - let Some(amount) = &self.amount else { - self.error_message = Some("Please enter a valid amount.".into()); - self.status = MintTokensStatus::ErrorMessage("Invalid amount".into()); - self.show_confirmation_popup = false; - return; - }; - - let maybe_identifier = if self.recipient_identity_id.trim().is_empty() { - None - } else { - // Attempt to parse from base58 or hex - match Identifier::from_string_try_encodings( - &self.recipient_identity_id, - &[Encoding::Base58, Encoding::Hex], - ) { - Ok(id) => Some(id), - Err(_) => { - self.error_message = Some("Invalid recipient identity format.".into()); - self.status = - MintTokensStatus::ErrorMessage("Invalid recipient identity".into()); - self.show_confirmation_popup = false; - return; - } - } - }; - - ui.label(format!( - "Are you sure you want to mint {} token(s)?", - amount - )); - - // If user provided a recipient: - if let Some(ref recipient_id) = maybe_identifier { - ui.label(format!( - "Recipient: {}", - recipient_id.to_string(Encoding::Base58) - )); - } else { - ui.label("No recipient specified; tokens will be minted to default identity."); - } - - ui.add_space(10.0); - - // Confirm button - if ui.button("Confirm").clicked() { - self.show_confirmation_popup = false; - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = MintTokensStatus::WaitingForResult(now); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch the actual backend mint action - action = AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::MintTokens { - sending_identity: self.identity_token_info.identity.clone(), - data_contract: Arc::new( - self.identity_token_info.data_contract.contract.clone(), - ), - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("Expected a key"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - amount: amount.value(), - recipient_id: maybe_identifier, - group_info, - }, - ))); - } - - // Cancel button - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); - - if !is_open { - self.show_confirmation_popup = false; - } - action - } - - /// Renders a simple "Success!" screen after completion - fn show_success_screen(&self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - ui.vertical_centered(|ui| { - ui.add_space(50.0); - - ui.heading("🎉"); - if self.group_action_id.is_some() { - // This mint is already initiated by the group, we are just signing it - ui.heading("Group Mint Signing Successful."); - } else if !self.is_unilateral_group_member && self.group.is_some() { - ui.heading("Group Mint Initiated."); - } else { - ui.heading("Mint Successful."); - } - - ui.add_space(20.0); - - if self.group_action_id.is_some() { - if ui.button("Back to Group Actions").clicked() { - action = AppAction::PopScreenAndRefresh; - } - if ui.button("Back to Tokens").clicked() { - action = AppAction::SetMainScreenThenGoToMainScreen( - RootScreenType::RootScreenMyTokenBalances, - ); - } - } else { - if ui.button("Back to Tokens").clicked() { - action = AppAction::PopScreenAndRefresh; - } - - if !self.is_unilateral_group_member && ui.button("Go to Group Actions").clicked() { - action = AppAction::PopThenAddScreenToMainScreen( - RootScreenType::RootScreenDocumentQuery, - Screen::GroupActionsScreen(GroupActionsScreen::new( - &self.app_context.clone(), - )), - ); - } - } - }); - action - } -} - -impl ScreenLike for MintTokensScreen { - fn display_message(&mut self, message: &str, message_type: MessageType) { - match message_type { - MessageType::Success => { - if message.contains("Successfully minted tokens") || message == "MintTokens" { - self.status = MintTokensStatus::Complete; - } - } - MessageType::Error => { - self.status = MintTokensStatus::ErrorMessage(message.to_string()); - self.error_message = Some(message.to_string()); - } - MessageType::Info => { - // no-op - } - } - } - - fn refresh(&mut self) { - // If you need to reload local identity data or re-check keys: - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities - .into_iter() - .find(|id| id.identity.id() == self.identity_token_info.identity.identity.id()) - { - self.identity_token_info.identity = updated_identity; - } - } - } - - fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action; - - // Build a top panel - if self.group_action_id.is_some() { - action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Contracts", AppAction::GoToMainScreen), - ("Group Actions", AppAction::PopScreen), - ("Mint", AppAction::None), - ], - vec![], - ); - } else { - action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - (&self.identity_token_info.token_alias, AppAction::PopScreen), - ("Mint", AppAction::None), - ], - vec![], - ); - } - - // Left panel - action |= add_left_panel( - ctx, - &self.app_context, - crate::ui::RootScreenType::RootScreenMyTokenBalances, - ); - - // Subscreen chooser - action |= add_tokens_subscreen_chooser_panel(ctx, &self.app_context); - - let central_panel_action = island_central_panel(ctx, |ui| { - let dark_mode = ui.ctx().style().visuals.dark_mode; - - // If we are in the "Complete" status, just show success screen - if self.status == MintTokensStatus::Complete { - return self.show_success_screen(ui); - } - - ui.heading("Mint Tokens"); - ui.add_space(10.0); - - // Check if user has any auth keys - let has_keys = if self.app_context.is_developer_mode() { - !self - .identity_token_info - .identity - .identity - .public_keys() - .is_empty() - } else { - !self - .identity_token_info - .identity - .available_authentication_keys_with_critical_security_level() - .is_empty() - }; - - if !has_keys { - ui.colored_label( - DashColors::error_color(dark_mode), - format!( - "No authentication keys with CRITICAL security level found for this {} identity.", - self.identity_token_info.identity.identity_type, - ), - ); - ui.add_space(10.0); - - // Show "Add key" or "Check keys" option - let first_key = self - .identity_token_info - .identity - .identity - .get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); - - if let Some(key) = first_key { - if ui.button("Check Keys").clicked() { - action |= AppAction::AddScreen(Screen::KeyInfoScreen(KeyInfoScreen::new( - self.identity_token_info.identity.clone(), - key.clone(), - None, - &self.app_context, - ))); - } - ui.add_space(5.0); - } - - if ui.button("Add key").clicked() { - action |= AppAction::AddScreen(Screen::AddKeyScreen(AddKeyScreen::new( - self.identity_token_info.identity.clone(), - &self.app_context, - ))); - } - } else { - // Possibly handle locked wallet scenario (similar to TransferTokens) - if self.selected_wallet.is_some() { - let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); - - if needed_unlock && !just_unlocked { - // Must unlock before we can proceed - return AppAction::None; - } - } - - // 1) Key selection - ui.heading("1. Select the key to sign the Mint transaction"); - ui.add_space(10.0); - - let mut selected_identity = Some(self.identity_token_info.identity.clone()); - add_identity_key_chooser( - ui, - &self.app_context, - std::iter::once(&self.identity_token_info.identity), - &mut selected_identity, - &mut self.selected_key, - TransactionType::TokenAction, - ); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - // 2) Amount to mint - ui.heading("2. Amount to mint"); - ui.add_space(5.0); - if self.group_action_id.is_some() { - ui.label( - "You are signing an existing group Mint so you are not allowed to choose the amount.", - ); - ui.add_space(5.0); - ui.label(format!( - "Amount: {}", - self.amount - .as_ref() - .map(|a| a.to_string()) - .unwrap_or_default() - )); - } else { - self.render_amount_input(ui); - } - - if self - .identity_token_info - .token_config - .distribution_rules() - .minting_allow_choosing_destination() - || self.app_context.is_developer_mode() - { - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - if self - .identity_token_info - .token_config - .distribution_rules() - .new_tokens_destination_identity() - .is_some() - { - ui.heading("3. Recipient identity (optional)"); - } else { - ui.heading("3. Recipient identity (required)"); - } - ui.add_space(5.0); - self.render_recipient_input(ui); - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - // Render text input for the public note - ui.heading("4. Public note (optional)"); - ui.add_space(5.0); - if self.group_action_id.is_some() { - ui.label( - "You are signing an existing group Mint so you are not allowed to put a note.", - ); - ui.add_space(5.0); - ui.label(format!( - "Note: {}", - self.public_note.clone().unwrap_or("None".to_string()) - )); - } else { - ui.horizontal(|ui| { - ui.label("Public note (optional):"); - ui.add_space(10.0); - let mut txt = self.public_note.clone().unwrap_or_default(); - if ui - .text_edit_singleline(&mut txt) - .on_hover_text( - "A note about the transaction that can be seen by the public.", - ) - .changed() - { - self.public_note = if !txt.is_empty() { Some(txt) } else { None }; - } - }); - } - - let button_text = render_group_action_text( - ui, - &self.group, - &self.identity_token_info, - "Mint", - &self.group_action_id, - ); - - // Mint button - if self.app_context.is_developer_mode() || !button_text.contains("Test") { - ui.add_space(10.0); - let button = - egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .corner_radius(3.0); - - if ui.add(button).clicked() { - self.show_confirmation_popup = true; - } - } - - // If the user pressed "Mint," show a popup - if self.show_confirmation_popup { - action |= self.show_confirmation_popup(ui); - } - - // Show in-progress or error messages - ui.add_space(10.0); - match &self.status { - MintTokensStatus::NotStarted => { - // no-op - } - MintTokensStatus::WaitingForResult(start_time) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - let elapsed = now - start_time; - ui.label(format!("Minting... elapsed: {} seconds", elapsed)); - } - MintTokensStatus::ErrorMessage(msg) => { - ui.colored_label( - DashColors::error_color(dark_mode), - format!("Error: {}", msg), - ); - } - MintTokensStatus::Complete => { - // handled above - } - } - } - - AppAction::None - }); - - action |= central_panel_action; - action - } -} - -impl ScreenWithWalletUnlock for MintTokensScreen { - fn selected_wallet_ref(&self) -> &Option>> { - &self.selected_wallet - } - - fn wallet_password_ref(&self) -> &String { - &self.wallet_password - } - - fn wallet_password_mut(&mut self) -> &mut String { - &mut self.wallet_password - } - - fn show_password(&self) -> bool { - self.show_password - } - - fn show_password_mut(&mut self) -> &mut bool { - &mut self.show_password - } - - fn set_error_message(&mut self, error_message: Option) { - self.error_message = error_message; - } - - fn error_message(&self) -> Option<&String> { - self.error_message.as_ref() - } -} diff --git a/src/ui/tokens/transfer_tokens_screen.rs.bak b/src/ui/tokens/transfer_tokens_screen.rs.bak deleted file mode 100644 index 6af8044fc..000000000 --- a/src/ui/tokens/transfer_tokens_screen.rs.bak +++ /dev/null @@ -1,587 +0,0 @@ -use crate::app::{AppAction, BackendTasksExecutionMode}; -use crate::backend_task::BackendTask; -use crate::backend_task::tokens::TokenTask; -use crate::context::AppContext; -use crate::model::amount::Amount; -use crate::model::qualified_identity::QualifiedIdentity; -use crate::model::wallet::Wallet; -use crate::ui::components::amount_input::AmountInput; -use crate::ui::components::component_trait::Component; -use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; -use crate::ui::components::identity_selector::IdentitySelector; -use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::island_central_panel; -use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; -use crate::ui::components::top_panel::add_top_panel; -use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; -use crate::ui::components::{Component, ComponentResponse}; -use crate::ui::helpers::{TransactionType, add_identity_key_chooser}; -use crate::ui::identities::keys::add_key_screen::AddKeyScreen; -use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; -use crate::ui::theme::DashColors; -use crate::ui::{MessageType, Screen, ScreenLike}; -use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; -use dash_sdk::dpp::platform_value::string_encoding::Encoding; -use dash_sdk::dpp::prelude::TimestampMillis; -use dash_sdk::platform::{Identifier, IdentityPublicKey}; -use eframe::egui::{self, Context, Ui}; -use egui::{Color32, RichText}; -use std::collections::HashSet; -use std::sync::{Arc, RwLock}; -use std::time::{SystemTime, UNIX_EPOCH}; - -use crate::ui::identities::get_selected_wallet; - -use super::tokens_screen::IdentityTokenBalance; - -#[derive(PartialEq)] -pub enum TransferTokensStatus { - NotStarted, - WaitingForResult(TimestampMillis), - ErrorMessage(String), - Complete, -} - -pub struct TransferTokensScreen { - pub identity: QualifiedIdentity, - pub identity_token_balance: IdentityTokenBalance, - known_identities: Vec, - selected_key: Option, - pub public_note: Option, - pub receiver_identity_id: String, - pub amount: Option, - pub amount_input: Option, - transfer_tokens_status: TransferTokensStatus, - max_amount: Amount, - pub app_context: Arc, - confirmation_dialog: Option, - selected_wallet: Option>>, - wallet_password: String, - show_password: bool, -} - -impl TransferTokensScreen { - pub fn new( - identity_token_balance: IdentityTokenBalance, - app_context: &Arc, - ) -> Self { - let known_identities = app_context - .load_local_qualified_identities() - .expect("Identities not loaded"); - - let identity = known_identities - .iter() - .find(|identity| identity.identity.id() == identity_token_balance.identity_id) - .expect("Identity not found") - .clone(); - let max_amount = Amount::from(&identity_token_balance); - let identity_clone = identity.identity.clone(); - let selected_key = identity_clone.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); - let mut error_message = None; - let selected_wallet = - get_selected_wallet(&identity, None, selected_key, &mut error_message); - - let amount = Some(Amount::from(&identity_token_balance).with_value(0)); - - Self { - identity, - identity_token_balance, - known_identities, - selected_key: selected_key.cloned(), - public_note: None, - receiver_identity_id: String::new(), - amount, - amount_input: None, - transfer_tokens_status: TransferTokensStatus::NotStarted, - max_amount, - app_context: app_context.clone(), - confirmation_dialog: None, - selected_wallet, - wallet_password: String::new(), - show_password: false, - } - } - - fn render_amount_input(&mut self, ui: &mut Ui) { - ui.label(format!("Available balance: {}", self.max_amount)); - ui.add_space(5.0); - - // Lazy initialization with proper decimal places - let amount_input = match self.amount_input.as_mut() { - Some(input) => input, - _ => { - self.amount_input = Some( - AmountInput::new( - self.amount - .as_ref() - .unwrap_or(&Amount::from(&self.identity_token_balance)), - ) - .with_label("Amount:") - .with_max_button(true), - ); - - self.amount_input - .as_mut() - .expect("AmountInput should be initialized above") - } - }; - - // Check if input should be disabled when operation is in progress - let enabled = match self.transfer_tokens_status { - TransferTokensStatus::WaitingForResult(_) | TransferTokensStatus::Complete => false, - TransferTokensStatus::NotStarted | TransferTokensStatus::ErrorMessage(_) => { - amount_input.set_max_amount(Some(self.max_amount.value())); - true - } - }; - - let response = ui.add_enabled_ui(enabled, |ui| amount_input.show(ui)).inner; - - response.inner.update(&mut self.amount); - // errors are handled inside AmountInput - } - - fn render_to_identity_input(&mut self, ui: &mut Ui) { - let _response = ui.add( - IdentitySelector::new( - "transfer_recipient_selector", - &mut self.receiver_identity_id, - &self.known_identities, - ) - .width(300.0) - .label("Recipient:") - .exclude(&[self.identity.identity.id()]), - ); - } - - fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let msg = format!( - "Are you sure you want to transfer {} tokens to {}?", - self.amount.unwrap_or(0), - self.receiver_identity_id - ); - - let confirmation_dialog = self.confirmation_dialog.get_or_insert_with(|| { - ConfirmationDialog::new("Confirm Transfer", msg) - .confirm_text(Some("Transfer")) - .cancel_text(Some("Cancel")) - }); - - let response = confirmation_dialog.show(ui); - match response.inner.dialog_response { - Some(ConfirmationStatus::Confirmed) => { - self.confirmation_dialog = None; - self.confirmation_ok() - }, - Some(ConfirmationStatus::Canceled) => { - self.confirmation_dialog = None; - AppAction::None - }, - None => AppAction::None, - } - } - - fn confirmation_ok(&mut self) -> AppAction { - if self.amount.is_none() || self.amount == Some(0) { - self.status = TransferTokensStatus::ErrorMessage("Invalid amount".into()); - self.error_message = Some("Invalid amount".into()); - return AppAction::None; - } - - let parsed_receiver_id = Identifier::from_string_try_encodings( - &self.receiver_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - - if parsed_receiver_id.is_err() { - self.status = TransferTokensStatus::ErrorMessage("Invalid receiver".into()); - self.error_message = Some("Invalid receiver".into()); - return AppAction::None; - } - - let receiver_id = parsed_receiver_id.unwrap(); - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = TransferTokensStatus::WaitingForResult(now); - - let data_contract = Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::TransferTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - receiver_identity: receiver_id, - amount: self.amount.unwrap_or(0), - group_info, - }, - ))) - } - pub fn show_success(&self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - - // Center the content vertically and horizontally - ui.vertical_centered(|ui| { - ui.add_space(50.0); - - ui.heading("🎉"); - ui.heading("Success!"); - - ui.add_space(20.0); - - // Display the "Back to Identities" button - if ui.button("Back to Tokens").clicked() { - // Handle navigation back to the identities screen - action |= AppAction::PopScreenAndRefresh; - } - }); - - action - } -} - -impl ScreenLike for TransferTokensScreen { - fn display_message(&mut self, message: &str, message_type: MessageType) { - match message_type { - MessageType::Success => { - if message == "TransferTokens" { - self.transfer_tokens_status = TransferTokensStatus::Complete; - } - } - MessageType::Info => {} - MessageType::Error => { - // It's not great because the error message can be coming from somewhere else if there are other processes happening - self.transfer_tokens_status = - TransferTokensStatus::ErrorMessage(message.to_string()); - } - } - } - - fn refresh(&mut self) { - // Refresh the identity because there might be new keys - self.identity = self - .app_context - .load_local_qualified_identities() - .unwrap() - .into_iter() - .find(|identity| identity.identity.id() == self.identity.identity.id()) - .unwrap(); - let token_balances = self - .app_context - .db - .get_identity_token_balances(&self.app_context) - .expect("Token balances not loaded"); - self.max_amount = token_balances - .values() - .find(|balance| balance.identity_id == self.identity.identity.id()) - .map(Amount::from) - .unwrap_or_default(); - } - - /// Renders the UI components for the withdrawal screen - fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - ( - &self.identity_token_balance.token_alias, - AppAction::PopScreen, - ), - ("Transfer", AppAction::None), - ], - vec![], - ); - - // Left panel - action |= add_left_panel( - ctx, - &self.app_context, - crate::ui::RootScreenType::RootScreenMyTokenBalances, - ); - - // Subscreen chooser - action |= add_tokens_subscreen_chooser_panel(ctx, &self.app_context); - - let central_panel_action = island_central_panel(ctx, |ui| { - let dark_mode = ui.ctx().style().visuals.dark_mode; - - // Show the success screen if the transfer was successful - if self.transfer_tokens_status == TransferTokensStatus::Complete { - return self.show_success(ui); - } - - ui.heading(format!( - "Transfer {}", - self.identity_token_balance.token_alias - )); - ui.add_space(10.0); - - let has_keys = if self.app_context.is_developer_mode() { - !self.identity.identity.public_keys().is_empty() - } else { - !self - .identity - .available_authentication_keys_with_critical_security_level() - .is_empty() - }; - - if !has_keys { - ui.colored_label( - DashColors::error_color(dark_mode), - format!( - "You do not have any authentication keys with CRITICAL security level loaded for this {} identity.", - self.identity.identity_type - ), - ); - ui.add_space(10.0); - - let key = self.identity.identity.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); - - if let Some(key) = key { - if ui.button("Check Keys").clicked() { - return AppAction::AddScreen(Screen::KeyInfoScreen(KeyInfoScreen::new( - self.identity.clone(), - key.clone(), - None, - &self.app_context, - ))); - } - ui.add_space(5.0); - } - - if ui.button("Add key").clicked() { - return AppAction::AddScreen(Screen::AddKeyScreen(AddKeyScreen::new( - self.identity.clone(), - &self.app_context, - ))); - } - } else { - if self.selected_wallet.is_some() { - let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); - - if needed_unlock && !just_unlocked { - return AppAction::None; - } - } - - // Select the key to sign with - ui.heading("1. Select the key to sign the transaction with"); - ui.add_space(10.0); - - let mut selected_identity = Some(self.identity.clone()); - add_identity_key_chooser( - ui, - &self.app_context, - std::iter::once(&self.identity), - &mut selected_identity, - &mut self.selected_key, - TransactionType::TokenTransfer, - ); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - // Input the amount to transfer - ui.heading("2. Input the amount to transfer"); - ui.add_space(5.0); - - self.render_amount_input(ui); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - // Input the ID of the identity to transfer to - ui.heading("3. ID of the identity to transfer to"); - ui.add_space(5.0); - self.render_to_identity_input(ui); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - // Render text input for the public note - ui.heading("4. Public note (optional)"); - ui.add_space(5.0); - ui.horizontal(|ui| { - ui.label("Public note (optional):"); - ui.add_space(10.0); - let mut txt = self.public_note.clone().unwrap_or_default(); - if ui - .text_edit_singleline(&mut txt) - .on_hover_text( - "A note about the transaction that can be seen by the public.", - ) - .changed() - { - self.public_note = Some(txt); - } - }); - ui.add_space(10.0); - - // Transfer button - - let ready = self.amount.is_some() - && !self.receiver_identity_id.is_empty() - && self.selected_key.is_some(); - let mut new_style = (**ui.style()).clone(); - new_style.spacing.button_padding = egui::vec2(10.0, 5.0); - ui.set_style(new_style); - let button = egui::Button::new(RichText::new("Transfer").color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .frame(true) - .corner_radius(3.0); - if ui - .add_enabled(ready, button) - .on_disabled_hover_text("Please ensure all fields are filled correctly") - .clicked() - { - // Use the amount value directly since it's already parsed - if self.amount.as_ref().is_some_and(|v| v > &self.max_amount) { - self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( - "Amount exceeds available balance".to_string(), - ); - } else if self.amount.as_ref().is_none_or(|a| a.value() == 0) { - self.transfer_tokens_status = TransferTokensStatus::ErrorMessage( - "Amount must be greater than zero".to_string(), - ); - } else { - self.confirmation_popup = true; - } - } - - if self.confirmation_popup { - return self.show_confirmation_popup(ui); - } - - // Handle transfer status messages - ui.add_space(5.0); - match &self.transfer_tokens_status { - TransferTokensStatus::NotStarted => { - // Do nothing - } - TransferTokensStatus::WaitingForResult(start_time) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - let elapsed_seconds = now - start_time; - - let display_time = if elapsed_seconds < 60 { - format!( - "{} second{}", - elapsed_seconds, - if elapsed_seconds == 1 { "" } else { "s" } - ) - } else { - let minutes = elapsed_seconds / 60; - let seconds = elapsed_seconds % 60; - format!( - "{} minute{} and {} second{}", - minutes, - if minutes == 1 { "" } else { "s" }, - seconds, - if seconds == 1 { "" } else { "s" } - ) - }; - - ui.label(format!( - "Transferring... Time taken so far: {}", - display_time - )); - } - TransferTokensStatus::ErrorMessage(msg) => { - ui.colored_label( - DashColors::error_color(dark_mode), - format!("Error: {}", msg), - ); - } - TransferTokensStatus::Complete => { - // Handled above - } - } - } - - AppAction::None - }); - action |= central_panel_action; - action - } -} - -impl ScreenWithWalletUnlock for TransferTokensScreen { - fn selected_wallet_ref(&self) -> &Option>> { - &self.selected_wallet - } - - fn wallet_password_ref(&self) -> &String { - &self.wallet_password - } - - fn wallet_password_mut(&mut self) -> &mut String { - &mut self.wallet_password - } - - fn show_password(&self) -> bool { - self.show_password - } - - fn show_password_mut(&mut self) -> &mut bool { - &mut self.show_password - } - - fn set_error_message(&mut self, error_message: Option) { - if let Some(error_message) = error_message { - self.transfer_tokens_status = TransferTokensStatus::ErrorMessage(error_message); - } - } - - fn error_message(&self) -> Option<&String> { - if let TransferTokensStatus::ErrorMessage(error_message) = &self.transfer_tokens_status { - Some(error_message) - } else { - None - } - } -} diff --git a/src/ui/tokens/unfreeze_tokens_screen.rs.bak b/src/ui/tokens/unfreeze_tokens_screen.rs.bak deleted file mode 100644 index 4b312a2cd..000000000 --- a/src/ui/tokens/unfreeze_tokens_screen.rs.bak +++ /dev/null @@ -1,694 +0,0 @@ -use super::tokens_screen::IdentityTokenInfo; -use crate::app::AppAction; -use crate::backend_task::BackendTask; -use crate::backend_task::tokens::TokenTask; -use crate::context::AppContext; -use crate::model::qualified_identity::QualifiedIdentity; -use crate::model::wallet::Wallet; -use crate::ui::components::component_trait::Component; -use crate::ui::components::confirmation_dialog::{ConfirmationDialog, ConfirmationStatus}; -use crate::ui::components::identity_selector::IdentitySelector; -use crate::ui::components::left_panel::add_left_panel; -use crate::ui::components::styled::island_central_panel; -use crate::ui::components::tokens_subscreen_chooser_panel::add_tokens_subscreen_chooser_panel; -use crate::ui::components::top_panel::add_top_panel; -use crate::ui::components::wallet_unlock::ScreenWithWalletUnlock; -use crate::ui::contracts_documents::group_actions_screen::GroupActionsScreen; -use crate::ui::helpers::{TransactionType, add_identity_key_chooser, render_group_action_text}; -use crate::ui::identities::get_selected_wallet; -use crate::ui::identities::keys::add_key_screen::AddKeyScreen; -use crate::ui::identities::keys::key_info_screen::KeyInfoScreen; -use crate::ui::{MessageType, RootScreenType, Screen, ScreenLike}; -use dash_sdk::dpp::data_contract::GroupContractPosition; -use dash_sdk::dpp::data_contract::accessors::v0::DataContractV0Getters; -use dash_sdk::dpp::data_contract::accessors::v1::DataContractV1Getters; -use dash_sdk::dpp::data_contract::associated_token::token_configuration::accessors::v0::TokenConfigurationV0Getters; -use dash_sdk::dpp::data_contract::change_control_rules::authorized_action_takers::AuthorizedActionTakers; -use dash_sdk::dpp::data_contract::group::Group; -use dash_sdk::dpp::data_contract::group::accessors::v0::GroupV0Getters; -use dash_sdk::dpp::group::GroupStateTransitionInfo; -use dash_sdk::dpp::group::GroupStateTransitionInfoStatus; -use dash_sdk::dpp::identity::accessors::IdentityGettersV0; -use dash_sdk::dpp::identity::{KeyType, Purpose, SecurityLevel}; -use dash_sdk::platform::{Identifier, IdentityPublicKey}; -use eframe::egui::{self, Color32, Context, Ui}; -use egui::RichText; -use std::collections::HashSet; -use std::sync::{Arc, RwLock}; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// The states for the unfreeze flow -#[derive(PartialEq)] -pub enum UnfreezeTokensStatus { - NotStarted, - WaitingForResult(u64), - ErrorMessage(String), - Complete, -} - -/// A screen that allows unfreezing a previously frozen identity's tokens for a specific contract -pub struct UnfreezeTokensScreen { - pub identity: QualifiedIdentity, - pub identity_token_info: IdentityTokenInfo, - selected_key: Option, - pub public_note: Option, - - group: Option<(GroupContractPosition, Group)>, - is_unilateral_group_member: bool, - pub group_action_id: Option, - /// A list of identities that are frozen and can be unfrozen. - /// - /// TODO: Right now it is just a list of all identities, but it should be filtered to only show frozen ones. - frozen_identities: Vec, - - /// The identity we want to freeze - pub unfreeze_identity_id: String, - - status: UnfreezeTokensStatus, - error_message: Option, - - // Basic references - pub app_context: Arc, - - // Confirmation dialog - confirmation_dialog: Option, - - // If password-based wallet unlocking is needed - selected_wallet: Option>>, - wallet_password: String, - show_password: bool, -} - -impl UnfreezeTokensScreen { - pub fn new(identity_token_info: IdentityTokenInfo, app_context: &Arc) -> Self { - // TODO: filter to include only frozen identities - let frozen_identities = app_context - .load_local_qualified_identities() - .expect("Identities not loaded"); - - let possible_key = identity_token_info - .identity - .identity - .get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ) - .cloned(); - - let mut error_message = None; - - let group = match identity_token_info - .token_config - .unfreeze_rules() - .authorized_to_make_change_action_takers() - { - AuthorizedActionTakers::NoOne => { - error_message = Some("Burning is not allowed on this token".to_string()); - None - } - AuthorizedActionTakers::ContractOwner => { - if identity_token_info.data_contract.contract.owner_id() - != identity_token_info.identity.identity.id() - { - error_message = Some( - "You are not allowed to burn this token. Only the contract owner is." - .to_string(), - ); - } - None - } - AuthorizedActionTakers::Identity(identifier) => { - if identifier != &identity_token_info.identity.identity.id() { - error_message = Some("You are not allowed to burn this token".to_string()); - } - None - } - AuthorizedActionTakers::MainGroup => { - match identity_token_info.token_config.main_control_group() { - None => { - error_message = Some( - "Invalid contract: No main control group, though one should exist" - .to_string(), - ); - None - } - Some(group_pos) => { - match identity_token_info - .data_contract - .contract - .expected_group(group_pos) - { - Ok(group) => Some((group_pos, group.clone())), - Err(e) => { - error_message = Some(format!("Invalid contract: {}", e)); - None - } - } - } - } - } - AuthorizedActionTakers::Group(group_pos) => { - match identity_token_info - .data_contract - .contract - .expected_group(*group_pos) - { - Ok(group) => Some((*group_pos, group.clone())), - Err(e) => { - error_message = Some(format!("Invalid contract: {}", e)); - None - } - } - } - }; - - let mut is_unilateral_group_member = false; - if group.is_some() { - if let Some((_, group)) = group.clone() { - let your_power = group - .members() - .get(&identity_token_info.identity.identity.id()); - - if let Some(your_power) = your_power { - if your_power >= &group.required_power() { - is_unilateral_group_member = true; - } - } - } - }; - - // Attempt to get an unlocked wallet reference - let selected_wallet = get_selected_wallet( - &identity_token_info.identity, - None, - possible_key.as_ref(), - &mut error_message, - ); - - Self { - identity: identity_token_info.identity.clone(), - identity_token_info, - selected_key: possible_key, - group, - is_unilateral_group_member, - group_action_id: None, - public_note: None, - unfreeze_identity_id: String::new(), - status: UnfreezeTokensStatus::NotStarted, - error_message, - app_context: app_context.clone(), - confirmation_dialog: None, - selected_wallet, - wallet_password: String::new(), - show_password: false, - frozen_identities, - } - } - - fn render_unfreeze_identity_input(&mut self, ui: &mut Ui) { - let _response = ui.add( - IdentitySelector::new( - "unfreeze_identity_selector", - &mut self.unfreeze_identity_id, - &self.frozen_identities, - ) - .width(300.0) - .label("Identity ID to unfreeze:"), - ); - } - - fn show_confirmation_popup(&mut self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - let mut is_open = true; - egui::Window::new("Confirm Unfreeze") - .collapsible(false) - .open(&mut is_open) - .show(ui.ctx(), |ui| { - // Validate user input - let parsed = Identifier::from_string_try_encodings( - &self.unfreeze_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - if parsed.is_err() { - self.error_message = Some("Please enter a valid identity ID.".into()); - self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); - self.show_confirmation_popup = false; - return; - } - let unfreeze_id = parsed.unwrap(); - - ui.label(format!( - "Are you sure you want to unfreeze identity {}?", - self.unfreeze_identity_id - )); - - ui.add_space(10.0); - - // Confirm - if ui.button("Confirm").clicked() { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = UnfreezeTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch to backend - action |= AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::UnfreezeTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - unfreeze_identity: unfreeze_id, - group_info, - }, - ))); - } - - // Cancel - if ui.button("Cancel").clicked() { - self.show_confirmation_popup = false; - } - }); - - if !is_open { - self.show_confirmation_popup = false; - } - action - } - - fn confirmation_ok(&mut self) -> AppAction { - // Validate user input - let parsed = Identifier::from_string_try_encodings( - &self.unfreeze_identity_id, - &[ - dash_sdk::dpp::platform_value::string_encoding::Encoding::Base58, - dash_sdk::dpp::platform_value::string_encoding::Encoding::Hex, - ], - ); - if parsed.is_err() { - self.error_message = Some("Please enter a valid identity ID.".into()); - self.status = UnfreezeTokensStatus::ErrorMessage("Invalid identity ID".into()); - return AppAction::None; - } - let unfreeze_id = parsed.unwrap(); - - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - self.status = UnfreezeTokensStatus::WaitingForResult(now); - - // Grab the data contract for this token from the app context - let data_contract = - Arc::new(self.identity_token_info.data_contract.contract.clone()); - - let group_info = if self.group_action_id.is_some() { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoOtherSigner( - GroupStateTransitionInfo { - group_contract_position: *pos, - action_id: self.group_action_id.unwrap(), - action_is_proposer: false, - }, - ) - }) - } else { - self.group.as_ref().map(|(pos, _)| { - GroupStateTransitionInfoStatus::GroupStateTransitionInfoProposer(*pos) - }) - }; - - // Dispatch to backend - AppAction::BackendTask(BackendTask::TokenTask(Box::new( - TokenTask::UnfreezeTokens { - actor_identity: self.identity.clone(), - data_contract, - token_position: self.identity_token_info.token_position, - signing_key: self.selected_key.clone().expect("No key selected"), - public_note: if self.group_action_id.is_some() { - None - } else { - self.public_note.clone() - }, - unfreeze_identity: unfreeze_id, - group_info, - }, - ))) - } - - fn show_success_screen(&self, ui: &mut Ui) -> AppAction { - let mut action = AppAction::None; - ui.vertical_centered(|ui| { - ui.add_space(50.0); - - ui.heading("🎉"); - if self.group_action_id.is_some() { - // This is already initiated by the group, we are just signing it - ui.heading("Group Unfreeze Signing Successful."); - } else if !self.is_unilateral_group_member && self.group.is_some() { - ui.heading("Group Unfreeze Initiated."); - } else { - ui.heading("Unfroze Identity Successfully."); - } - - ui.add_space(20.0); - - if self.group_action_id.is_some() { - if ui.button("Back to Group Actions").clicked() { - action |= AppAction::PopScreenAndRefresh; - } - if ui.button("Back to Tokens").clicked() { - action |= AppAction::SetMainScreenThenGoToMainScreen( - RootScreenType::RootScreenMyTokenBalances, - ); - } - } else { - if ui.button("Back to Tokens").clicked() { - action |= AppAction::PopScreenAndRefresh; - } - - if !self.is_unilateral_group_member && ui.button("Go to Group Actions").clicked() { - action |= AppAction::PopThenAddScreenToMainScreen( - RootScreenType::RootScreenDocumentQuery, - Screen::GroupActionsScreen(GroupActionsScreen::new( - &self.app_context.clone(), - )), - ); - } - } - }); - action - } -} - -impl ScreenLike for UnfreezeTokensScreen { - fn display_message(&mut self, message: &str, message_type: MessageType) { - match message_type { - MessageType::Success => { - // Possibly "UnfreezeTokens" or something else from your backend - if message.contains("Successfully unfroze identity") || message == "UnfreezeTokens" - { - self.status = UnfreezeTokensStatus::Complete; - } - } - MessageType::Error => { - self.status = UnfreezeTokensStatus::ErrorMessage(message.to_string()); - self.error_message = Some(message.to_string()); - } - MessageType::Info => {} - } - } - - fn refresh(&mut self) { - if let Ok(all_identities) = self.app_context.load_local_user_identities() { - if let Some(updated_identity) = all_identities - .into_iter() - .find(|id| id.identity.id() == self.identity.identity.id()) - { - self.identity = updated_identity; - } - } - } - - fn ui(&mut self, ctx: &Context) -> AppAction { - let mut action; - - // Build a top panel - if self.group_action_id.is_some() { - action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Contracts", AppAction::GoToMainScreen), - ("Group Actions", AppAction::PopScreen), - ("Unfreeze", AppAction::None), - ], - vec![], - ); - } else { - action = add_top_panel( - ctx, - &self.app_context, - vec![ - ("Tokens", AppAction::GoToMainScreen), - (&self.identity_token_info.token_alias, AppAction::PopScreen), - ("Unfreeze", AppAction::None), - ], - vec![], - ); - } - - // Left panel - action |= add_left_panel( - ctx, - &self.app_context, - crate::ui::RootScreenType::RootScreenMyTokenBalances, - ); - - // Subscreen chooser - action |= add_tokens_subscreen_chooser_panel(ctx, &self.app_context); - - island_central_panel(ctx, |ui| { - if self.status == UnfreezeTokensStatus::Complete { - action |= self.show_success_screen(ui); - return; - } - - ui.heading("Unfreeze a Frozen Identity’s Tokens"); - ui.add_space(10.0); - - // Check if user has any auth keys - let has_keys = if self.app_context.is_developer_mode() { - !self.identity.identity.public_keys().is_empty() - } else { - !self - .identity - .available_authentication_keys_with_critical_security_level() - .is_empty() - }; - - if !has_keys { - ui.colored_label( - Color32::RED, - format!( - "No authentication keys with CRITICAL security level found for this {} identity.", - self.identity.identity_type, - ), - ); - ui.add_space(10.0); - - // Show "Add key" or "Check keys" option - let first_key = self.identity.identity.get_first_public_key_matching( - Purpose::AUTHENTICATION, - HashSet::from([SecurityLevel::CRITICAL]), - KeyType::all_key_types().into(), - false, - ); - - if let Some(key) = first_key { - if ui.button("Check Keys").clicked() { - action |= AppAction::AddScreen(Screen::KeyInfoScreen(KeyInfoScreen::new( - self.identity.clone(), - key.clone(), - None, - &self.app_context, - ))); - } - ui.add_space(5.0); - } - - if ui.button("Add key").clicked() { - action |= AppAction::AddScreen(Screen::AddKeyScreen(AddKeyScreen::new( - self.identity.clone(), - &self.app_context, - ))); - } - } else { - // Possibly handle locked wallet scenario - if self.selected_wallet.is_some() { - let (needed_unlock, just_unlocked) = self.render_wallet_unlock_if_needed(ui); - - if needed_unlock && !just_unlocked { - return; - } - } - - // 1) Key selection - ui.heading("1. Select the key to sign the Unfreeze transition"); - ui.add_space(10.0); - - let mut selected_identity = Some(self.identity.clone()); - add_identity_key_chooser( - ui, - &self.app_context, - std::iter::once(&self.identity), - &mut selected_identity, - &mut self.selected_key, - TransactionType::TokenAction, - ); - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - // 2) Identity to unfreeze - ui.heading("2. Enter the identity ID to unfreeze"); - ui.add_space(5.0); - if self.group_action_id.is_some() { - ui.label( - "You are signing an existing group Unfreeze so you are not allowed to choose the identity.", - ); - ui.add_space(5.0); - ui.label(format!("Identity: {}", self.unfreeze_identity_id)); - } else { - self.render_unfreeze_identity_input(ui); - } - - ui.add_space(10.0); - ui.separator(); - ui.add_space(10.0); - - // Render text input for the public note - ui.heading("3. Public note (optional)"); - ui.add_space(5.0); - if self.group_action_id.is_some() { - ui.label( - "You are signing an existing group Mint so you are not allowed to put a note.", - ); - ui.add_space(5.0); - ui.label(format!( - "Note: {}", - self.public_note.clone().unwrap_or("None".to_string()) - )); - } else { - ui.horizontal(|ui| { - ui.label("Public note (optional):"); - ui.add_space(10.0); - let mut txt = self.public_note.clone().unwrap_or_default(); - if ui - .text_edit_singleline(&mut txt) - .on_hover_text( - "A note about the transaction that can be seen by the public.", - ) - .changed() - { - self.public_note = if !txt.is_empty() { Some(txt) } else { None }; - } - }); - } - - let button_text = render_group_action_text( - ui, - &self.group, - &self.identity_token_info, - "Unfreeze", - &self.group_action_id, - ); - - // Unfreeze button - if self.app_context.is_developer_mode() || !button_text.contains("Test") { - ui.add_space(10.0); - let button = - egui::Button::new(RichText::new(button_text).color(Color32::WHITE)) - .fill(Color32::from_rgb(0, 128, 255)) - .corner_radius(3.0); - - if ui.add(button).clicked() { - // Initialize confirmation dialog when button is clicked - self.confirmation_dialog = None; // Reset for fresh dialog - } - } - - // Show confirmation dialog if it exists - if self.confirmation_dialog.is_some() { - action |= self.show_confirmation_popup(ui); - } - - // Show in-progress or error messages - ui.add_space(10.0); - match &self.status { - UnfreezeTokensStatus::NotStarted => { - // no-op - } - UnfreezeTokensStatus::WaitingForResult(start_time) => { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("Time went backwards") - .as_secs(); - let elapsed = now - start_time; - ui.label(format!("Unfreezing... elapsed: {}s", elapsed)); - } - UnfreezeTokensStatus::ErrorMessage(msg) => { - ui.colored_label(Color32::RED, format!("Error: {}", msg)); - } - UnfreezeTokensStatus::Complete => { - // handled above - } - } - } - }); - - action - } -} - -impl ScreenWithWalletUnlock for UnfreezeTokensScreen { - fn selected_wallet_ref(&self) -> &Option>> { - &self.selected_wallet - } - - fn wallet_password_ref(&self) -> &String { - &self.wallet_password - } - - fn wallet_password_mut(&mut self) -> &mut String { - &mut self.wallet_password - } - - fn show_password(&self) -> bool { - self.show_password - } - - fn show_password_mut(&mut self) -> &mut bool { - &mut self.show_password - } - - fn set_error_message(&mut self, error_message: Option) { - self.error_message = error_message; - } - - fn error_message(&self) -> Option<&String> { - self.error_message.as_ref() - } -}