add products view for customers

This commit is contained in:
creating2morrow 2023-06-08 17:22:13 -04:00
parent eb806fa36e
commit 9fc930fb5b
5 changed files with 375 additions and 81 deletions

View file

@ -10,6 +10,7 @@ use log::{
info,
};
use rocket::serde::json::Json;
use std::error::Error;
/// Create a new product
pub fn create(d: Json<Product>) -> Product {
@ -102,3 +103,32 @@ fn validate_product(p: &Json<Product>) -> bool {
&& p.name.len() < utils::string_limit()
&& p.image.len() < utils::image_limit()
}
/// Send the request to vendor for the products available
pub async fn get_vendor_products(
contact: String,
jwp: String,
) -> Result<Vec<Product>, Box<dyn Error>> {
let host = utils::get_i2p_http_proxy();
let proxy = reqwest::Proxy::http(&host)?;
let client = reqwest::Client::builder().proxy(proxy).build();
match client?
.get(format!("http://{}/market/products", contact))
.header("proof", jwp)
.send()
.await
{
Ok(response) => {
let res = response.json::<Vec<Product>>().await;
debug!("get vendor products response: {:?}", res);
match res {
Ok(r) => Ok(r),
_ => Ok(Default::default()),
}
}
Err(e) => {
error!("failed to fetch products due to: {:?}", e);
Ok(Default::default())
}
}
}

View file

@ -21,6 +21,38 @@ use rand_core::RngCore;
use rocket::serde::json::Json;
use std::time::Duration;
/// Struct for the vendor / contact status window
pub struct ContactStatus {
/// UNIX timestamp of expiration as string
pub exp: String,
/// human readable date of expiration as string
pub h_exp: String,
/// i2p address of current status check
pub i2p: String,
/// JSON Web Proof of current status check
pub jwp: String,
/// Alias for contact
pub nick: String,
/// Must sign key for contacts befor messages can be sent
pub signed_key: bool,
/// transaction proof signature of current status check
pub txp: String,
}
impl Default for ContactStatus {
fn default() -> Self {
ContactStatus {
exp: utils::empty_string(),
h_exp: utils::empty_string(),
i2p: utils::empty_string(),
jwp: utils::empty_string(),
nick: String::from("anon"),
signed_key: false,
txp: utils::empty_string(),
}
}
}
/// Enum for selecting hash validation
#[derive(PartialEq)]
enum ExternalSoftware {
@ -698,6 +730,24 @@ pub fn toggle_vendor_enabled() -> bool {
}
}
pub fn search_gui_db(f: String, data: String) -> String {
let s = db::Interface::open();
let k = format!("{}-{}", f, data);
db::Interface::read(&s.env, &s.handle, &k)
}
pub fn write_gui_db(f: String, key: String, data: String) {
let s = db::Interface::open();
let k = format!("{}-{}", f, key);
db::Interface::write(&s.env, &s.handle, &k, &data);
}
pub fn clear_gui_db(f: String, key: String) {
let s = db::Interface::open();
let k = format!("{}-{}", f, key);
db::Interface::delete(&s.env, &s.handle, &k);
}
// Tests
//-------------------------------------------------------------------------------

View file

@ -27,37 +27,6 @@ impl Default for Compose {
}
}
/// Struct for the contact status window
struct Status {
/// UNIX timestamp of expiration as string
exp: String,
/// human readable date of expiration as string
h_exp: String,
/// i2p address of current status check
i2p: String,
/// JSON Web Proof of current status check
jwp: String,
/// Alias for contact
nick: String,
signed_key: bool,
/// transaction proof signature of current status check
txp: String,
}
impl Default for Status {
fn default() -> Self {
Status {
exp: utils::empty_string(),
h_exp: utils::empty_string(),
i2p: utils::empty_string(),
jwp: utils::empty_string(),
nick: String::from("anon"),
signed_key: false,
txp: utils::empty_string(),
}
}
}
/// The AddressBookApp unfornuately does more than that.
///
/// Herein lies the logic for filtering contacts, generating JWPs,
@ -101,7 +70,7 @@ pub struct AddressBookApp {
payment_tx: Sender<bool>,
payment_rx: Receiver<bool>,
showing_status: bool,
status: Status,
status: utils::ContactStatus,
send_message_tx: Sender<bool>,
send_message_rx: Receiver<bool>,
s_contact: models::Contact,
@ -359,7 +328,7 @@ impl eframe::App for AddressBookApp {
if !self.status.signed_key {
if ui.button("Sign Key").clicked() {
contact::trust_gpg(self.status.i2p.clone());
write_gui_db(
utils::write_gui_db(
String::from("gui-signed-key"),
self.status.i2p.clone(),
String::from("1"),
@ -371,9 +340,9 @@ impl eframe::App for AddressBookApp {
&& self.status.jwp == utils::empty_string();
if self.status.jwp != utils::empty_string() || failed_to_prove {
if ui.button("Clear stale JWP").clicked() {
clear_gui_db(String::from("gui-txp"), self.status.i2p.clone());
clear_gui_db(String::from("gui-jwp"), self.status.i2p.clone());
clear_gui_db(String::from("gui-exp"), self.status.i2p.clone());
utils::clear_gui_db(String::from("gui-txp"), self.status.i2p.clone());
utils::clear_gui_db(String::from("gui-jwp"), self.status.i2p.clone());
utils::clear_gui_db(String::from("gui-exp"), self.status.i2p.clone());
self.showing_status = false;
}
}
@ -538,7 +507,7 @@ impl eframe::App for AddressBookApp {
});
row.col(|ui| {
if ui.button("Check Status").clicked() {
let nick_db = search_gui_db(
let nick_db = utils::search_gui_db(
String::from("gui-nick"),
String::from(&c.i2p_address),
);
@ -550,16 +519,16 @@ impl eframe::App for AddressBookApp {
self.status.nick = nick;
self.status.i2p = String::from(&c.i2p_address);
// get the txp
self.status.txp = search_gui_db(
self.status.txp = utils::search_gui_db(
String::from("gui-txp"),
String::from(&c.i2p_address),
);
// get the jwp
self.status.jwp = search_gui_db(
self.status.jwp = utils::search_gui_db(
String::from("gui-jwp"),
String::from(&c.i2p_address),
);
let r_exp = search_gui_db(
let r_exp = utils::search_gui_db(
String::from("gui-exp"),
String::from(&c.i2p_address),
);
@ -642,24 +611,6 @@ fn add_contact_timeout(tx: Sender<bool>, ctx: egui::Context) {
});
}
fn search_gui_db(f: String, data: String) -> String {
let s = db::Interface::open();
let k = format!("{}-{}", f, data);
db::Interface::read(&s.env, &s.handle, &k)
}
fn write_gui_db(f: String, key: String, data: String) {
let s = db::Interface::open();
let k = format!("{}-{}", f, key);
db::Interface::write(&s.env, &s.handle, &k, &data);
}
fn clear_gui_db(f: String, key: String) {
let s = db::Interface::open();
let k = format!("{}-{}", f, key);
db::Interface::delete(&s.env, &s.handle, &k);
}
fn send_invoice_req(tx: Sender<reqres::Invoice>, ctx: egui::Context, contact: String) {
log::debug!("async send_invoice_req");
tokio::spawn(async move {
@ -682,9 +633,9 @@ fn send_payment_req(
) {
log::debug!("async send_payment_req");
log::debug!("cleaning stale jwp values");
clear_gui_db(String::from("gui-txp"), String::from(&contact));
clear_gui_db(String::from("gui-jwp"), String::from(&contact));
clear_gui_db(String::from("gui-exp"), String::from(&contact));
utils::clear_gui_db(String::from("gui-txp"), String::from(&contact));
utils::clear_gui_db(String::from("gui-jwp"), String::from(&contact));
utils::clear_gui_db(String::from("gui-exp"), String::from(&contact));
let mut retry_count = 1;
tokio::spawn(async move {
let ptxp_address = String::from(&d.address);
@ -733,7 +684,7 @@ fn send_payment_req(
.await;
retry_count += 1;
}
write_gui_db(
utils::write_gui_db(
String::from("gui-txp"),
String::from(&contact),
String::from(&ftxp.signature),
@ -747,7 +698,7 @@ fn send_payment_req(
// if we made it this far we can now request a JWP from our friend
match proof::prove_payment(String::from(&contact), &ftxp).await {
Ok(result) => {
write_gui_db(
utils::write_gui_db(
String::from("gui-jwp"),
String::from(&contact),
String::from(&result.jwp),
@ -757,7 +708,7 @@ fn send_payment_req(
// subtract 120 seconds since we had to wait for one confirmation
let grace: i64 = seconds - BLOCK_TIME_IN_SECS_EST as i64;
let unix: i64 = chrono::offset::Utc::now().timestamp() + grace;
write_gui_db(
utils::write_gui_db(
String::from("gui-exp"),
String::from(&contact),
format!("{}", unix),
@ -796,14 +747,14 @@ fn send_message_req(tx: Sender<bool>, ctx: egui::Context, body: String, to: Stri
}
fn check_signed_key(contact: String) -> bool {
let v = search_gui_db(String::from("gui-signed-key"), contact);
let v = utils::search_gui_db(String::from("gui-signed-key"), contact);
v != utils::empty_string()
}
fn change_nick_req(contact: String, nick: String) {
log::debug!("change nick");
clear_gui_db(String::from("gui-nick"), String::from(&contact));
write_gui_db(String::from("gui-nick"), String::from(&contact), nick);
utils::clear_gui_db(String::from("gui-nick"), String::from(&contact));
utils::write_gui_db(String::from("gui-nick"), String::from(&contact), nick);
}
fn send_can_transfer_req(tx: Sender<bool>, ctx: egui::Context, invoice: u128) {

View file

@ -5,12 +5,22 @@ use std::sync::mpsc::{
};
pub struct MarketApp {
contact_info_tx: Sender<models::Contact>,
contact_info_rx: Receiver<models::Contact>,
find_vendor: String,
get_vendor_products_tx: Sender<Vec<models::Product>>,
get_vendor_products_rx: Receiver<Vec<models::Product>>,
is_ordering: bool,
is_pinging: bool,
is_product_image_set: bool,
is_showing_products: bool,
is_showing_product_image: bool,
is_showing_product_update: bool,
is_showing_orders: bool,
is_showing_vendor_status: bool,
is_showing_vendors: bool,
is_vendor_enabled: bool,
is_window_shopping: bool,
orders: Vec<models::Order>,
product_image: egui_extras::RetainedImage,
products: Vec<models::Product>,
@ -22,6 +32,10 @@ pub struct MarketApp {
new_product_qty: String,
_refresh_on_delete_product_tx: Sender<bool>,
_refresh_on_delete_product_rx: Receiver<bool>,
s_contact: models::Contact,
showing_vendor_status: bool,
vendor_status: utils::ContactStatus,
vendors: Vec<models::Contact>,
}
impl Default for MarketApp {
@ -32,13 +46,25 @@ impl Default for MarketApp {
let s = db::Interface::open();
let r = db::Interface::read(&s.env, &s.handle, contact::NEVEKO_VENDOR_ENABLED);
let is_vendor_enabled = r == contact::NEVEKO_VENDOR_MODE_ON;
let (contact_info_tx, contact_info_rx) = std::sync::mpsc::channel();
let (get_vendor_products_tx, get_vendor_products_rx) = std::sync::mpsc::channel();
MarketApp {
contact_info_rx,
contact_info_tx,
find_vendor: utils::empty_string(),
get_vendor_products_rx,
get_vendor_products_tx,
is_ordering: false,
is_pinging: false,
is_product_image_set: false,
is_showing_orders: false,
is_showing_products: false,
is_showing_product_image: false,
is_showing_product_update: false,
is_showing_vendor_status: false,
is_showing_vendors: false,
is_vendor_enabled,
is_window_shopping: false,
orders: Vec::new(),
product_image: egui_extras::RetainedImage::from_image_bytes(
"qr.png",
@ -54,6 +80,10 @@ impl Default for MarketApp {
new_product_qty: utils::empty_string(),
_refresh_on_delete_product_tx,
_refresh_on_delete_product_rx,
s_contact: Default::default(),
showing_vendor_status: false,
vendor_status: Default::default(),
vendors: Vec::new(),
}
}
}
@ -62,6 +92,188 @@ impl eframe::App for MarketApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Hook into async channel threads
//-----------------------------------------------------------------------------------
if let Ok(contact_info) = self.contact_info_rx.try_recv() {
self.s_contact = contact_info;
if self.s_contact.xmr_address != utils::empty_string() {
self.is_pinging = false;
}
}
if let Ok(vendor_products) = self.get_vendor_products_rx.try_recv() {
self.products = vendor_products;
}
// TODO(c2m): create order form
// View vendors
//-----------------------------------------------------------------------------------
let mut is_showing_vendors = self.is_showing_vendors;
egui::Window::new("Vendors")
.open(&mut is_showing_vendors)
.vscroll(true)
.show(&ctx, |ui| {
// Vendor filter
//-----------------------------------------------------------------------------------
ui.heading("\nFind Vendor");
ui.label("\n");
ui.horizontal(|ui| {
let find_vendor_label = ui.label("filter vendors: ");
ui.text_edit_singleline(&mut self.find_vendor)
.labelled_by(find_vendor_label.id);
});
ui.label("\n");
use egui_extras::{
Column,
TableBuilder,
};
let table = TableBuilder::new(ui)
.striped(true)
.resizable(true)
.cell_layout(egui::Layout::left_to_right(egui::Align::Center))
.column(Column::auto())
.column(Column::auto())
.column(Column::auto())
.column(Column::auto())
.column(Column::remainder())
.min_scrolled_height(0.0);
table
.header(20.0, |mut header| {
header.col(|ui| {
ui.strong("Nickname");
});
header.col(|ui| {
ui.strong(".b32.i2p");
});
header.col(|ui| {
ui.strong("");
});
header.col(|ui| {
ui.strong("");
});
})
.body(|mut body| {
for v in &self.vendors {
if v.i2p_address.contains(&self.find_vendor) && v.is_vendor {
let row_height = 20.0;
body.row(row_height, |mut row| {
row.col(|ui| {
ui.label("vendor");
});
row.col(|ui| {
ui.label(format!("{}", v.i2p_address));
});
row.col(|ui| {
if ui.button("Check Status").clicked() {
let nick_db = utils::search_gui_db(
String::from("gui-nick"),
String::from(&v.i2p_address),
);
let nick = if nick_db == utils::empty_string() {
String::from("anon")
} else {
nick_db
};
self.vendor_status.nick = nick;
self.vendor_status.i2p = String::from(&v.i2p_address);
// get the txp
self.vendor_status.txp = utils::search_gui_db(
String::from("gui-txp"),
String::from(&v.i2p_address),
);
// get the jwp
self.vendor_status.jwp = utils::search_gui_db(
String::from("gui-jwp"),
String::from(&v.i2p_address),
);
let r_exp = utils::search_gui_db(
String::from("gui-exp"),
String::from(&v.i2p_address),
);
self.vendor_status.exp = r_exp;
let expire = match self.vendor_status.exp.parse::<i64>()
{
Ok(n) => n,
Err(_e) => 0,
};
self.vendor_status.h_exp =
chrono::NaiveDateTime::from_timestamp_opt(
expire, 0,
)
.unwrap()
.to_string();
// MESSAGES WON'T BE SENT UNTIL KEY IS SIGNED AND
// TRUSTED!
self.vendor_status.signed_key =
check_signed_key(self.vendor_status.i2p.clone());
send_contact_info_req(
self.contact_info_tx.clone(),
ctx.clone(),
self.vendor_status.i2p.clone(),
);
self.showing_vendor_status = true;
self.is_pinging = true;
}
});
row.col(|ui| {
let now = chrono::offset::Utc::now().timestamp();
let expire = match self.vendor_status.exp.parse::<i64>() {
Ok(n) => n,
Err(_e) => 0,
};
if now < expire
&& self.vendor_status.signed_key
&& self.vendor_status.jwp != utils::empty_string()
&& v.i2p_address == self.vendor_status.i2p
{
if ui.button("View Products").clicked() {
send_products_from_vendor_req(
self.get_vendor_products_tx.clone(),
ctx.clone(),
self.vendor_status.i2p.clone(),
self.vendor_status.jwp.clone(),
);
self.is_window_shopping = true;
self.is_showing_products = true;
}
}
});
});
}
}
});
if ui.button("Exit").clicked() {
self.is_showing_vendors = false;
}
});
// Vendor status window
//-----------------------------------------------------------------------------------
let mut is_showing_vendor_status = self.is_showing_vendor_status;
egui::Window::new(&self.vendor_status.i2p)
.open(&mut is_showing_vendor_status)
.vscroll(true)
.title_bar(false)
.id(egui::Id::new(self.vendor_status.i2p.clone()))
.show(&ctx, |ui| {
if self.is_pinging {
ui.add(egui::Spinner::new());
ui.label("pinging...");
}
let status = if self.s_contact.xmr_address != utils::empty_string() {
"online"
} else {
"offline"
};
ui.label(format!("status: {}", status));
ui.label(format!("nick: {}", self.vendor_status.nick));
ui.label(format!("tx proof: {}", self.vendor_status.txp));
ui.label(format!("jwp: {}", self.vendor_status.jwp));
ui.label(format!("expiration: {}", self.vendor_status.h_exp));
ui.label(format!("signed key: {}", self.vendor_status.signed_key));
if ui.button("Exit").clicked() {
self.is_showing_vendor_status = false;
}
});
// Product image window
//-----------------------------------------------------------------------------------
@ -81,6 +293,8 @@ impl eframe::App for MarketApp {
}
});
// Vendor specific
// Update Product window
//-----------------------------------------------------------------------------------
let mut is_showing_product_update = self.is_showing_product_update;
@ -159,10 +373,9 @@ impl eframe::App for MarketApp {
}
});
// Products window
// Vendor Products Management window
//-----------------------------------------------------------------------------------
let mut is_showing_products = self.is_showing_products;
let mut is_showing_orders = self.is_showing_orders;
egui::Window::new("Products")
.open(&mut is_showing_products)
.vscroll(true)
@ -258,13 +471,20 @@ impl eframe::App for MarketApp {
row.col(|ui| {
ui.style_mut().wrap = Some(false);
ui.horizontal(|ui| {
if ui.button("Update").clicked() {
self.product_update_pid = p.pid.clone();
self.new_product_desc = p.description.clone();
self.new_product_name = p.name.clone();
self.new_product_price = format!("{}", p.price);
self.new_product_qty = format!("{}", p.qty);
self.is_showing_product_update = true;
if !self.is_window_shopping {
if ui.button("Update").clicked() {
self.product_update_pid = p.pid.clone();
self.new_product_desc = p.description.clone();
self.new_product_name = p.name.clone();
self.new_product_price = format!("{}", p.price);
self.new_product_qty = format!("{}", p.qty);
self.is_showing_product_update = true;
}
} else {
if ui.button("Create Order").clicked() {
self.product_update_pid = p.pid.clone();
self.is_ordering = true;
}
}
});
});
@ -278,6 +498,7 @@ impl eframe::App for MarketApp {
// TODO(c2m): Orders window
//-----------------------------------------------------------------------------------
let mut is_showing_orders = self.is_showing_orders;
egui::Window::new("Orders")
.open(&mut is_showing_orders)
.vscroll(true)
@ -343,6 +564,10 @@ impl eframe::App for MarketApp {
});
});
// End Vendor specific
// Market Dashboard Main window
//-----------------------------------------------------------------------------------
egui::CentralPanel::default().show(ctx, |ui| {
if ui.button("Refresh").clicked() {
self.products = product::find_all();
@ -360,7 +585,7 @@ impl eframe::App for MarketApp {
}
});
if ui.button("View Vendors").clicked() {
// TODO(c2m):
self.is_showing_vendors = true;
}
ui.label("\n");
if ui.button("View Orders").clicked() {
@ -437,11 +662,49 @@ impl eframe::App for MarketApp {
}
}
fn _refresh_on_delete_product_req(tx: Sender<bool>, ctx: egui::Context) {
fn _refresh_on_delete_product_req(_tx: Sender<bool>, _ctx: egui::Context) {
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
log::error!("refreshing products....");
let _ = tx.send(true);
ctx.request_repaint();
todo!();
// let _ = tx.send(true);
// ctx.request_repaint();
});
}
//------------------------------------------------------------------------------
fn send_contact_info_req(tx: Sender<models::Contact>, ctx: egui::Context, contact: String) {
log::debug!("async send_contact_info_req");
tokio::spawn(async move {
match contact::add_contact_request(contact).await {
Ok(contact) => {
let _ = tx.send(contact);
ctx.request_repaint();
}
_ => log::debug!("failed to request invoice"),
}
});
}
fn check_signed_key(contact: String) -> bool {
let v = utils::search_gui_db(String::from("gui-signed-key"), contact);
v != utils::empty_string()
}
fn send_products_from_vendor_req(
tx: Sender<Vec<models::Product>>,
ctx: egui::Context,
contact: String,
jwp: String,
) {
log::debug!("fetching products for vendor: {}", contact);
tokio::spawn(async move {
let result = product::get_vendor_products(contact, jwp).await;
if result.is_ok() {
let products: Vec<models::Product> = result.unwrap();
log::info!("retreived {:?} products", products);
let _ = tx.send(products);
ctx.request_repaint();
}
});
}

View file

@ -90,7 +90,7 @@ impl Default for State {
login: Default::default(),
mailbox: Default::default(),
market: Default::default(),
selected_anchor: Default::default(),
selected_anchor: Anchor::Home,
settings: Default::default(),
wallet: Default::default(),
// async notifications