Build 9: QR scan, camera fixes, first-run onboarding, sweep fixes

Camera/QR (validated live with a v4l2loopback virtual camera):
- decode non-MJPEG frames on Linux (raw YUYV was undecodable: eternal
  spinner), enumerate devices off the UI thread without unwrap, open
  cameras by their real device index (list position broke whenever
  /dev/video0 was absent), show "No camera found" after 5s
- scan-to-pay: QR button in the send recipient search row and the
  mobile home header; in-surface camera panel feeding recipient
  resolution; only text payloads accepted (seeds/slatepacks refused)
- receive QR was unscannable: full cells (0.5px gaps fragmented finder
  patterns at 4.5px cells), always ink-on-white plate (inverted codes
  fail many scanners), center mark 26% -> 19% (zbar chokes above);
  rqrr+zbar both decode the live card now; unit tests cover the exact
  widget geometry

First-run onboarding (replaces the stock empty state only; the wallet
list, add-wallet modal and GRIM creation flow stay for later wallets):
- intro -> node choice (Private integrated / Instant external with
  URL) -> create or restore (wraps MnemonicSetup; word grid, paste,
  SeedQR scan) -> optional @username claim with prominent skip
- fonts bind at creation context so frame one can use Geist families

Sweep fixes (22 confirmed findings, the simple ones):
- yellow theme: active sidebar nav was dark-on-dark (P1)
- window frame ring painted with theme bg: transparent clear color
  rendered as a black band without a compositor (P2)
- import/rotate identity inputs get visible field wells (P2)
- receive buttons: verb labels + transient "Copied" feedback
- review hero card fills the column; fee row copy; pay-tab stale and
  duplicated hints; unlock modal copy; node card subtitle truncation
- build number stays current (.git/logs/HEAD rerun trigger)

Backup restore: import accepts the export-time password and re-encrypts
under the current wallet password; cross-device restores work.

31 lib tests green. Mainnet-validated: both flows restored real
wallets; 0.1 grin sent B->A through NIP-17 over Tor with async
open/close ping-pong; tx 71fbfce4f591 posted on-chain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-06-11 04:07:53 -04:00
parent 908df117e6
commit d8cf06b577
16 changed files with 1412 additions and 125 deletions
+36 -20
View File
@@ -16,8 +16,8 @@ use eframe::epaint::RectShape;
use egui::os::OperatingSystem;
use egui::scroll_area::ScrollBarVisibility;
use egui::{
Align, CornerRadius, CursorIcon, Id, Layout, Margin, OpenUrl, RichText, ScrollArea, Sense,
StrokeKind, UiBuilder,
Align, CornerRadius, CursorIcon, Id, Layout, Margin, OpenUrl, ScrollArea, Sense, StrokeKind,
UiBuilder,
};
use egui_async::Bind;
use std::time::Duration;
@@ -25,10 +25,11 @@ use std::time::Duration;
use crate::AppConfig;
use crate::gui::Colors;
use crate::gui::icons::{
ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CLOUD_ARROW_DOWN, COMPUTER_TOWER, FOLDER_PLUS, GEAR,
GEAR_FINE, GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE,
ARROW_LEFT, BOOKMARKS, CALENDAR_CHECK, CLOUD_ARROW_DOWN, COMPUTER_TOWER, GEAR, GEAR_FINE,
GLOBE, GLOBE_SIMPLE, LOCK_KEY, NOTEPAD, PLUS, SIDEBAR_SIMPLE, SUITCASE,
};
use crate::gui::platform::PlatformCallbacks;
use crate::gui::views::goblin::onboarding::OnboardingContent;
use crate::gui::views::settings::SettingsContent;
use crate::gui::views::types::{
ContentContainer, LinePosition, ModalPosition, TitleContentType, TitleType,
@@ -64,6 +65,8 @@ pub struct WalletsContent {
wallet_content: WalletContent,
/// Wallet creation content.
creation_content: Option<WalletCreationContent>,
/// First-run onboarding content, shown while no wallets exist.
onboarding: Option<OnboardingContent>,
/// Settings content.
settings_content: Option<SettingsContent>,
@@ -95,6 +98,7 @@ impl Default for WalletsContent {
wallet_settings_content: WalletSettingsModal::new(ConnectionMethod::Integrated),
wallet_content: WalletContent::default(),
creation_content: None,
onboarding: None,
settings_content: None,
check_update: Bind::new(false),
update_info: (false, None),
@@ -194,16 +198,21 @@ impl ContentContainer for WalletsContent {
let showing_settings = self.showing_settings();
let creating_wallet = self.creating_wallet();
let showing_wallet = self.showing_wallet() && !creating_wallet && !showing_settings;
// First-run onboarding owns the surface while no wallets exist (and
// keeps it through its identity step, when the new wallet is already
// open but not yet selected).
let onboarding_active = !showing_wallet && self.onboarding_active();
let dual_panel = is_dual_panel_mode(ui);
let content_width = ui.available_width();
let list_hidden = showing_settings
|| creating_wallet
|| onboarding_active
|| self.wallets.list().is_empty()
|| (showing_wallet && (!dual_panel || !AppConfig::show_wallets_at_dual_panel()));
// Show title panel, except over the full-bleed Goblin wallet surface
// (it has its own header/navigation; lock + node info live there).
if !showing_wallet {
// and onboarding (they have their own header/navigation).
if !showing_wallet && !onboarding_active {
self.title_ui(ui, dual_panel, cb);
}
@@ -300,6 +309,8 @@ impl ContentContainer for WalletsContent {
},
fill: if self.showing_settings() {
Colors::fill_lite()
} else if onboarding_active {
Colors::fill()
} else {
Colors::fill_deep()
},
@@ -337,20 +348,17 @@ impl ContentContainer for WalletsContent {
self.select_wallet(w, None, cb);
}
}
} else if self.wallets.list().is_empty() {
View::center_content(ui, 350.0 + View::get_bottom_inset(), |ui| {
View::app_logo_name_version(ui);
ui.add_space(4.0);
let text = t!("wallets.create_desc");
ui.label(RichText::new(text).size(16.0).color(Colors::gray()));
ui.add_space(8.0);
// Show wallet creation button.
let add_text = format!("{} {}", FOLDER_PLUS, t!("wallets.add"));
View::button(ui, add_text, Colors::white_or_black(false), || {
self.show_add_wallet_modal();
});
});
} else if onboarding_active {
// First-run onboarding replaces the stock empty state;
// later wallets still use the list's add-wallet flow.
if self.onboarding.is_none() {
self.onboarding = Some(OnboardingContent::default());
}
let ob = self.onboarding.as_mut().unwrap();
if let Some(w) = ob.ui(ui, &mut self.wallets, cb) {
self.onboarding = None;
self.select_wallet(&w, None, cb);
}
} else {
return;
}
@@ -402,6 +410,14 @@ impl WalletsContent {
self.creation_content.is_some()
}
/// Check if first-run onboarding owns the surface (no wallets yet, or
/// the onboarding identity step is still finishing).
pub fn onboarding_active(&self) -> bool {
!self.showing_settings()
&& !self.creating_wallet()
&& (self.onboarding.is_some() || self.wallets.list().is_empty())
}
/// Check if application settings are showing.
pub fn showing_settings(&self) -> bool {
self.settings_content.is_some()
+1 -1
View File
@@ -133,7 +133,7 @@ impl MnemonicSetup {
}
/// Draw grid of words for mnemonic phrase.
fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) {
pub(crate) fn word_list_ui(&mut self, ui: &mut egui::Ui, edit: bool) {
ui.add_space(6.0);
ui.scope(|ui| {
// Setup spacing between columns.
+1 -1
View File
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
mod creation;
pub(crate) mod creation;
pub mod modals;
mod content;