From 5ea549abf496593c2005a4c11aa530bbecd960c4 Mon Sep 17 00:00:00 2001 From: 2ro <17595647+2ro@users.noreply.github.com> Date: Tue, 7 Jul 2026 17:18:32 -0400 Subject: [PATCH] goblin: restructure Advanced page into Advanced Nostr Settings + Danger Zone Group the Advanced settings page under two grey/red section kickers: - ADVANCED NOSTR SETTINGS: the Nostr key, and directly below it the single always-everything .backup download (one button, no checklist). Moved the .backup off the main Settings identity card so it has one home (no duplicated exposure). Caption states what the file truly holds: the current identity's key and username. - DANGER ZONE (red kicker): Delete wallet, now password-gated. The old tap-twice confirm is replaced by a wallet-password gate (get_recovery), with an inline "Download backup" button before the password field wired to the same seal action. Copy states the delete also removes every identity and urges a backup first. Add w::kicker_danger; BackupState.anchor_delete routes the seal form to whichever section opened it. New strings in all nine locales; clear the backup password from memory on leaving the page. --- locales/de.yml | 8 +- locales/en.yml | 8 +- locales/es.yml | 8 +- locales/fr.yml | 8 +- locales/ja.yml | 8 +- locales/ko.yml | 8 +- locales/ru.yml | 8 +- locales/tr.yml | 8 +- locales/zh-CN.yml | 8 +- src/gui/views/goblin/mod.rs | 139 ++++++++++++++++++++++++-------- src/gui/views/goblin/widgets.rs | 11 +++ 11 files changed, 180 insertions(+), 42 deletions(-) diff --git a/locales/de.yml b/locales/de.yml index b85bd91a..b033ad81 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -620,7 +620,7 @@ goblin: password: "Wallet-Passwort" wrong_password: "Falsches Passwort." delete: "Wallet löschen" - delete_desc: "Diese Wallet dauerhaft von diesem Gerät entfernen. Ohne deinen Seed sind Guthaben nicht wiederherstellbar." + delete_desc: "Diese Wallet und jede ihrer Identitäten dauerhaft von diesem Gerät entfernen. Sichere zuerst ein Backup. Ohne deinen Seed sind Guthaben nicht wiederherstellbar." delete_confirm: "Zum Löschen erneut tippen" manage_node: "Node-Verbindung verwalten" repair_confirm: "Ja, jetzt reparieren" @@ -632,6 +632,12 @@ goblin: copy_nsec: "nsec kopieren" show_qr: "QR anzeigen" hide_qr: "QR ausblenden" + nostr_section: "Erweiterte Nostr-Einstellungen" + backup_caption: "Enthält den Schlüssel und Benutzernamen deiner aktuellen Identität." + danger_zone: "Gefahrenzone" + delete_warning: "Dies löscht deine Wiederherstellungsphrase und jede Identität auf diesem Gerät. Alles, was du nicht gesichert hast, ist unwiederbringlich verloren." + download_backup: "Backup herunterladen" + delete_final: "Endgültig löschen" privacy: title: "Netzwerk-Privatsphäre" intro: "Goblin sendet seinen privaten Datenverkehr über Tor und verbirgt so deine IP vor dem Relay — die Verschlüsselung verbirgt den Rest, sodass ein Relay eine Zahlung nicht zu dir zurückverfolgen kann." diff --git a/locales/en.yml b/locales/en.yml index 89d14433..182ca5d8 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -620,7 +620,7 @@ goblin: password: "Wallet password" wrong_password: "Wrong password." delete: "Delete wallet" - delete_desc: "Permanently remove this wallet from this device. Without your seed, funds can't be recovered." + delete_desc: "Permanently remove this wallet and every identity on this device. Back up first. Without your seed, funds can't be recovered." delete_confirm: "Tap again to delete" manage_node: "Manage node connection" repair_confirm: "Yes, repair now" @@ -632,6 +632,12 @@ goblin: copy_nsec: "Copy nsec" show_qr: "Show QR" hide_qr: "Hide QR" + nostr_section: "Advanced nostr settings" + backup_caption: "Contains your current identity's key and username." + danger_zone: "Danger zone" + delete_warning: "This erases your recovery phrase and every identity on this device. Anything you haven't backed up is gone for good." + download_backup: "Download backup" + delete_final: "Delete permanently" privacy: title: "Network privacy" intro: "Goblin sends its private traffic through Tor, which hides your IP from the relay — encryption hides the rest, so a relay can't link a payment back to you." diff --git a/locales/es.yml b/locales/es.yml index c8afc5f7..8b4f8d24 100644 --- a/locales/es.yml +++ b/locales/es.yml @@ -620,7 +620,7 @@ goblin: password: "Contraseña de la billetera" wrong_password: "Contraseña incorrecta." delete: "Eliminar billetera" - delete_desc: "Elimina permanentemente esta billetera de este dispositivo. Sin tu frase de recuperación, los fondos no se pueden recuperar." + delete_desc: "Elimina permanentemente esta billetera y todas sus identidades de este dispositivo. Haz una copia de seguridad primero. Sin tu frase de recuperación, los fondos no se pueden recuperar." delete_confirm: "Toca de nuevo para eliminar" manage_node: "Administrar conexión del nodo" repair_confirm: "Sí, reparar ahora" @@ -632,6 +632,12 @@ goblin: copy_nsec: "Copiar nsec" show_qr: "Mostrar QR" hide_qr: "Ocultar QR" + nostr_section: "Ajustes avanzados de nostr" + backup_caption: "Contiene la clave y el nombre de usuario de tu identidad actual." + danger_zone: "Zona de peligro" + delete_warning: "Esto borra tu frase de recuperación y todas las identidades de este dispositivo. Todo lo que no hayas respaldado se perderá para siempre." + download_backup: "Descargar copia de seguridad" + delete_final: "Eliminar permanentemente" privacy: title: "Privacidad de red" intro: "Goblin envía su tráfico privado a través de Tor, lo que oculta tu IP del relé — el cifrado oculta el resto, de modo que un relé no puede vincular un pago contigo." diff --git a/locales/fr.yml b/locales/fr.yml index baf5d91b..6206a709 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -620,7 +620,7 @@ goblin: password: "Mot de passe du portefeuille" wrong_password: "Mot de passe incorrect." delete: "Supprimer le portefeuille" - delete_desc: "Supprimer définitivement ce portefeuille de cet appareil. Sans votre seed, les fonds sont irrécupérables." + delete_desc: "Supprimer définitivement ce portefeuille et toutes ses identités de cet appareil. Faites d'abord une sauvegarde. Sans votre seed, les fonds sont irrécupérables." delete_confirm: "Touchez à nouveau pour supprimer" manage_node: "Gérer la connexion au nœud" repair_confirm: "Oui, réparer maintenant" @@ -632,6 +632,12 @@ goblin: copy_nsec: "Copier le nsec" show_qr: "Afficher le QR" hide_qr: "Masquer le QR" + nostr_section: "Paramètres nostr avancés" + backup_caption: "Contient la clé et le nom d'utilisateur de votre identité actuelle." + danger_zone: "Zone de danger" + delete_warning: "Ceci efface votre phrase de récupération et toutes les identités de cet appareil. Tout ce qui n'a pas été sauvegardé est perdu définitivement." + download_backup: "Télécharger la sauvegarde" + delete_final: "Supprimer définitivement" privacy: title: "Confidentialité réseau" intro: "Goblin envoie son trafic privé via Tor, qui masque votre IP au relais — le chiffrement masque le reste, afin qu'un relais ne puisse pas relier un paiement à vous." diff --git a/locales/ja.yml b/locales/ja.yml index 141cb558..d6560299 100644 --- a/locales/ja.yml +++ b/locales/ja.yml @@ -620,7 +620,7 @@ goblin: password: "ウォレットのパスワード" wrong_password: "パスワードが間違っています。" delete: "ウォレットを削除" - delete_desc: "この端末からこのウォレットを完全に削除します。リカバリーフレーズがなければ資金は復元できません。" + delete_desc: "この端末からこのウォレットとすべてのアイデンティティを完全に削除します。先にバックアップしてください。リカバリーフレーズがなければ資金は復元できません。" delete_confirm: "もう一度タップして削除" manage_node: "ノード接続を管理" repair_confirm: "はい、今すぐ修復する" @@ -632,6 +632,12 @@ goblin: copy_nsec: "nsecをコピー" show_qr: "QRを表示" hide_qr: "QRを非表示" + nostr_section: "詳細なnostr設定" + backup_caption: "現在のアイデンティティのキーとユーザー名が含まれます。" + danger_zone: "危険ゾーン" + delete_warning: "この端末のリカバリーフレーズとすべてのアイデンティティが消去されます。バックアップしていないものは完全に失われます。" + download_backup: "バックアップをダウンロード" + delete_final: "完全に削除" privacy: title: "ネットワークプライバシー" intro: "Goblinはプライベートな通信をTor経由で送信し、リレーからIPアドレスを隠します — 暗号化がそれ以外の情報を隠すため、リレーが支払いをあなたに結び付けることはできません。" diff --git a/locales/ko.yml b/locales/ko.yml index a211aa4f..2da1d04c 100644 --- a/locales/ko.yml +++ b/locales/ko.yml @@ -620,7 +620,7 @@ goblin: password: "지갑 비밀번호" wrong_password: "비밀번호가 틀렸습니다." delete: "지갑 삭제" - delete_desc: "이 기기에서 이 지갑을 영구적으로 삭제합니다. 복구 문구가 없으면 자금을 복구할 수 없습니다." + delete_desc: "이 기기에서 이 지갑과 모든 신원을 영구적으로 삭제합니다. 먼저 백업하세요. 복구 문구가 없으면 자금을 복구할 수 없습니다." delete_confirm: "다시 눌러서 삭제" manage_node: "노드 연결 관리" repair_confirm: "예, 지금 복구" @@ -632,6 +632,12 @@ goblin: copy_nsec: "nsec 복사" show_qr: "QR 표시" hide_qr: "QR 숨기기" + nostr_section: "고급 nostr 설정" + backup_caption: "현재 신원의 키와 사용자 이름이 포함됩니다." + danger_zone: "위험 구역" + delete_warning: "이 기기의 복구 문구와 모든 신원이 삭제됩니다. 백업하지 않은 항목은 영구적으로 사라집니다." + download_backup: "백업 다운로드" + delete_final: "영구 삭제" privacy: title: "네트워크 프라이버시" intro: "Goblin은 개인 트래픽을 Tor를 통해 전송해 릴레이로부터 내 IP를 숨깁니다 — 암호화가 나머지를 숨겨서 릴레이가 결제를 나와 연결 지을 수 없습니다." diff --git a/locales/ru.yml b/locales/ru.yml index acf6d72f..e0c4fd0a 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -620,7 +620,7 @@ goblin: password: "Пароль кошелька" wrong_password: "Неверный пароль." delete: "Удалить кошелёк" - delete_desc: "Безвозвратно удалить этот кошелёк с этого устройства. Без seed-фразы средства не восстановить." + delete_desc: "Безвозвратно удалить этот кошелёк и все его личности с этого устройства. Сначала сделайте резервную копию. Без seed-фразы средства не восстановить." delete_confirm: "Нажмите ещё раз для удаления" manage_node: "Управление подключением к узлу" repair_confirm: "Да, восстановить сейчас" @@ -632,6 +632,12 @@ goblin: copy_nsec: "Копировать nsec" show_qr: "Показать QR" hide_qr: "Скрыть QR" + nostr_section: "Расширенные настройки nostr" + backup_caption: "Содержит ключ и имя пользователя вашей текущей личности." + danger_zone: "Опасная зона" + delete_warning: "Это удалит вашу фразу восстановления и все личности на этом устройстве. Всё, что не сохранено в резервной копии, будет потеряно навсегда." + download_backup: "Скачать резервную копию" + delete_final: "Удалить навсегда" privacy: title: "Сетевая приватность" intro: "Goblin отправляет приватный трафик через Tor, который скрывает ваш IP от реле — шифрование скрывает остальное, чтобы реле не могло связать платёж с вами." diff --git a/locales/tr.yml b/locales/tr.yml index 5ef523e8..a700284f 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -620,7 +620,7 @@ goblin: password: "Cüzdan parolası" wrong_password: "Yanlış parola." delete: "Cüzdanı sil" - delete_desc: "Bu cüzdanı bu cihazdan kalıcı olarak kaldır. Tohumun olmadan fonlar kurtarılamaz." + delete_desc: "Bu cüzdanı ve tüm kimliklerini bu cihazdan kalıcı olarak kaldır. Önce yedek al. Tohumun olmadan fonlar kurtarılamaz." delete_confirm: "Silmek için tekrar dokun" manage_node: "Düğüm bağlantısını yönet" repair_confirm: "Evet, şimdi onar" @@ -632,6 +632,12 @@ goblin: copy_nsec: "nsec'i kopyala" show_qr: "QR göster" hide_qr: "QR gizle" + nostr_section: "Gelişmiş nostr ayarları" + backup_caption: "Mevcut kimliğinizin anahtarını ve kullanıcı adını içerir." + danger_zone: "Tehlikeli bölge" + delete_warning: "Bu, kurtarma kelimelerinizi ve bu cihazdaki tüm kimlikleri siler. Yedeklemediğiniz her şey kalıcı olarak kaybolur." + download_backup: "Yedeği indir" + delete_final: "Kalıcı olarak sil" privacy: title: "Ağ gizliliği" intro: "Goblin özel trafiğini Tor üzerinden gönderir ve senin IP adresini relaydan gizler — şifreleme de gerisini gizler, böylece bir relay bir ödemeyi sana bağlayamaz." diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 4e6fc190..b1991932 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -620,7 +620,7 @@ goblin: password: "钱包密码" wrong_password: "密码错误。" delete: "删除钱包" - delete_desc: "从此设备永久移除该钱包。没有助记词,资金将无法找回。" + delete_desc: "从此设备永久移除该钱包及其所有身份。请先备份。没有助记词,资金将无法找回。" delete_confirm: "再次点击以删除" manage_node: "管理节点连接" repair_confirm: "是的,立即修复" @@ -632,6 +632,12 @@ goblin: copy_nsec: "复制 nsec" show_qr: "显示二维码" hide_qr: "隐藏二维码" + nostr_section: "高级 nostr 设置" + backup_caption: "包含您当前身份的密钥和用户名。" + danger_zone: "危险区域" + delete_warning: "这将删除此设备上的恢复助记词和所有身份。未备份的内容将永久丢失。" + download_backup: "下载备份" + delete_final: "永久删除" privacy: title: "网络隐私" intro: "Goblin 通过 Tor 发送其私密流量,向中继隐藏你的 IP — 加密隐藏其余部分,使中继无法将付款关联到你。" diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index 760c8518..27a59fd1 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -262,6 +262,10 @@ struct AdvancedState { confirm_repair: bool, /// Armed "really delete?" confirm. confirm_delete: bool, + /// Wallet password typed into the Danger Zone delete gate. + delete_pass: String, + /// The delete password didn't decrypt the seed — show the wrong-password line. + delete_wrong: bool, } /// Inputs and last result for the manual slatepack page (GRIM's native flow, @@ -576,6 +580,9 @@ struct BackupState { error: Option, /// The backup file was created. done: bool, + /// The form was opened from the Danger Zone delete flow, so it renders + /// inline there instead of under the Advanced nostr section. + anchor_delete: bool, } /// Inline username-claim widget state. @@ -3577,16 +3584,9 @@ impl GoblinWalletView { cb.vibrate_copy(); self.copy_flash = Some(std::time::Instant::now()); } - // One encrypted backup FILE (key + username + history sealed - // together) — replaces the old copy-nsec / copy-JSON split. - if settings_row_btn( - ui, - &t!("goblin.settings.backup_file"), - crate::gui::icons::DOWNLOAD_SIMPLE, - ) && self.backup.is_none() - { - self.backup = Some(BackupState::default()); - } + // The encrypted .backup file now lives on the Advanced page, + // under ADVANCED NOSTR SETTINGS (single home — no duplicate + // exposure here). // Nostr relays the wallet publishes/reads gift wraps on. // Sits with the identity rows because relays are a nostr // concern; opens the relay editor (handled below). @@ -3649,17 +3649,6 @@ impl GoblinWalletView { self.copy_flash = None; } } - ui.add_space(6.0); - ui.label( - RichText::new(t!("goblin.settings.backup_note")) - .font(FontId::new(12.0, fonts::regular())) - .color(t.text_mute), - ); - if self.backup.is_some() { - ui.add_space(8.0); - self.backup_ui(ui, wallet, cb); - } - ui.add_space(16.0); let mut open_node = false; let mut open_slatepack = false; @@ -4386,6 +4375,8 @@ impl GoblinWalletView { let t = theme::tokens(); if self.sub_header(ui, &t!("goblin.advanced.title")) { self.advanced = AdvancedState::default(); + // Don't leave a half-entered backup password sitting in memory. + self.backup = None; self.settings_page = SettingsPage::Main; return; } @@ -4400,12 +4391,14 @@ impl GoblinWalletView { let mut open_node = false; let mut open_integrated = false; { - let adv = &mut self.advanced; ScrollArea::vertical() .id_salt("goblin_advanced_scroll") .auto_shrink([false; 2]) .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden) .show(ui, |ui| { + // Borrow ends (NLL) at the last `adv.` use in the Nostr-key card; + // the .backup + Danger Zone sections below then use `self`. + let adv = &mut self.advanced; ui.label( RichText::new(t!("goblin.advanced.intro")) .font(FontId::new(14.0, fonts::regular())) @@ -4590,6 +4583,10 @@ impl GoblinWalletView { }); ui.add_space(12.0); + // ── ADVANCED NOSTR SETTINGS ────────────────────────────── + // The Nostr key, and directly below it the .backup download. + w::kicker(ui, &t!("goblin.advanced.nostr_section")); + ui.add_space(8.0); // Nostr key (nsec). Password-gated reveal, then Copy + a QR // so it can be carried into a nostr app's private-key login // (e.g. magick.market) without retyping. Same gate as the @@ -4674,23 +4671,98 @@ impl GoblinWalletView { }); ui.add_space(12.0); - // Delete. + // .backup download — directly under the Nostr key, the second + // half of the ADVANCED NOSTR SETTINGS group. One button, no + // checklist: it seals your CURRENT identity (key + username) + // into an encrypted .backup file. (`adv` is no longer used past + // here, so `self` is free again.) + w::card(ui, |ui| { + ui.set_min_width(ui.available_width()); + advanced_head(ui, &t!("goblin.settings.backup_file_title"), t.surface_text); + advanced_desc(ui, &t!("goblin.advanced.backup_caption")); + ui.add_space(10.0); + if w::big_action_on_card(ui, &t!("goblin.settings.backup_file")).clicked() + && self.backup.is_none() + { + self.backup = Some(BackupState::default()); + } + }); + // The password/seal form, anchored here unless it was opened from + // the Danger Zone delete flow below. + if self.backup.as_ref().is_some_and(|b| !b.anchor_delete) { + ui.add_space(8.0); + self.backup_ui(ui, wallet, cb); + } + ui.add_space(16.0); + + // ── DANGER ZONE ────────────────────────────────────────── + // Delete the wallet — password-gated, with a back-up prompt. + w::kicker_danger(ui, &t!("goblin.advanced.danger_zone")); + ui.add_space(8.0); w::card(ui, |ui| { ui.set_min_width(ui.available_width()); advanced_head(ui, &t!("goblin.advanced.delete"), t.neg); advanced_desc(ui, &t!("goblin.advanced.delete_desc")); ui.add_space(10.0); - if adv.confirm_delete { - if w::big_action_on_card_ink( - ui, - &t!("goblin.advanced.delete_confirm"), - t.neg, - ) - .clicked() + if self.advanced.confirm_delete { + ui.label( + RichText::new(t!("goblin.advanced.delete_warning")) + .font(FontId::new(13.0, fonts::regular())) + .color(t.neg), + ); + ui.add_space(10.0); + // Back up BEFORE the password field (spec) — the same seal + // action, but anchored to this flow so its form renders + // here, not up in the nostr section. + if w::big_action_on_card(ui, &t!("goblin.advanced.download_backup")) + .clicked() && self.backup.is_none() { - wallet.delete_wallet(); - leave = true; + self.backup = Some(BackupState { + anchor_delete: true, + ..Default::default() + }); } + if self.backup.as_ref().is_some_and(|b| b.anchor_delete) { + self.backup_ui(ui, wallet, cb); + ui.add_space(10.0); + } + w::field_well(ui, |ui| { + TextEdit::new(egui::Id::from("advanced_delete_pass")) + .focus(false) + .hint_text(t!("goblin.advanced.password")) + .password() + .text_color(t.surface_text) + .body() + .ui(ui, &mut self.advanced.delete_pass, cb); + }); + if self.advanced.delete_wrong { + ui.add_space(6.0); + ui.label( + RichText::new(t!("goblin.advanced.wrong_password")) + .font(FontId::new(13.0, fonts::medium())) + .color(t.neg), + ); + } + ui.add_space(10.0); + let adv = &mut self.advanced; + ui.add_enabled_ui(!adv.delete_pass.is_empty(), |ui| { + if w::big_action_on_card_ink( + ui, + &t!("goblin.advanced.delete_final"), + t.neg, + ) + .clicked() + { + // Wallet-password gate: get_recovery only returns Ok + // when the password decrypts the seed. + if wallet.get_recovery(adv.delete_pass.clone()).is_ok() { + wallet.delete_wallet(); + leave = true; + } else { + adv.delete_wrong = true; + } + } + }); } else if w::big_action_on_card_ink( ui, &t!("goblin.advanced.delete"), @@ -4698,7 +4770,7 @@ impl GoblinWalletView { ) .clicked() { - adv.confirm_delete = true; + self.advanced.confirm_delete = true; } }); ui.add_space(20.0); @@ -4706,6 +4778,7 @@ impl GoblinWalletView { } if leave { self.advanced = AdvancedState::default(); + self.backup = None; self.settings_page = SettingsPage::Main; } if open_node { diff --git a/src/gui/views/goblin/widgets.rs b/src/gui/views/goblin/widgets.rs index e314fdd2..bb50ba16 100644 --- a/src/gui/views/goblin/widgets.rs +++ b/src/gui/views/goblin/widgets.rs @@ -203,6 +203,17 @@ pub fn kicker(ui: &mut Ui, text: &str) { ); } +/// A kicker rendered in the danger colour — marks a destructive section +/// (e.g. the Advanced page's Danger Zone). +pub fn kicker_danger(ui: &mut Ui, text: &str) { + let t = theme::tokens(); + ui.label( + RichText::new(text.to_uppercase()) + .font(fonts::kicker()) + .color(t.neg), + ); +} + /// A Cash-App-style on/off switch. Yellow (brand accent) when on, neutral track /// when off. Returns the response — the caller flips the bound state on click. pub fn toggle(ui: &mut Ui, on: bool) -> Response {