Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/ui/components/amount_input.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::model::amount::Amount;
use crate::ui::components::{Component, ComponentResponse};
use dash_sdk::dpp::balances::credits::MAX_CREDITS;
use dash_sdk::dpp::fee::Credits;
use egui::{InnerResponse, Response, TextEdit, Ui, Vec2, WidgetText};

Expand Down Expand Up @@ -113,7 +114,7 @@ impl AmountInput {
unit_name: amount.unit_name().map(|s| s.to_string()),
label: None,
hint_text: None,
max_amount: None,
max_amount: Some(MAX_CREDITS),
min_amount: Some(1), // Default minimum is 1 (greater than zero)
show_max_button: false,
desired_width: None,
Expand Down Expand Up @@ -176,6 +177,17 @@ impl AmountInput {
self
}

/// Sets value of the input field.
///
/// This will update the internal state and mark the component as changed.
pub fn set_value(&mut self, value: Amount) -> &mut Self {
self.amount_str = value.to_string_without_unit();
self.decimal_places = value.decimal_places();
self.unit_name = value.unit_name().map(|s| s.to_string());
self.changed = true; // Mark as changed to trigger validation
self
}

/// Sets the hint text for the input field.
pub fn with_hint_text<T: Into<WidgetText>>(mut self, hint_text: T) -> Self {
self.hint_text = Some(hint_text.into());
Expand All @@ -197,6 +209,8 @@ impl AmountInput {

/// Sets the maximum amount allowed (mutable reference version).
/// Use this for dynamic configuration when the max amount changes at runtime (e.g., balance updates).
///
/// Defaults to [`MAX_CREDITS`](dash_sdk::dpp::balances::credits::MAX_CREDITS).
pub fn set_max_amount(&mut self, max_amount: Option<Credits>) -> &mut Self {
self.max_amount = max_amount;
self
Expand Down
4 changes: 3 additions & 1 deletion src/ui/contracts_documents/group_actions_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,9 @@ impl GroupActionsScreen {
}
TokenEvent::ChangePriceForDirectPurchase(schedule, note_opt) => {
let mut change_price_screen =
SetTokenPriceScreen::new(identity_token_info, &self.app_context);
SetTokenPriceScreen::new(identity_token_info, &self.app_context)
.with_schedule(schedule.clone());

change_price_screen.group_action_id = Some(action_id);
change_price_screen.token_pricing_schedule = format!("{:?}", schedule);
change_price_screen.public_note = note_opt.clone();
Expand Down
129 changes: 87 additions & 42 deletions src/ui/tokens/direct_token_purchase_screen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::sync::{Arc, RwLock};
use std::time::{SystemTime, UNIX_EPOCH};

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_configuration_convention::accessors::v0::TokenConfigurationConventionV0Getters;
use dash_sdk::dpp::fee::Credits;
use dash_sdk::dpp::tokens::token_pricing_schedule::TokenPricingSchedule;
use eframe::egui::{self, Color32, Context, Ui};
Expand All @@ -13,12 +15,15 @@ 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, DASH_DECIMAL_PLACES};
use crate::model::wallet::Wallet;
use crate::ui::components::amount_input::AmountInput;
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::get_selected_wallet;
use crate::ui::identities::keys::add_key_screen::AddKeyScreen;
Expand All @@ -45,11 +50,11 @@ pub struct PurchaseTokenScreen {
pub identity_token_info: IdentityTokenInfo,
selected_key: Option<IdentityPublicKey>,

// Specific to this transition
amount_to_purchase: String,
total_agreed_price: String,
// Specific to this transition - using AmountInput components following design pattern
amount_to_purchase_input: Option<AmountInput>,
amount_to_purchase_value: Option<Amount>,
fetched_pricing_schedule: Option<TokenPricingSchedule>,
calculated_price: Option<Credits>,
calculated_price_credits: Option<Credits>,
pricing_fetch_attempted: bool,

/// Screen stuff
Expand Down Expand Up @@ -89,10 +94,10 @@ impl PurchaseTokenScreen {
Self {
identity_token_info,
selected_key: possible_key,
amount_to_purchase: "".to_string(),
total_agreed_price: "".to_string(),
amount_to_purchase_input: None,
amount_to_purchase_value: None,
fetched_pricing_schedule: None,
calculated_price: None,
calculated_price_credits: None,
pricing_fetch_attempted: false,
status: PurchaseTokensStatus::NotStarted,
error_message: None,
Expand All @@ -104,16 +109,33 @@ impl PurchaseTokenScreen {
}
}

/// Renders a text input for the user to specify an amount to purchase
/// Renders AmountInput components for the user to specify an amount to purchase
fn render_amount_input(&mut self, ui: &mut Ui) -> AppAction {
let mut action = AppAction::None;

ui.horizontal(|ui| {
ui.label("Amount to Purchase:");
let response = ui.text_edit_singleline(&mut self.amount_to_purchase);
// Use AmountInput for token amount with lazy initialization
let amount_input = self.amount_to_purchase_input.get_or_insert_with(|| {
AmountInput::new(
Amount::new(
0,
self.identity_token_info
.token_config
.conventions()
.decimals(),
)
.with_unit_name(&self.identity_token_info.token_alias),
)
.with_label("Amount to Purchase:")
.with_hint_text("Enter token amount to purchase")
.with_min_amount(Some(1))
});

let response = amount_input.show(ui);
response.inner.update(&mut self.amount_to_purchase_value);

// When amount changes, recalculate the price if we have pricing schedule
if response.changed() {
// When amount changes, update domain data and recalculate the price
if response.inner.has_changed() {
self.recalculate_price();
}

Expand Down Expand Up @@ -143,13 +165,24 @@ impl PurchaseTokenScreen {
ui.add_space(5.0);
ui.label("Current pricing:");
match pricing_schedule {
TokenPricingSchedule::SinglePrice(price) => {
ui.label(format!(" Fixed price: {} credits per token", price));
TokenPricingSchedule::SinglePrice(price_credits) => {
let price =
Amount::new(*price_credits, DASH_DECIMAL_PLACES).with_unit_name("DASH");
ui.label(format!(
" Fixed price: {} ({} credits) per token",
price, price_credits
));
}
TokenPricingSchedule::SetPrices(tiers) => {
ui.label(" Tiered pricing:");
for (amount, price) in tiers {
ui.label(format!(" {} tokens: {} credits each", amount, price));
for (amount_value, price_credits) in tiers {
let amount = Amount::from_token(&self.identity_token_info, *amount_value);
let price =
Amount::new(*price_credits, DASH_DECIMAL_PLACES).with_unit_name("DASH");
ui.label(format!(
" {} tokens: {} ({} credits) each",
amount, price, price_credits
));
}
}
}
Expand All @@ -160,10 +193,11 @@ impl PurchaseTokenScreen {

/// Recalculates the total price based on amount and pricing schedule
fn recalculate_price(&mut self) {
if let (Some(pricing_schedule), Ok(amount)) = (
if let (Some(pricing_schedule), Some(amount_value)) = (
&self.fetched_pricing_schedule,
self.amount_to_purchase.parse::<u64>(),
&self.amount_to_purchase_value,
) {
let amount = amount_value.value();
let price_per_token = match pricing_schedule {
TokenPricingSchedule::SinglePrice(price) => *price,
TokenPricingSchedule::SetPrices(tiers) => {
Expand All @@ -179,10 +213,9 @@ impl PurchaseTokenScreen {
};

let total_price = amount.saturating_mul(price_per_token);
self.calculated_price = Some(total_price);
self.total_agreed_price = total_price.to_string();
self.calculated_price_credits = Some(total_price);
} else {
self.calculated_price = None;
self.calculated_price_credits = None;
}
}

Expand All @@ -206,27 +239,29 @@ impl PurchaseTokenScreen {
)
.show(ui.ctx(), |ui| {
// Validate user input
let amount_ok = self.amount_to_purchase.parse::<u64>().ok();
if amount_ok.is_none() {
let amount_value = self.amount_to_purchase_value.as_ref();
let Some(amount) = amount_value else {
self.error_message = Some("Please enter a valid amount.".into());
self.status = PurchaseTokensStatus::ErrorMessage("Invalid amount".into());
self.show_confirmation_popup = false;
return;
}
};

let total_agreed_price_ok: Option<Credits> =
self.total_agreed_price.parse::<u64>().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());
let Some(total_price_credits) = self.calculated_price_credits else {
self.error_message = Some(
"Cannot calculate total price. Please fetch token pricing first.".into(),
);
self.status = PurchaseTokensStatus::ErrorMessage("No pricing fetched".into());
self.show_confirmation_popup = false;
return;
}
};

let total_price_dash =
Amount::new(total_price_credits, DASH_DECIMAL_PLACES).with_unit_name("DASH");

ui.label(format!(
"Are you sure you want to purchase {} token(s) for {} Credits?",
self.amount_to_purchase, self.total_agreed_price
"Are you sure you want to purchase {} token(s) for {} ({} Credits)?",
amount, total_price_dash, total_price_credits
));

ui.add_space(10.0);
Expand All @@ -250,9 +285,8 @@ impl PurchaseTokenScreen {
),
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"),
amount: amount.value(),
total_agreed_price: total_price_credits,
})),
BackendTask::TokenTask(Box::new(TokenTask::QueryMyTokenBalances)),
],
Expand Down Expand Up @@ -474,12 +508,17 @@ impl ScreenLike for PurchaseTokenScreen {

ui.add_space(10.0);

// Display calculated price
if let Some(calculated_price) = self.calculated_price {
// Display calculated price and total agreed price input
if let Some(calculated_price_credits) = self.calculated_price_credits {
ui.group(|ui| {
ui.heading("Calculated total price:");
ui.label(format!("{} credits", calculated_price));
let dash_amount = Amount::new(calculated_price_credits, DASH_DECIMAL_PLACES)
.with_unit_name("DASH");
ui.label(format!("{} DASH ({} credits)",dash_amount, calculated_price_credits));
ui.label("Note: This is the calculated price based on the current pricing schedule.");

ui.add_space(10.0);

});
} else if self.fetched_pricing_schedule.is_some() {
ui.colored_label(
Expand All @@ -494,9 +533,15 @@ impl ScreenLike for PurchaseTokenScreen {
ui.separator();
ui.add_space(10.0);

// Purchase button (disabled if no pricing is available)
let can_purchase =
self.fetched_pricing_schedule.is_some() && self.calculated_price.is_some();
// Purchase button (disabled if no valid amounts are available)
let can_purchase = self.fetched_pricing_schedule.is_some()
&& self.calculated_price_credits.unwrap_or_default() > 0
&& self
.amount_to_purchase_value
.as_ref()
.map(|v| v.value())
.unwrap_or_default()
> 0;
let purchase_text = "Purchase".to_string();

if can_purchase {
Expand Down
Loading