diff --git a/public/styles/main.scss b/public/styles/main.scss index 95a819e..5d173ec 100644 --- a/public/styles/main.scss +++ b/public/styles/main.scss @@ -1454,6 +1454,8 @@ $fw-bold: 700; border-radius: 12px; list-style: none; z-index: 10; + max-height: 30vh; + overflow: auto; } .dropdown--right .dropdown__list { @@ -2376,6 +2378,21 @@ textarea::placeholder { width: max-content; } +.deposit__form__inputs { + display: flex; + flex-direction: column; + gap: 24px; +} + +.deposit__row { + display: flex; + gap: 32px; +} + +.summary .dropdown__label { + color: var(--text-secondary); +} + .balances { display: flex; flex-direction: column; diff --git a/src/hooks/use_deposit.rs b/src/hooks/use_deposit.rs new file mode 100644 index 0000000..23b0490 --- /dev/null +++ b/src/hooks/use_deposit.rs @@ -0,0 +1,109 @@ +use std::str::FromStr; + +use dioxus::prelude::*; + +#[derive(Clone, Debug)] +pub enum DepositTo { + Address(String), + Community(u16), +} + +#[derive(Clone, Default, Debug)] +pub struct DepositForm { + pub dest: DepositTo, + pub amount: String, +} + +pub enum DepositError { + MalformedAddress, + InvalidAmount, +} + +impl DepositForm { + pub fn is_valid(&self) -> bool { + let has_info = match &self.dest { + // Check if is a valid address + DepositTo::Address(addrs) => addrs.len() > 0, + DepositTo::Community(_) => true, + }; + + has_info && self.amount.len() > 0 + } + + pub fn address(&self) -> String { + match &self.dest { + DepositTo::Address(addrs) => addrs.to_string(), + _ => String::new(), + } + } + + pub fn to_deposit(&self) -> Result<(String, u64, bool), DepositError> { + let amount = self.amount.parse::().map_err(|_| { + log::warn!("Malformed amount"); + DepositError::InvalidAmount + })?; + let amount = (amount * 1_000_000_000_000.0) as u64; + match &self.dest { + DepositTo::Address(addrs) => { + let address = sp_core::sr25519::Public::from_str(&addrs) + .map_err(|_| DepositError::MalformedAddress)?; + let hex_address = format!("0x{}", hex::encode(address.0)); + Ok((hex_address, amount, false)) + } + DepositTo::Community(id) => Ok((id.to_string(), amount, true)), + } + } +} + +impl Default for DepositTo { + fn default() -> Self { + Self::Address(String::new()) + } +} + +pub fn use_deposit() -> UseDepositState { + let deposit = consume_context::>(); + + use_hook(|| UseDepositState { + inner: UseDepositInner { deposit }, + }) +} + +#[derive(Clone, Copy)] +pub struct UseDepositState { + inner: UseDepositInner, +} + +#[derive(Clone, Copy, Default)] +pub struct UseDepositInner { + deposit: Signal, +} + +impl UseDepositState { + pub fn get(&self) -> UseDepositInner { + self.inner.clone() + } + + pub fn get_deposit(&self) -> DepositForm { + self.inner.deposit.read().clone() + } + + pub fn set_deposit(&mut self, deposit: DepositForm) { + let mut inner = self.inner.deposit.write(); + *inner = deposit; + } + + pub fn deposit_mut(&mut self) -> Signal { + self.inner.deposit.clone() + } + + pub fn is_form_complete(&self) -> bool { + let deposit = self.inner.deposit.read(); + + deposit.is_valid() + } + + pub fn default(&mut self) { + self.inner = UseDepositInner::default() + } +} diff --git a/src/hooks/use_startup.rs b/src/hooks/use_startup.rs index ae9f036..70b77ae 100644 --- a/src/hooks/use_startup.rs +++ b/src/hooks/use_startup.rs @@ -9,6 +9,7 @@ use super::{ use_accounts::{Account, IsDaoOwner}, use_attach::AttachFile, use_communities::Communities, + use_deposit::DepositForm, use_initiative::{ActionsForm, ConfirmationForm, InfoForm, SettingsForm}, use_notification::NotificationItem, use_onboard::{BasicsForm, InvitationForm, ManagementForm}, @@ -57,6 +58,7 @@ pub fn use_startup() { use_context_provider::>(|| Signal::new(IsTimestampHandled(false))); use_context_provider::>(|| Signal::new(WithdrawForm::default())); + use_context_provider::>(|| Signal::new(DepositForm::default())); // Clients diff --git a/src/lib.rs b/src/lib.rs index ef4bf02..9819081 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod pages { pub mod account; pub mod dashboard; + pub mod deposit; pub mod explore; pub mod initiative; pub mod initiatives; @@ -25,6 +26,7 @@ pub mod hooks { pub mod use_attach; pub mod use_communities; pub mod use_connect_wallet; + pub mod use_deposit; pub mod use_initiative; pub mod use_language; pub mod use_market_client; diff --git a/src/locales/en-US.json b/src/locales/en-US.json index ecc46d5..df986f1 100644 --- a/src/locales/en-US.json +++ b/src/locales/en-US.json @@ -103,9 +103,9 @@ }, "withdraw": { "payment": { - "label": "Checkout", - "title": "Payment", - "subtitle": "Payment Method", + "label": "Define the withdraw method", + "title": "Pick a Method", + "subtitle": "Methods", "methods": { "card": { "title": "Credit Card", @@ -131,6 +131,70 @@ } } }, + "deposit": { + "tabs": { + "accounts": "My accounts", + "others": "Other accounts", + "communities": "Communities" + }, + "form": { + "title": "Deposit Address", + "account": { + "label": "Account" + }, + "address": { + "label": "Address" + }, + "community": { + "label": "Community" + }, + "amount": { + "label": "Amount", + "placeholder": "Amount" + }, + "cta": { + "continue": "Confirm deposit" + } + }, + "payment": { + "label": "Define the deposit method", + "title": "Pick a Method", + "subtitle": "Methods", + "methods": { + "card": { + "title": "Credit Card", + "fee": "+{fee}%", + "cta": "Add New Card" + }, + "paypal": { + "title": "Paypal", + "fee": "+{fee}%" + }, + "pse": { + "title": "PSE", + "fee": "+{fee}%" + }, + "kusama": { + "title": "Kusama", + "fee": "Free" + }, + "eth": { + "title": "ETH/Polygon", + "fee": "Free" + } + } + }, + "tips": { + "loading": { + "title": "The deposit is in process", + "description": "This may take a moment" + }, + "created": { + "title": "Excellent! 🚀", + "description": "Your deposit has been successfully" + } + } + }, "dao": { "tabs": { "all": "All initiatives", @@ -468,7 +532,10 @@ "not_empty": "Oops! ✋ This field cannot be empty", "upload_fail": "Oops! ✋ The file could not be uploaded. Please try again.", "community_creation": "Oops! ✋ We couldn't create your community. Please check and try again.", - "initiative_creation": "Oops! ✋ We couldn't create the initiative. Please try again." + "initiative_creation": "Oops! ✋ We couldn't create the initiative. Please try again.", + "invalid_amount": "Oops! ✋ This amount field is invalid", + "invalid_address": "Oops! ✋ This address is invalid", + "deposit_failed": "Oops! ✋ We couldn't make this deposit. Please try again." }, "market": { "query_failed": "The price could not be consulted" diff --git a/src/locales/es-ES.json b/src/locales/es-ES.json index 61eba6c..a6796da 100644 --- a/src/locales/es-ES.json +++ b/src/locales/es-ES.json @@ -131,6 +131,70 @@ } } }, + "deposit": { + "tabs": { + "accounts": "Mis cuentas", + "others": "Otras cuentas", + "communities": "Comunidades" + }, + "form": { + "title": "Dirección de Depósito", + "account": { + "label": "Cuenta" + }, + "address": { + "label": "Dirección" + }, + "community": { + "label": "Comunidad" + }, + "amount": { + "label": "Cantidad", + "placeholder": "Cantidad" + }, + "cta": { + "continue": "Confirmar depósito" + } + }, + "payment": { + "label": "Depósito", + "title": "Completa la información de depósito", + "subtitle": "Método", + "methods": { + "card": { + "title": "Tarjeta de Crédito", + "fee": "+{fee}%", + "cta": "Añadir Nueva Tarjeta" + }, + "paypal": { + "title": "Paypal", + "fee": "+{fee}%" + }, + "pse": { + "title": "PSE", + "fee": "+{fee}%" + }, + "kusama": { + "title": "Kusama", + "fee": "Gratis" + }, + "eth": { + "title": "ETH/Polygon", + "fee": "Gratis" + } + } + }, + "tips": { + "loading": { + "title": "El depósito está en proceso", + "description": "Esto puede tardar un momento" + }, + "created": { + "title": "¡Excelente! 🚀", + "description": "Tu depósito ha sido exitoso" + } + } + }, "dao": { "tabs": { "all": "Iniciativas", @@ -464,7 +528,10 @@ "not_empty": "¡Ups! ✋ Este campo no puede estar vacio.", "upload_fail": "¡Ups! ✋ No se pudo subir el archivo. Por favor, inténtalo de nuevo.", "community_creation": "¡Ups! ✋ No logramos crear tu comunidad. Por favor, verifica e inténtalo de nuevo.", - "initiative_creation": "¡Ups! ✋ No logramos crear tu iniciativa. Por favor, inténtalo de nuevo." + "initiative_creation": "¡Ups! ✋ No logramos crear tu iniciativa. Por favor, inténtalo de nuevo.", + "invalid_amount": "¡Ups! ✋ Este campo de cantidad es inválido", + "invalid_address": "¡Ups! ✋ Esta dirección es inválida", + "deposit_failed": "¡Ups! ✋ No pudimos realizar este depósito. Por favor, inténtalo de nuevo." }, "market": { "query_failed": "No se pudo consultar el precio" diff --git a/src/pages/account.rs b/src/pages/account.rs index 95d2c63..8a979a5 100644 --- a/src/pages/account.rs +++ b/src/pages/account.rs @@ -141,24 +141,24 @@ pub fn Account() -> Element { } div { class: "account__balance__cta", Button { - class: "button--comming-soon", text: translate!(i18, "account.tabs.wallet.balance.options.deposit"), size: ElementSize::Small, variant: ButtonVariant::Secondary, on_click: move |_| { spawn( async move { - + nav.push(vec![ + Box::new(is_chain_available(i18, timestamp, notification)) + ], "/deposit"); Ok::<(), String>(()) }.unwrap_or_else(move |_: String| { - + }) ); }, status: None, } Button { - class: "", text: translate!(i18, "account.tabs.wallet.balance.options.withdraw"), size: ElementSize::Small, variant: ButtonVariant::Secondary, @@ -168,7 +168,7 @@ pub fn Account() -> Element { nav.push(vec![Box::new(is_chain_available(i18, timestamp, notification))], "/withdraw"); Ok::<(), String>(()) }.unwrap_or_else(move |_: String| { - + }) ); }, @@ -203,7 +203,7 @@ pub fn Account() -> Element { } } } - + } div { class: "account__container", h3 { class: "account__balance__title", @@ -218,7 +218,7 @@ pub fn Account() -> Element { th { {translate!(i18, "account.tabs.wallet.assets.cost")} } th { {translate!(i18, "account.tabs.wallet.assets.total")} } } - + match *tab_value.read() { AccountTabs::Kreivo => rsx!( tr { @@ -231,14 +231,14 @@ pub fn Account() -> Element { { format!("${} USD", if ksm_usd() == 0.0 || kreivo_balance() <= 0.001 { "-".to_string() } else { format!("{:.2}", ksm_usd() * kreivo_balance()) } )} } } - + tr { class: "list__asset--comming-soon", td { class: "list__name", "USDT" } td { "-" } td { "-" } td { "-" } } - + tr { class: "list__asset--comming-soon", td { class: "list__name", "dUSD" } td { "-" } @@ -268,28 +268,28 @@ pub fn Account() -> Element { th { {translate!(i18, "account.tabs.transfers.table.quantity")} } th { {translate!(i18, "account.tabs.transfers.table.account")} } } - + tr { td { class: "list__name", "KSM" } td { "2024-08-20 20:16:34" } td { "10" } td { "5E4S9C..." } } - + tr { td { class: "list__name", "KSM" } td { "2024-08-20 20:16:34" } td { "10" } td { "5E4S9C..." } } - + tr { td { class: "list__name", "KSM" } td { "2024-08-20 20:16:34" } td { "10" } td { "5E4S9C..." } } - + tr { td { class: "list__name", "KSM" } td { "2024-08-20 20:16:34" } diff --git a/src/pages/deposit.rs b/src/pages/deposit.rs new file mode 100644 index 0000000..efbab8f --- /dev/null +++ b/src/pages/deposit.rs @@ -0,0 +1,362 @@ +use dioxus::prelude::*; +use dioxus_std::{i18n::use_i18, translate}; +use futures_util::{StreamExt, TryFutureExt}; + +use crate::{ + components::atoms::{ + dropdown::{DropdownItem, ElementSize}, + AccountButton, BankCardLine, Button, CheckboxCard, Dropdown, Icon, Input, KusamaLogo, + PaymentMethod, PaypalLogo, PolygonLogo, Tab, Title, + }, + hooks::{ + use_accounts::use_accounts, + use_communities::use_communities, + use_deposit::{use_deposit, DepositError, DepositTo}, + use_notification::use_notification, + use_our_navigator::use_our_navigator, + use_tooltip::{use_tooltip, TooltipItem}, + }, + pages::onboarding::convert_to_jsvalue, +}; +use wasm_bindgen::prelude::*; + +pub enum PaymentMethods { + Card, + Paypal, + PSE, + KUSAMA, + ETH, + None, +} + +#[derive(PartialEq, Clone)] +pub enum DepositKreivoTabs { + Accounts, + Wallet, + Community, +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(catch, js_namespace = window, js_name = deposit)] + async fn depositAction( + dest: JsValue, + amount: u64, + to_community: bool, + ) -> Result; +} + +#[component] +pub fn Deposit() -> Element { + let i18 = use_i18(); + let mut deposit = use_deposit(); + + let accounts = use_accounts(); + let communities = use_communities(); + let mut notification = use_notification(); + let mut tooltip = use_tooltip(); + let nav = use_our_navigator(); + + let mut payment_selected = use_signal(|| PaymentMethods::None); + let mut tab_value = use_signal(|| DepositKreivoTabs::Accounts); + + let mut dropdown_value = use_signal::>(|| None); + + let mut items = vec![]; + for account in accounts.get().into_iter() { + let address = account.address(); + + items.push(rsx!(AccountButton { + title: account.name(), + description: address.clone(), + on_click: move |_| {} + })) + } + + let mut community_items = vec![]; + for community in communities.get_communities().into_iter() { + community_items.push(rsx!(AccountButton { + title: format!("{} ({})", community.name, community.id), + description: "".to_string(), + on_click: move |_| {} + })) + } + + let on_handle_account = use_coroutine(move |mut rx: UnboundedReceiver| async move { + while let Some(event) = rx.next().await { + let account = &accounts.get()[event as usize]; + + let value = Some(DropdownItem { + key: account.address().clone(), + value: account.name(), + }); + + dropdown_value.set(value); + + deposit + .deposit_mut() + .with_mut(|w| w.dest = DepositTo::Address(account.address())) + } + }); + + let on_handle_community = use_coroutine(move |mut rx: UnboundedReceiver| async move { + while let Some(event) = rx.next().await { + let community = &communities.get_communities()[event as usize]; + + let value = Some(DropdownItem { + key: community.id.to_string(), + value: community.name.clone(), + }); + + dropdown_value.set(value); + + deposit + .deposit_mut() + .with_mut(|w| w.dest = DepositTo::Community(community.id)) + } + }); + + use_effect(use_reactive((&*tab_value.read(),), move |(_,)| { + dropdown_value.set(None); + })); + + use_before_render(move || { + deposit.default(); + }); + + rsx!( + div { class: "page--initiative", + div { class: "payment__form", + div { class: "form__wrapper", + div { class: "form__title", + span { class: "label", {translate!(i18, "deposit.payment.label")} } + Title { text: translate!(i18, "deposit.payment.title") } + } + div { class: "row deposit__row", + div { class: "summary summary--form", + div { class: "row deposit__row", + div { class: "summary__wrapper", + h4 { class: "summary__subtitle", {translate!(i18, "deposit.payment.subtitle")} } + div { class: "payment__methods", + CheckboxCard { + id: "a".to_string(), + name: String::from("management"), + checked: matches!(*payment_selected.read(), PaymentMethods::KUSAMA), + class: "checkbox-card--payment", + body: rsx!( + PaymentMethod { title : translate!(i18, "deposit.payment.methods.kusama.title"), + fee : translate!(i18, "deposit.payment.methods.kusama.fee"), icon : rsx!(Icon { + icon : KusamaLogo, height : 20, width : 20, fill : "var(--fill-600)" }), } + ), + on_change: move |_| { + payment_selected.set(PaymentMethods::KUSAMA); + } + } + CheckboxCard { + id: "a".to_string(), + name: String::from("management"), + checked: matches!(*payment_selected.read(), PaymentMethods::Card), + soon: true, + class: "checkbox-card--payment", + body: rsx!( + PaymentMethod { title : translate!(i18, "deposit.payment.methods.card.title"), + fee : translate!(i18, "deposit.payment.methods.card.fee", fee : 5), icon : + rsx!(Icon { icon : BankCardLine, height : 20, width : 20, fill : + "var(--fill-600)" }), } + ), + on_change: move |_| { + payment_selected.set(PaymentMethods::Card); + } + } + CheckboxCard { + id: "a".to_string(), + name: String::from("management"), + checked: matches!(*payment_selected.read(), PaymentMethods::Paypal), + soon: true, + class: "checkbox-card--payment", + body: rsx!( + PaymentMethod { title : translate!(i18, "deposit.payment.methods.paypal.title"), + fee : translate!(i18, "deposit.payment.methods.paypal.fee", fee : 5), icon : + rsx!(Icon { icon : PaypalLogo, height : 20, width : 20, fill : "var(--fill-600)" + }), } + ), + on_change: move |_| { + payment_selected.set(PaymentMethods::Paypal); + } + } + CheckboxCard { + id: "a".to_string(), + name: String::from("management"), + checked: matches!(*payment_selected.read(), PaymentMethods::PSE), + soon: true, + class: "checkbox-card--payment", + body: rsx!( + PaymentMethod { title : translate!(i18, "deposit.payment.methods.pse.title"), + fee : translate!(i18, "deposit.payment.methods.pse.fee", fee : 3), icon : + rsx!(Icon { icon : PaypalLogo, height : 20, width : 20, fill : "var(--fill-600)" + }), } + ), + on_change: move |_| { + payment_selected.set(PaymentMethods::PSE); + } + } + CheckboxCard { + id: "a".to_string(), + name: String::from("management"), + checked: matches!(*payment_selected.read(), PaymentMethods::ETH), + soon: true, + class: "checkbox-card--payment", + body: rsx!( + PaymentMethod { title : translate!(i18, "deposit.payment.methods.eth.title"), + fee : translate!(i18, "deposit.payment.methods.eth.fee"), icon : rsx!(Icon { + icon : PolygonLogo, height : 20, width : 20, fill : "var(--fill-600)" }), } + ), + on_change: move |_| { + payment_selected.set(PaymentMethods::ETH); + } + } + } + } + if !matches!(*payment_selected.read(), PaymentMethods::None) { + div { class: "summary__wrapper", + h4 { class: "summary__subtitle", {translate!(i18, "deposit.form.title")} } + div { class: "deposit__form__inputs", + div { class: "account__options", + Tab { + text: translate!(i18, "deposit.tabs.accounts"), + is_active: if *tab_value.read() == DepositKreivoTabs::Accounts { true } else { false }, + on_click: move |_| { + tab_value.set(DepositKreivoTabs::Accounts); + } + } + Tab { + text: translate!(i18, "deposit.tabs.others"), + is_active: if *tab_value.read() == DepositKreivoTabs::Wallet { true } else { false }, + on_click: move |_| { + tab_value.set(DepositKreivoTabs::Wallet); + } + } + Tab { + text: translate!(i18, "deposit.tabs.communities"), + is_active: if *tab_value.read() == DepositKreivoTabs::Community { true } else { false }, + on_click: move |_| { + tab_value.set(DepositKreivoTabs::Community); + } + } + } + div { class: "widthdraw__data", + match *tab_value.read() { + DepositKreivoTabs::Accounts => rsx!{ + Dropdown { + class: "payment__wallet dropdown--left".to_string(), + value: dropdown_value(), + label: translate!(i18, "deposit.form.account.label"), + size: ElementSize::Medium, + placeholder: translate!(i18, "header.cta.account"), + default: None, + on_change: move |event: usize| { + on_handle_account.send(event as u8); + }, + body: items + } + }, + DepositKreivoTabs::Wallet => rsx!{ + Input { + message: deposit.get_deposit().address(), + placeholder: "5HBVkGX...", + label: translate!(i18, "deposit.form.address.label"), + error: None, + on_input: move |event: Event| { + dropdown_value.set(None); + deposit + .deposit_mut() + .with_mut(|w| w.dest = DepositTo::Address(event.value())); + }, + on_keypress: move |_| {}, + on_click: move |_| {}, + } + }, + DepositKreivoTabs::Community => rsx!{ + Dropdown { + class: "payment__wallet dropdown--left".to_string(), + value: dropdown_value(), + label: translate!(i18, "deposit.form.community.label"), + size: ElementSize::Medium, + placeholder: translate!(i18, "header.cta.account"), + default: None, + on_change: move |event: usize| { + on_handle_community.send(event as u8); + }, + body: community_items + } + }, + }, + Input { + message: deposit.get_deposit().amount, + placeholder: translate!(i18, "deposit.form.amount.placeholder"), + label: translate!(i18, "deposit.form.amount.label"), + error: None, + right_text: rsx!(span { class : "input--right__text", "KSM" }), + on_input: move |event: Event| { + deposit + .deposit_mut() + .with_mut(|w| { + w.amount = event.value(); + }) + }, + on_keypress: move |_| {}, + on_click: move |_| {} + } + } + } + Button { + text: translate!(i18, "deposit.form.cta.continue"), + disabled: !deposit.is_form_complete(), + size: ElementSize::Medium, + on_click: move |_| { + spawn( + async move { + tooltip + .handle_tooltip(TooltipItem { + title: translate!(i18, "deposit.tips.loading.title"), + body: translate!(i18, "deposit.tips.loading.description"), + show: true, + }); + let (destination, amount, to_community) = deposit.get_deposit().to_deposit().map_err(|e| match e { + DepositError::MalformedAddress => translate!(i18, "errors.wallet.account_address"), + DepositError::InvalidAmount => translate!(i18, "errors.form.invalid_amount"), + })?; + let destination = convert_to_jsvalue(&destination) + .map_err(|_| { + log::warn!("Malformed dest account"); + translate!(i18, "errors.form.invalid_address") + })?; + depositAction(destination, amount, to_community) + .await + .map_err(|e| { + log::warn!("Deposit failed {:?}", e); + translate!(i18, "errors.form.deposit_failed") + })?; + tooltip.hide(); + notification.handle_success(&translate!(i18, "deposit.tips.created.description")); + nav.push(vec![], "/account"); + Ok::<(), String>(()) + } + .unwrap_or_else(move |e: String| { + tooltip.hide(); + notification.handle_error(&e); + }), + ); + }, + status: None + } + } + } + } + } + } + } + } + } + ) +} diff --git a/src/pages/route.rs b/src/pages/route.rs index 5e46bce..96afc74 100644 --- a/src/pages/route.rs +++ b/src/pages/route.rs @@ -4,7 +4,7 @@ use crate::{ pages::{ account::Account, dashboard::Dashboard, explore::Explore, initiative::Initiative, initiatives::Initiatives, login::Login, not_found::PageNotFound, onboarding::Onboarding, - vote::Vote, withdraw::Withdraw + vote::Vote, withdraw::Withdraw, deposit::Deposit }, }; #[derive(Clone, Routable, Debug, PartialEq)] @@ -21,6 +21,8 @@ pub enum Route { Account {}, #[route("/withdraw")] Withdraw {}, + #[route("/deposit")] + Deposit {}, #[layout(Onboard)] #[route("/explore")] Explore {},