1
0
forked from GRIN/grim

Build 19: open-wallet shortcuts, Pay QR scan, configurable pairing, hide yellow

- Lower-left sidebar cards are now individual shortcuts: tapping the
  identity chip opens identity settings, tapping the node card jumps
  straight to the Node menu (was: both opened generic settings).
- Pay screen gains a scan-to-pay QR puck top-right that opens the camera
  and prefills the recipient while keeping the typed amount (reuses the
  Home scan + SendFlow::request_scan/prefill_amount path).
- Replace the USD-only "fiat preview" with a configurable "Pairing":
  Off / USD / EUR / GBP / JPY / CNY / Bitcoin / Sats (default USD). price.rs
  now fetches GRIN against any vs_currency (sats price vs btc, ×1e8) and
  caches per code; a Settings → Pairing sub-page picks it; the Pay, send,
  and balance previews all route through one pairing_preview() helper.
- Hide the Yellow theme from the picker (cycle is Dark ↔ Light now); the
  ThemeKind::Yellow tokens stay defined — it's beta, not removed.

Verified live on :2 (Default wallet): node card → Node menu; identity chip
→ identity settings; QR puck renders top-right of Pay; Pairing → Sats shows
"≈ 1,912 sats" for 50ツ over a live BTC rate, resets to USD; theme cycles
Dark↔Light, never Yellow. 34 lib tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-11 22:22:39 -04:00
parent 0438d70cae
commit 15c19303ff
5 changed files with 328 additions and 107 deletions
+182 -57
View File
@@ -92,6 +92,7 @@ enum SettingsPage {
Node,
Relays,
Nips,
Pairing,
}
impl Default for GoblinWalletView {
@@ -496,14 +497,25 @@ impl GoblinWalletView {
}
// Node status + profile cards pinned to the bottom (node info lives
// here so the surface needs no separate network column).
// here so the surface needs no separate network column). Each card is
// its own shortcut: the node card opens the Node menu, the identity
// chip opens identity settings.
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
let width = ui.available_width();
let bottom = ui.allocate_ui_with_layout(
ui.allocate_ui_with_layout(
Vec2::new(width, 196.0),
Layout::top_down(Align::Min),
|ui| {
self.node_card_ui(ui, wallet);
// Node status card → Settings → Node menu.
let node = ui
.scope(|ui| self.node_card_ui(ui, wallet))
.response
.interact(Sense::click())
.on_hover_cursor(egui::CursorIcon::PointingHand);
if node.clicked() {
self.tab = Tab::Me;
self.settings_page = SettingsPage::Node;
}
ui.add_space(8.0);
let (handle, connected, npub_hex) = wallet
.nostr_service()
@@ -519,35 +531,42 @@ impl GoblinWalletView {
.unwrap_or_else(|| ("Anonymous".to_string(), false, String::new()));
let hue = data::hue_of(&npub_hex);
let tex = self.handle_tex(ui.ctx(), wallet, &handle);
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
ui.horizontal(|ui| {
w::avatar_any(ui, &handle, 36.0, hue, tex.as_ref());
ui.add_space(10.0);
ui.vertical(|ui| {
ui.label(
RichText::new(&handle)
.font(FontId::new(14.0, fonts::semibold()))
.color(t.surface_text),
);
ui.label(
RichText::new(if connected {
"synced · Tor"
} else {
"connecting…"
})
.font(FontId::new(12.0, fonts::regular()))
.color(t.surface_text_mute),
);
// Identity chip → identity settings.
let id_resp = ui
.scope(|ui| {
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
ui.horizontal(|ui| {
w::avatar_any(ui, &handle, 36.0, hue, tex.as_ref());
ui.add_space(10.0);
ui.vertical(|ui| {
ui.label(
RichText::new(&handle)
.font(FontId::new(14.0, fonts::semibold()))
.color(t.surface_text),
);
ui.label(
RichText::new(if connected {
"synced · Tor"
} else {
"connecting…"
})
.font(FontId::new(12.0, fonts::regular()))
.color(t.surface_text_mute),
);
});
});
});
});
});
})
.response
.interact(Sense::click())
.on_hover_cursor(egui::CursorIcon::PointingHand);
if id_resp.clicked() {
self.tab = Tab::Me;
self.settings_page = SettingsPage::Main;
}
},
);
// Both bottom cards (node status + profile) open Settings.
if bottom.response.interact(Sense::click()).clicked() {
self.tab = Tab::Me;
}
});
}
@@ -789,11 +808,37 @@ impl GoblinWalletView {
fn pay_ui(&mut self, ui: &mut egui::Ui, _wallet: &Wallet) {
let t = theme::tokens();
ui.add_space(8.0);
ui.label(
RichText::new("Pay")
.font(FontId::new(28.0, fonts::bold()))
.color(t.text),
);
ui.horizontal(|ui| {
ui.label(
RichText::new("Pay")
.font(FontId::new(28.0, fonts::bold()))
.color(t.text),
);
// Scan-to-pay QR, top-right (mirrors the Home header scan puck):
// open the scanner with the typed amount preserved.
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
let (rect, resp) = ui.allocate_exact_size(Vec2::splat(36.0), Sense::click());
ui.painter().circle_filled(rect.center(), 18.0, t.surface2);
ui.painter().text(
rect.center(),
egui::Align2::CENTER_CENTER,
QR_CODE,
FontId::new(17.0, fonts::regular()),
t.surface_text,
);
if resp
.on_hover_cursor(egui::CursorIcon::PointingHand)
.on_hover_text("Scan to pay")
.clicked()
{
let mut f = SendFlow::default();
f.prefill_amount(self.pay_amount.clone());
f.request_scan();
self.pay_amount.clear();
self.send = Some(f);
}
});
});
// Big centered amount.
let display = if self.pay_amount.is_empty() {
@@ -804,12 +849,12 @@ impl GoblinWalletView {
let tall = ui.available_height() > 560.0;
ui.add_space(if tall { 56.0 } else { 24.0 });
w::amount_text_centered(ui, &display, 76.0);
if let Some(rate) = crate::http::grin_usd_rate() {
if let Ok(grin) = display.parse::<f64>() {
if let Ok(grin) = display.parse::<f64>() {
if let Some(preview) = pairing_preview(grin) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(format!("≈ ${:.2}", grin * rate))
RichText::new(preview)
.font(FontId::new(14.0, fonts::regular()))
.color(t.text_dim),
);
@@ -1178,6 +1223,7 @@ impl GoblinWalletView {
SettingsPage::Node => return self.node_settings_ui(ui, wallet),
SettingsPage::Relays => return self.relays_ui(ui, wallet),
SettingsPage::Nips => return self.nips_ui(ui),
SettingsPage::Pairing => return self.pairing_settings_ui(ui),
SettingsPage::Main => {}
}
ui.add_space(8.0);
@@ -1436,22 +1482,21 @@ impl GoblinWalletView {
}
ui.add_space(16.0);
let mut open_pairing = false;
settings_group(ui, "Privacy", |ui| {
settings_row(ui, "Tor routing", "All payments routed over Tor");
// Tap to cycle the incoming-payment accept policy.
if settings_row_btn(ui, "Auto-accept", accept_policy_label(wallet)) {
cycle_accept_policy(wallet);
}
// Tap to toggle the fiat (USD) preview.
let fiat = if crate::AppConfig::fiat_preview() {
"On"
} else {
"Off"
};
if settings_row_btn(ui, "Fiat preview (USD)", fiat) {
crate::AppConfig::toggle_fiat_preview();
// Amount pairing: what the ≈ preview is shown against.
if settings_row_nav(ui, "Pairing", crate::AppConfig::pairing().label()) {
open_pairing = true;
}
});
if open_pairing {
self.settings_page = SettingsPage::Pairing;
}
ui.add_space(16.0);
w::kicker(ui, "Appearance");
@@ -1547,6 +1592,52 @@ impl GoblinWalletView {
}
/// Node connection editor: pick integrated/external, add or remove nodes.
fn pairing_settings_ui(&mut self, ui: &mut egui::Ui) {
let t = theme::tokens();
if self.sub_header(ui, "Pairing") {
self.settings_page = SettingsPage::Main;
return;
}
ScrollArea::vertical()
.auto_shrink([false; 2])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
.show(ui, |ui| {
ui.label(
RichText::new("What your balance and amounts are shown against.")
.font(FontId::new(13.0, fonts::regular()))
.color(t.text_dim),
);
ui.add_space(12.0);
let current = crate::AppConfig::pairing();
settings_group(ui, "Pair with", |ui| {
for p in crate::settings::Pairing::ALL {
let active = p == current;
let row = ui.horizontal(|ui| {
ui.label(
RichText::new(p.label())
.font(FontId::new(15.0, fonts::medium()))
.color(t.surface_text),
);
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if active {
ui.label(
RichText::new(crate::gui::icons::CHECK)
.font(FontId::new(16.0, fonts::regular()))
.color(t.pos),
);
}
});
});
ui.add_space(10.0);
if !active && row.response.interact(Sense::click()).clicked() {
crate::AppConfig::set_pairing(p);
}
}
});
ui.add_space(16.0);
});
}
fn node_settings_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet) {
use crate::wallet::types::ConnectionMethod;
use crate::wallet::{ConnectionsConfig, ExternalConnection};
@@ -2754,12 +2845,14 @@ fn accept_policy_label(wallet: &Wallet) -> &'static str {
.unwrap_or("Anyone")
}
/// Cycle the color theme Dark Light → Yellow → Dark and re-apply visuals.
/// Cycle the color theme Dark Light and re-apply visuals. Yellow is kept
/// defined (gui/theme.rs) but out of the picker for now — it's still in beta;
/// `Yellow => Dark` is an escape hatch for anyone whose config already has it.
fn cycle_theme(ctx: &egui::Context) {
use crate::gui::theme::ThemeKind;
let next = match crate::AppConfig::theme() {
ThemeKind::Dark => ThemeKind::Light,
ThemeKind::Light => ThemeKind::Yellow,
ThemeKind::Light => ThemeKind::Dark,
ThemeKind::Yellow => ThemeKind::Dark,
};
crate::AppConfig::set_theme(next);
@@ -2842,16 +2935,48 @@ fn fmt_thousands(n: u64) -> String {
}
fn fiat_line(data: &Option<WalletData>) -> Option<String> {
let _ = data;
// Fiat rate provider is wired in P7; hide the line until a rate is available.
crate::http::grin_usd_rate().map(|rate| {
let spendable = data
.as_ref()
.map(|d| d.info.amount_currently_spendable)
.unwrap_or(0);
let grin = spendable as f64 / 1_000_000_000.0;
format!("≈ ${:.2} · 1ツ = ${:.4}", grin * rate, rate)
})
let p = crate::AppConfig::pairing();
let vs = p.vs_currency()?;
let rate = crate::http::grin_rate(vs)?;
let spendable = data
.as_ref()
.map(|d| d.info.amount_currently_spendable)
.unwrap_or(0);
let grin = spendable as f64 / 1_000_000_000.0;
Some(format!(
"{} · 1ツ = {}",
fmt_pairing(grin * rate, p),
fmt_pairing(rate, p)
))
}
/// Format a value already in the pairing's unit (dollars, BTC, …) with the
/// right symbol/precision. Sats scales the BTC value by 1e8.
fn fmt_pairing(value: f64, p: crate::settings::Pairing) -> String {
use crate::settings::Pairing;
match p {
Pairing::Usd => format!("${:.2}", value),
Pairing::Eur => format!("{:.2}", value),
Pairing::Gbp => format!("£{:.2}", value),
Pairing::Jpy => format!("¥{:.0}", value),
Pairing::Cny => format!("CN¥{:.2}", value),
Pairing::Btc => {
let s = format!("{:.8}", value);
let s = s.trim_end_matches('0').trim_end_matches('.');
format!("{}", if s.is_empty() { "0" } else { s })
}
Pairing::Sats => format!("{} sats", fmt_thousands((value * 1e8).round() as u64)),
Pairing::Off => String::new(),
}
}
/// The "≈ …" amount preview for the current pairing, or `None` when off / no
/// rate yet. Shared by the Pay screen, the send flow, and the balance hero.
fn pairing_preview(grin: f64) -> Option<String> {
let p = crate::AppConfig::pairing();
let vs = p.vs_currency()?;
let rate = crate::http::grin_rate(vs)?;
Some(format!("{}", fmt_pairing(grin * rate, p)))
}
/// Convert a bech32 npub to hex for short display fallbacks.
+3 -3
View File
@@ -715,12 +715,12 @@ impl SendFlow {
self.amount.clone()
};
w::amount_text_centered(ui, &display, 64.0);
if let Some(rate) = crate::http::grin_usd_rate() {
if let Ok(grin) = display.parse::<f64>() {
if let Ok(grin) = display.parse::<f64>() {
if let Some(preview) = super::pairing_preview(grin) {
ui.add_space(6.0);
ui.vertical_centered(|ui| {
ui.label(
RichText::new(format!("≈ ${:.2}", grin * rate))
RichText::new(preview)
.font(FontId::new(14.0, fonts::regular()))
.color(t.text_dim),
);