testing customer to vendor product ux

This commit is contained in:
creating2morrow 2023-06-11 03:56:53 -04:00
parent a2f8041f59
commit bbc817d263
8 changed files with 147 additions and 63 deletions

View file

@ -175,14 +175,16 @@ pub async fn request_invoice(contact: String) -> Result<reqres::Invoice, Box<dyn
} }
} }
/// Send the request to contact to add them /// Send the request to contact to add them. Set the prune arg to 1
pub async fn add_contact_request(contact: String) -> Result<Contact, Box<dyn Error>> { ///
/// for gpg key removal.
pub async fn add_contact_request(contact: String, prune: u32) -> Result<Contact, Box<dyn Error>> {
// TODO(c2m): Error handling for http 402 status // TODO(c2m): Error handling for http 402 status
let host = utils::get_i2p_http_proxy(); let host = utils::get_i2p_http_proxy();
let proxy = reqwest::Proxy::http(&host)?; let proxy = reqwest::Proxy::http(&host)?;
let client = reqwest::Client::builder().proxy(proxy).build(); let client = reqwest::Client::builder().proxy(proxy).build();
match client? match client?
.get(format!("http://{}/share", contact)) .get(format!("http://{}/share/{}", contact, prune))
.send() .send()
.await .await
{ {

View file

@ -82,6 +82,7 @@ pub fn find_all() -> Vec<Product> {
/// Modify product /// Modify product
pub fn modify(p: Json<Product>) -> Product { pub fn modify(p: Json<Product>) -> Product {
// TODO(c2m): don't allow modification to products with un-delivered orders
info!("modify product: {}", &p.pid); info!("modify product: {}", &p.pid);
let f_prod: Product = find(&p.pid); let f_prod: Product = find(&p.pid);
if f_prod.pid == utils::empty_string() { if f_prod.pid == utils::empty_string() {
@ -143,7 +144,7 @@ pub async fn get_vendor_product(
let proxy = reqwest::Proxy::http(&host)?; let proxy = reqwest::Proxy::http(&host)?;
let client = reqwest::Client::builder().proxy(proxy).build(); let client = reqwest::Client::builder().proxy(proxy).build();
match client? match client?
.get(format!("http://{}/market/product/{}", contact, pid)) .get(format!("http://{}/market/{}", contact, pid))
.header("proof", jwp) .header("proof", jwp)
.send() .send()
.await .await

View file

@ -29,6 +29,8 @@ pub struct ContactStatus {
pub h_exp: String, pub h_exp: String,
/// i2p address of current status check /// i2p address of current status check
pub i2p: String, pub i2p: String,
/// update vendor status of contact
pub is_vendor: bool,
/// JSON Web Proof of current status check /// JSON Web Proof of current status check
pub jwp: String, pub jwp: String,
/// Alias for contact /// Alias for contact
@ -45,6 +47,7 @@ impl Default for ContactStatus {
exp: utils::empty_string(), exp: utils::empty_string(),
h_exp: utils::empty_string(), h_exp: utils::empty_string(),
i2p: utils::empty_string(), i2p: utils::empty_string(),
is_vendor: false,
jwp: utils::empty_string(), jwp: utils::empty_string(),
nick: String::from("anon"), nick: String::from("anon"),
signed_key: false, signed_key: false,

View file

@ -446,7 +446,7 @@ impl eframe::App for AddressBookApp {
if ui.button("Add").clicked() { if ui.button("Add").clicked() {
// Get the contacts information from the /share API // Get the contacts information from the /share API
let contact = self.contact.clone(); let contact = self.contact.clone();
send_contact_info_req(self.contact_info_tx.clone(), ctx.clone(), contact); send_contact_info_req(self.contact_info_tx.clone(), ctx.clone(), contact, 0);
add_contact_timeout(self.contact_timeout_tx.clone(), ctx.clone()); add_contact_timeout(self.contact_timeout_tx.clone(), ctx.clone());
self.is_adding = true; self.is_adding = true;
} }
@ -548,6 +548,7 @@ impl eframe::App for AddressBookApp {
self.contact_info_tx.clone(), self.contact_info_tx.clone(),
ctx.clone(), ctx.clone(),
self.status.i2p.clone(), self.status.i2p.clone(),
1,
); );
self.showing_status = true; self.showing_status = true;
self.is_pinging = true; self.is_pinging = true;
@ -579,10 +580,15 @@ impl eframe::App for AddressBookApp {
// Send asyc requests to neveko-core // Send asyc requests to neveko-core
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
fn send_contact_info_req(tx: Sender<models::Contact>, ctx: egui::Context, contact: String) { fn send_contact_info_req(
tx: Sender<models::Contact>,
ctx: egui::Context,
contact: String,
prune: u32,
) {
log::debug!("async send_contact_info_req"); log::debug!("async send_contact_info_req");
tokio::spawn(async move { tokio::spawn(async move {
match contact::add_contact_request(contact).await { match contact::add_contact_request(contact, prune).await {
Ok(contact) => { Ok(contact) => {
let _ = tx.send(contact); let _ = tx.send(contact);
ctx.request_repaint(); ctx.request_repaint();

View file

@ -7,11 +7,14 @@ use std::sync::mpsc::{
pub struct MarketApp { pub struct MarketApp {
contact_info_tx: Sender<models::Contact>, contact_info_tx: Sender<models::Contact>,
contact_info_rx: Receiver<models::Contact>, contact_info_rx: Receiver<models::Contact>,
contact_timeout_tx: Sender<bool>,
contact_timeout_rx: Receiver<bool>,
find_vendor: String, find_vendor: String,
get_vendor_products_tx: Sender<Vec<models::Product>>, get_vendor_products_tx: Sender<Vec<models::Product>>,
get_vendor_products_rx: Receiver<Vec<models::Product>>, get_vendor_products_rx: Receiver<Vec<models::Product>>,
get_vendor_product_tx: Sender<models::Product>, get_vendor_product_tx: Sender<models::Product>,
get_vendor_product_rx: Receiver<models::Product>, get_vendor_product_rx: Receiver<models::Product>,
is_loading: bool,
is_ordering: bool, is_ordering: bool,
is_pinging: bool, is_pinging: bool,
is_product_image_set: bool, is_product_image_set: bool,
@ -21,6 +24,7 @@ pub struct MarketApp {
is_showing_orders: bool, is_showing_orders: bool,
is_showing_vendor_status: bool, is_showing_vendor_status: bool,
is_showing_vendors: bool, is_showing_vendors: bool,
is_timeout: bool,
is_vendor_enabled: bool, is_vendor_enabled: bool,
is_window_shopping: bool, is_window_shopping: bool,
orders: Vec<models::Order>, orders: Vec<models::Order>,
@ -36,13 +40,13 @@ pub struct MarketApp {
_refresh_on_delete_product_tx: Sender<bool>, _refresh_on_delete_product_tx: Sender<bool>,
_refresh_on_delete_product_rx: Receiver<bool>, _refresh_on_delete_product_rx: Receiver<bool>,
s_contact: models::Contact, s_contact: models::Contact,
showing_vendor_status: bool,
vendor_status: utils::ContactStatus, vendor_status: utils::ContactStatus,
vendors: Vec<models::Contact>, vendors: Vec<models::Contact>,
} }
impl Default for MarketApp { impl Default for MarketApp {
fn default() -> Self { fn default() -> Self {
let (contact_timeout_tx, contact_timeout_rx) = std::sync::mpsc::channel();
let (_refresh_on_delete_product_tx, _refresh_on_delete_product_rx) = let (_refresh_on_delete_product_tx, _refresh_on_delete_product_rx) =
std::sync::mpsc::channel(); std::sync::mpsc::channel();
let read_product_image = std::fs::read("./assets/qr.png").unwrap_or(Vec::new()); let read_product_image = std::fs::read("./assets/qr.png").unwrap_or(Vec::new());
@ -55,11 +59,14 @@ impl Default for MarketApp {
MarketApp { MarketApp {
contact_info_rx, contact_info_rx,
contact_info_tx, contact_info_tx,
contact_timeout_rx,
contact_timeout_tx,
find_vendor: utils::empty_string(), find_vendor: utils::empty_string(),
get_vendor_products_rx, get_vendor_products_rx,
get_vendor_products_tx, get_vendor_products_tx,
get_vendor_product_rx, get_vendor_product_rx,
get_vendor_product_tx, get_vendor_product_tx,
is_loading: false,
is_ordering: false, is_ordering: false,
is_pinging: false, is_pinging: false,
is_product_image_set: false, is_product_image_set: false,
@ -69,6 +76,7 @@ impl Default for MarketApp {
is_showing_product_update: false, is_showing_product_update: false,
is_showing_vendor_status: false, is_showing_vendor_status: false,
is_showing_vendors: false, is_showing_vendors: false,
is_timeout: false,
is_vendor_enabled, is_vendor_enabled,
is_window_shopping: false, is_window_shopping: false,
orders: Vec::new(), orders: Vec::new(),
@ -88,7 +96,6 @@ impl Default for MarketApp {
_refresh_on_delete_product_tx, _refresh_on_delete_product_tx,
_refresh_on_delete_product_rx, _refresh_on_delete_product_rx,
s_contact: Default::default(), s_contact: Default::default(),
showing_vendor_status: false,
vendor_status: Default::default(), vendor_status: Default::default(),
vendors: Vec::new(), vendors: Vec::new(),
} }
@ -99,17 +106,54 @@ impl eframe::App for MarketApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
// Hook into async channel threads // Hook into async channel threads
//----------------------------------------------------------------------------------- //-----------------------------------------------------------------------------------
if let Ok(contact_info) = self.contact_info_rx.try_recv() { if let Ok(contact_info) = self.contact_info_rx.try_recv() {
self.s_contact = contact_info; self.s_contact = contact_info;
if self.s_contact.xmr_address != utils::empty_string() { if self.s_contact.xmr_address != utils::empty_string() {
self.is_pinging = false; self.is_pinging = false;
self.vendor_status.is_vendor = self.s_contact.is_vendor;
} }
} }
if let Ok(vendor_products) = self.get_vendor_products_rx.try_recv() { if let Ok(vendor_products) = self.get_vendor_products_rx.try_recv() {
self.is_loading = false;
self.products = vendor_products; self.products = vendor_products;
} }
if let Ok(vendor_product) = self.get_vendor_product_rx.try_recv() { if let Ok(vendor_product) = self.get_vendor_product_rx.try_recv() {
self.is_loading = false;
if !vendor_product.image.is_empty() {
// only pull image from vendor when we want to view
let file_path = format!(
"/home/{}/.neveko/{}.jpeg",
std::env::var("USER").unwrap_or(String::from("user")),
vendor_product.pid
);
if self.is_window_shopping {
self.is_loading = true;
let contents = std::fs::read(&file_path).unwrap_or(Vec::new());
// this image should uwrap if vendor image bytes are
// bad
let default_img = std::fs::read("./assets/qr.png").unwrap_or(Vec::new());
let default_r_img =
egui_extras::RetainedImage::from_image_bytes("qr.png", &default_img)
.unwrap();
self.product_image =
egui_extras::RetainedImage::from_image_bytes(file_path, &contents)
.unwrap_or(default_r_img);
}
}
self.product_from_vendor = vendor_product; self.product_from_vendor = vendor_product;
self.is_product_image_set = true;
self.is_showing_product_image = true;
self.is_loading = false;
}
if let Ok(timeout) = self.contact_timeout_rx.try_recv() {
self.is_timeout = true;
if timeout {
self.is_pinging = false;
}
} }
// TODO(c2m): create order form // TODO(c2m): create order form
@ -163,7 +207,7 @@ impl eframe::App for MarketApp {
}) })
.body(|mut body| { .body(|mut body| {
for v in &self.vendors { for v in &self.vendors {
if v.i2p_address.contains(&self.find_vendor) && v.is_vendor { if v.i2p_address.contains(&self.find_vendor) {
let row_height = 20.0; let row_height = 20.0;
body.row(row_height, |mut row| { body.row(row_height, |mut row| {
row.col(|ui| { row.col(|ui| {
@ -195,6 +239,7 @@ impl eframe::App for MarketApp {
String::from("gui-jwp"), String::from("gui-jwp"),
String::from(&v.i2p_address), String::from(&v.i2p_address),
); );
log::debug!("jwp: {}", self.vendor_status.jwp);
let r_exp = utils::search_gui_db( let r_exp = utils::search_gui_db(
String::from("gui-exp"), String::from("gui-exp"),
String::from(&v.i2p_address), String::from(&v.i2p_address),
@ -220,7 +265,11 @@ impl eframe::App for MarketApp {
ctx.clone(), ctx.clone(),
self.vendor_status.i2p.clone(), self.vendor_status.i2p.clone(),
); );
self.showing_vendor_status = true; vendor_status_timeout(
self.contact_timeout_tx.clone(),
ctx.clone(),
);
self.is_showing_vendor_status = true;
self.is_pinging = true; self.is_pinging = true;
} }
}); });
@ -234,8 +283,10 @@ impl eframe::App for MarketApp {
&& self.vendor_status.signed_key && self.vendor_status.signed_key
&& self.vendor_status.jwp != utils::empty_string() && self.vendor_status.jwp != utils::empty_string()
&& v.i2p_address == self.vendor_status.i2p && v.i2p_address == self.vendor_status.i2p
&& self.vendor_status.is_vendor
{ {
if ui.button("View Products").clicked() { if ui.button("View Products").clicked() {
self.is_loading = true;
send_products_from_vendor_req( send_products_from_vendor_req(
self.get_vendor_products_tx.clone(), self.get_vendor_products_tx.clone(),
ctx.clone(), ctx.clone(),
@ -244,6 +295,7 @@ impl eframe::App for MarketApp {
); );
self.is_window_shopping = true; self.is_window_shopping = true;
self.is_showing_products = true; self.is_showing_products = true;
self.is_showing_vendors = false;
} }
} }
}); });
@ -274,7 +326,13 @@ impl eframe::App for MarketApp {
} else { } else {
"offline" "offline"
}; };
let mode = if self.vendor_status.is_vendor {
"enabled "
} else {
"disabled"
};
ui.label(format!("status: {}", status)); ui.label(format!("status: {}", status));
ui.label(format!("vendor mode: {}", mode));
ui.label(format!("nick: {}", self.vendor_status.nick)); ui.label(format!("nick: {}", self.vendor_status.nick));
ui.label(format!("tx proof: {}", self.vendor_status.txp)); ui.label(format!("tx proof: {}", self.vendor_status.txp));
ui.label(format!("jwp: {}", self.vendor_status.jwp)); ui.label(format!("jwp: {}", self.vendor_status.jwp));
@ -314,7 +372,10 @@ impl eframe::App for MarketApp {
Column, Column,
TableBuilder, TableBuilder,
}; };
if self.is_loading {
ui.add(egui::Spinner::new());
ui.label("loading...");
}
let table = TableBuilder::new(ui) let table = TableBuilder::new(ui)
.striped(true) .striped(true)
.resizable(true) .resizable(true)
@ -367,7 +428,6 @@ impl eframe::App for MarketApp {
row.col(|ui| { row.col(|ui| {
if ui.button("View").clicked() { if ui.button("View").clicked() {
if !self.is_product_image_set { if !self.is_product_image_set {
self.is_showing_product_image = true;
let file_path = format!( let file_path = format!(
"/home/{}/.neveko/{}.jpeg", "/home/{}/.neveko/{}.jpeg",
std::env::var("USER") std::env::var("USER")
@ -376,9 +436,8 @@ impl eframe::App for MarketApp {
); );
// For the sake of brevity product list doesn't have // For the sake of brevity product list doesn't have
// image bytes, get them // image bytes, get them
let mut i_product = product::find(&p.pid);
// only pull image from vendor when we want to view
if self.is_window_shopping { if self.is_window_shopping {
self.is_loading = true;
send_product_from_vendor_req( send_product_from_vendor_req(
self.get_vendor_product_tx.clone(), self.get_vendor_product_tx.clone(),
ctx.clone(), ctx.clone(),
@ -386,52 +445,38 @@ impl eframe::App for MarketApp {
self.vendor_status.jwp.clone(), self.vendor_status.jwp.clone(),
String::from(&p.pid), String::from(&p.pid),
); );
let e_product: models::Product = models::Product { } else {
pid: self.product_from_vendor.pid.clone(), let i_product = product::find(&p.pid);
description: self match std::fs::write(&file_path, &i_product.image) {
.product_from_vendor Ok(w) => w,
.description Err(_) => {
.clone(), log::error!("failed to write product image")
image: self }
.product_from_vendor
.image
.iter()
.cloned()
.collect(),
in_stock: self.product_from_vendor.in_stock,
name: self.product_from_vendor.name.clone(),
price: self.product_from_vendor.price,
qty: self.product_from_vendor.qty,
}; };
i_product = e_product; let contents =
}
match std::fs::write(&file_path, &i_product.image) {
Ok(w) => w,
Err(_) => {
log::error!("failed to write product image")
}
};
self.is_product_image_set = true;
let contents =
std::fs::read(&file_path).unwrap_or(Vec::new()); std::fs::read(&file_path).unwrap_or(Vec::new());
if !i_product.image.is_empty() { if !i_product.image.is_empty() {
// this image should uwrap if vendor image bytes are // this image should uwrap if vendor image bytes are
// bad // bad
let default_img = std::fs::read("./assets/qr.png") let default_img = std::fs::read("./assets/qr.png")
.unwrap_or(Vec::new()); .unwrap_or(Vec::new());
let default_r_img = let default_r_img =
egui_extras::RetainedImage::from_image_bytes( egui_extras::RetainedImage::from_image_bytes(
"qr.png", "qr.png",
&default_img, &default_img,
) )
.unwrap(); .unwrap();
self.product_image = self.product_image =
egui_extras::RetainedImage::from_image_bytes( egui_extras::RetainedImage::from_image_bytes(
file_path, &contents, file_path, &contents,
) )
.unwrap_or(default_r_img); .unwrap_or(default_r_img);
}
}
if !self.is_window_shopping {
self.is_product_image_set = true;
self.is_showing_product_image = true;
} }
self.is_product_image_set = true;
ctx.request_repaint(); ctx.request_repaint();
} }
} }
@ -633,6 +678,8 @@ impl eframe::App for MarketApp {
} }
}); });
if ui.button("View Vendors").clicked() { if ui.button("View Vendors").clicked() {
// assume all contacts are vendors until updated status check
self.vendors = contact::find_all();
self.is_showing_vendors = true; self.is_showing_vendors = true;
} }
ui.label("\n"); ui.label("\n");
@ -704,6 +751,7 @@ impl eframe::App for MarketApp {
if ui.button("View Products").clicked() { if ui.button("View Products").clicked() {
self.products = product::find_all(); self.products = product::find_all();
self.is_showing_products = true; self.is_showing_products = true;
self.is_showing_vendors = false;
} }
} }
}); });
@ -723,7 +771,7 @@ fn _refresh_on_delete_product_req(_tx: Sender<bool>, _ctx: egui::Context) {
fn send_contact_info_req(tx: Sender<models::Contact>, ctx: egui::Context, contact: String) { fn send_contact_info_req(tx: Sender<models::Contact>, ctx: egui::Context, contact: String) {
log::debug!("async send_contact_info_req"); log::debug!("async send_contact_info_req");
tokio::spawn(async move { tokio::spawn(async move {
match contact::add_contact_request(contact).await { match contact::add_contact_request(contact, 1).await {
Ok(contact) => { Ok(contact) => {
let _ = tx.send(contact); let _ = tx.send(contact);
ctx.request_repaint(); ctx.request_repaint();
@ -774,3 +822,15 @@ fn send_product_from_vendor_req(
} }
}); });
} }
fn vendor_status_timeout(tx: Sender<bool>, ctx: egui::Context) {
tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(
crate::ADD_CONTACT_TIMEOUT_SECS,
))
.await;
log::error!("vendor status timeout");
let _ = tx.send(true);
ctx.request_repaint();
});
}

View file

@ -36,12 +36,23 @@ pub async fn get_i2p_status() -> Custom<Json<i2p::HttpProxyStatus>> {
} }
} }
/// Share your contact information /// Share your contact information.
///
/// 0 - returns full info with gpg key
///
/// 1 - return pruned info without gpg key
/// ///
/// Protected: false /// Protected: false
#[get("/")] #[get("/<pruned>")]
pub async fn share_contact_info() -> Custom<Json<models::Contact>> { pub async fn share_contact_info(pruned: u32) -> Custom<Json<models::Contact>> {
let info: models::Contact = contact::share().await; let info: models::Contact = contact::share().await;
if pruned == 1 {
let p_info: models::Contact = models::Contact {
gpg_key: Vec::new(),
..info
};
return Custom(Status::Ok, Json(p_info));
}
Custom(Status::Ok, Json(info)) Custom(Status::Ok, Json(info))
} }

View file

@ -40,6 +40,7 @@ async fn rocket() -> _ {
routes![ routes![
controller::create_order, controller::create_order,
controller::create_dispute, controller::create_dispute,
controller::get_product,
controller::get_products, controller::get_products,
controller::finalize_order, controller::finalize_order,
controller::request_shipment, controller::request_shipment,