mirror of
https://code.gri.mw/GUI/grim.git
synced 2026-07-12 01:38:54 +00:00
Build 42: block over-balance sends + roomier search icon
Guard against paying more than the spendable balance at every entry: the Pay tab, the send-flow amount step, and the Review screen all render the amount in red with "You don't have enough grin" and refuse to proceed, so a payment that would only fail later at the node is stopped up front. On Android the phone gives a short error buzz on the blocked tap — new VIBRATE bridge (PlatformCallbacks::vibrate_error -> MainActivity.vibrateError); no-op on desktop. Grim silently clamped the amount and showed nothing. Also enlarge and vertically center the magnifier in the "Send to" search field (an 18px muted label became a 22px centered glyph).
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage"/>
|
||||
|
||||
@@ -510,6 +510,21 @@ public class MainActivity extends GameActivity {
|
||||
startActivity(Intent.createChooser(intent, "Share data"));
|
||||
}
|
||||
|
||||
// Called from native code to play a short "error" haptic (rejected payment).
|
||||
public void vibrateError() {
|
||||
Vibrator vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
|
||||
if (vibrator == null || !vibrator.hasVibrator()) {
|
||||
return;
|
||||
}
|
||||
// Two short pulses read as "no" / rejected, distinct from a tap.
|
||||
long[] pattern = new long[]{0, 40, 70, 40};
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
vibrator.vibrate(VibrationEffect.createWaveform(pattern, -1));
|
||||
} else {
|
||||
vibrator.vibrate(pattern, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// Called from native code to set status-bar icon color to contrast the
|
||||
// in-app theme. white = light icons for a dark background. The app draws
|
||||
// edge-to-edge, so the OS status-bar background is the app's own content;
|
||||
|
||||
@@ -207,6 +207,10 @@ impl PlatformCallbacks for Android {
|
||||
&[JValue::Bool(white as u8)],
|
||||
);
|
||||
}
|
||||
|
||||
fn vibrate_error(&self) {
|
||||
let _ = self.call_java_method("vibrateError", "()V", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
|
||||
@@ -47,4 +47,8 @@ pub trait PlatformCallbacks {
|
||||
/// Set the status-bar icon color to contrast the current theme. `white` =
|
||||
/// light icons (for a dark background). No-op off Android.
|
||||
fn set_status_bar_white_icons(&self, _white: bool) {}
|
||||
|
||||
/// Play a short "error" haptic (e.g. a rejected over-balance payment).
|
||||
/// No-op off Android.
|
||||
fn vibrate_error(&self) {}
|
||||
}
|
||||
|
||||
@@ -323,7 +323,7 @@ impl GoblinWalletView {
|
||||
.show_inside(ui, |ui| {
|
||||
w::centered_column(ui, Content::SIDE_PANEL_WIDTH * 1.2, |ui| match self.tab {
|
||||
Tab::Home => self.home_ui(ui, wallet, cb, wide_desktop),
|
||||
Tab::Pay => self.pay_ui(ui, wallet),
|
||||
Tab::Pay => self.pay_ui(ui, wallet, cb),
|
||||
Tab::Activity => self.activity_ui(ui, wallet, cb),
|
||||
Tab::Receive => self.receive_ui(ui, wallet, cb),
|
||||
Tab::Me => self.me_ui(ui, wallet, cb),
|
||||
@@ -806,7 +806,7 @@ impl GoblinWalletView {
|
||||
}
|
||||
|
||||
/// Pay tab: amount-first combined pay/request surface.
|
||||
fn pay_ui(&mut self, ui: &mut egui::Ui, _wallet: &Wallet) {
|
||||
fn pay_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
|
||||
let t = theme::tokens();
|
||||
ui.add_space(8.0);
|
||||
ui.horizontal(|ui| {
|
||||
@@ -848,8 +848,22 @@ impl GoblinWalletView {
|
||||
self.pay_amount.clone()
|
||||
};
|
||||
let tall = ui.available_height() > 560.0;
|
||||
// Block paying more than the spendable balance: red amount + a message
|
||||
// + an error buzz on tap (Request is unguarded — you can request more
|
||||
// than you hold).
|
||||
let spendable = wallet
|
||||
.get_data()
|
||||
.map(|d| d.info.amount_currently_spendable)
|
||||
.unwrap_or(0);
|
||||
let over = grin_core::core::amount_from_hr_string(&self.pay_amount)
|
||||
.map(|a| a > spendable)
|
||||
.unwrap_or(false);
|
||||
ui.add_space(if tall { 56.0 } else { 24.0 });
|
||||
w::amount_text_centered(ui, &display, 76.0);
|
||||
if over {
|
||||
w::amount_text_centered_ink(ui, &display, 76.0, t.neg, t.neg);
|
||||
} else {
|
||||
w::amount_text_centered(ui, &display, 76.0);
|
||||
}
|
||||
if let Ok(grin) = display.parse::<f64>() {
|
||||
if let Some(preview) = pairing_preview(grin) {
|
||||
ui.add_space(6.0);
|
||||
@@ -862,6 +876,16 @@ impl GoblinWalletView {
|
||||
});
|
||||
}
|
||||
}
|
||||
if over {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("You don't have enough grin")
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
});
|
||||
}
|
||||
ui.add_space(if tall { 32.0 } else { 16.0 });
|
||||
|
||||
// Numpad at narrow (mobile-shell) widths, typed input on the wide
|
||||
@@ -912,10 +936,14 @@ impl GoblinWalletView {
|
||||
)),
|
||||
|ui| {
|
||||
if w::big_action(ui, "Pay", false).clicked() && valid {
|
||||
let mut f = SendFlow::default();
|
||||
f.prefill_amount(self.pay_amount.clone());
|
||||
self.pay_amount.clear();
|
||||
self.send = Some(f);
|
||||
if over {
|
||||
cb.vibrate_error();
|
||||
} else {
|
||||
let mut f = SendFlow::default();
|
||||
f.prefill_amount(self.pay_amount.clone());
|
||||
self.pay_amount.clear();
|
||||
self.send = Some(f);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -278,10 +278,14 @@ impl SendFlow {
|
||||
}
|
||||
.show(ui, |ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
RichText::new(MAGNIFYING_GLASS)
|
||||
.font(FontId::new(18.0, fonts::regular()))
|
||||
.color(t.surface_text_mute),
|
||||
let (icon_rect, _) =
|
||||
ui.allocate_exact_size(egui::Vec2::new(24.0, 42.0), egui::Sense::hover());
|
||||
ui.painter().text(
|
||||
icon_rect.center(),
|
||||
egui::Align2::CENTER_CENTER,
|
||||
MAGNIFYING_GLASS,
|
||||
FontId::new(22.0, fonts::regular()),
|
||||
t.surface_text_dim,
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
let mut te = TextEdit::new(egui::Id::from("send_search"))
|
||||
@@ -716,6 +720,16 @@ impl SendFlow {
|
||||
}
|
||||
let recipient = self.recipient.clone().unwrap();
|
||||
|
||||
// Block sends over the spendable balance: red amount + a message + an
|
||||
// error buzz on tap, rather than letting it fail later at the node.
|
||||
let spendable = wallet
|
||||
.get_data()
|
||||
.map(|d| d.info.amount_currently_spendable)
|
||||
.unwrap_or(0);
|
||||
let over = amount_from_hr_string(&self.amount)
|
||||
.map(|a| a > spendable)
|
||||
.unwrap_or(false);
|
||||
|
||||
// Recipient chip, centered per the design.
|
||||
let name_label = format!("To {}", recipient.name);
|
||||
let name_galley = ui.painter().layout_no_wrap(
|
||||
@@ -743,7 +757,11 @@ impl SendFlow {
|
||||
} else {
|
||||
self.amount.clone()
|
||||
};
|
||||
w::amount_text_centered(ui, &display, 64.0);
|
||||
if over {
|
||||
w::amount_text_centered_ink(ui, &display, 64.0, t.neg, t.neg);
|
||||
} else {
|
||||
w::amount_text_centered(ui, &display, 64.0);
|
||||
}
|
||||
if let Ok(grin) = display.parse::<f64>() {
|
||||
if let Some(preview) = super::pairing_preview(grin) {
|
||||
ui.add_space(6.0);
|
||||
@@ -756,6 +774,16 @@ impl SendFlow {
|
||||
});
|
||||
}
|
||||
}
|
||||
if over {
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("You don't have enough grin")
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
});
|
||||
}
|
||||
ui.add_space(16.0);
|
||||
|
||||
// Quick chips.
|
||||
@@ -815,7 +843,11 @@ impl SendFlow {
|
||||
.unwrap_or(false);
|
||||
ui.add_enabled_ui(valid, |ui| {
|
||||
if w::big_action(ui, "Review", false).clicked() {
|
||||
self.stage = Stage::Review;
|
||||
if over {
|
||||
cb.vibrate_error();
|
||||
} else {
|
||||
self.stage = Stage::Review;
|
||||
}
|
||||
}
|
||||
});
|
||||
false
|
||||
@@ -836,6 +868,18 @@ impl SendFlow {
|
||||
let amount = self.amount.clone();
|
||||
let hero_tex = tex_for(avatars, ui.ctx(), wallet, &recipient.name);
|
||||
|
||||
// Over-balance guard: every path that reaches Review (including the
|
||||
// Pay-tab / scan prefill that jumps straight here, skipping the amount
|
||||
// step) must not offer a completable send for more than the spendable
|
||||
// balance. Re-checked each frame so a mid-flow balance drop disables it.
|
||||
let spendable = wallet
|
||||
.get_data()
|
||||
.map(|d| d.info.amount_currently_spendable)
|
||||
.unwrap_or(0);
|
||||
let over = amount_from_hr_string(&amount)
|
||||
.map(|a| a > spendable)
|
||||
.unwrap_or(false);
|
||||
|
||||
w::card(ui, |ui| {
|
||||
ui.set_min_width(ui.available_width());
|
||||
ui.add_space(8.0);
|
||||
@@ -871,16 +915,34 @@ impl SendFlow {
|
||||
w::info_row(ui, "Delivery", "Encrypted nostr DM over Tor");
|
||||
ui.add_space(16.0);
|
||||
|
||||
if self.hold.ui(ui, "Hold to send") {
|
||||
self.dispatch(wallet);
|
||||
self.stage = Stage::Sending;
|
||||
if over {
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("You don't have enough grin")
|
||||
.font(FontId::new(14.0, fonts::regular()))
|
||||
.color(t.neg),
|
||||
);
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
// Greyed out while over balance; the `&& !over` also refuses the send in
|
||||
// case the hold widget ignores the disabled state.
|
||||
ui.add_enabled_ui(!over, |ui| {
|
||||
if self.hold.ui(ui, "Hold to send") && !over {
|
||||
self.dispatch(wallet);
|
||||
self.stage = Stage::Sending;
|
||||
}
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.label(
|
||||
RichText::new("Press and hold to confirm")
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
RichText::new(if over {
|
||||
"Go back and lower the amount"
|
||||
} else {
|
||||
"Press and hold to confirm"
|
||||
})
|
||||
.font(FontId::new(12.0, fonts::regular()))
|
||||
.color(t.text_mute),
|
||||
);
|
||||
});
|
||||
false
|
||||
|
||||
Reference in New Issue
Block a user