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
+29 -8
View File
@@ -540,25 +540,46 @@ impl Wallet {
/// or an exported identity-backup JSON (which restores the username and
/// rotation history too). The current identity is overwritten; its npub
/// is kept in `prev_npubs` for reference. Blocking-safe (no network).
pub fn import_nostr_identity(&self, input: String, password: String) -> Result<String, String> {
pub fn import_nostr_identity(
&self,
input: String,
password: String,
backup_password: Option<String>,
) -> Result<String, String> {
let svc = self
.nostr_service()
.ok_or_else(|| "nostr is not running".to_string())?;
// Prove the password against the current identity before replacing.
// Prove THIS wallet's password against the current identity first.
let old = svc.identity.read().clone();
old.unlock(&password)
.map_err(|_| "Wrong password".to_string())?;
let input = input.trim();
let (mut new_identity, new_keys) = if input.starts_with('{') {
// Identity backup JSON: the ncryptsec inside stays as exported;
// it must decrypt with the provided (= export-time) password.
let ident: NostrIdentity =
// Identity backup JSON: decrypt with the password it was exported
// under (may differ on a new device), then RE-ENCRYPT under this
// wallet's password so future unlocks use the current one.
let backup: NostrIdentity =
serde_json::from_str(input).map_err(|_| "Invalid identity backup".to_string())?;
let keys = ident.unlock(&password).map_err(|_| {
"Backup password mismatch (use the wallet password from when \
it was exported)"
let bpw = backup_password
.as_deref()
.filter(|s| !s.is_empty())
.unwrap_or(&password);
let keys = backup.unlock(bpw).map_err(|_| {
"Couldn't decrypt the backup — if it was exported on another \
device, enter that wallet's password in the backup-password \
field"
.to_string()
})?;
let mut ident = NostrIdentity::from_unlocked_keys(
&keys,
&password,
backup.source,
backup.derivation_account,
)
.map_err(|e| format!("re-encryption failed: {e}"))?;
ident.nip05 = backup.nip05.clone();
ident.anonymous = backup.anonymous;
ident.prev_npubs = backup.prev_npubs.clone();
(ident, keys)
} else {
NostrIdentity::create_imported(input, &password)