1
0
forked from GRIN/grim

Revert "goblin: remove the Import identity option from onboarding"

This reverts commit b1baa14b85.
This commit is contained in:
2ro
2026-07-07 00:42:39 -04:00
parent 811eff825c
commit 26d3d41790
9 changed files with 315 additions and 1 deletions
+5
View File
@@ -820,6 +820,8 @@ goblin:
title: "Richte dein Wallet ein"
create_new: "Neu erstellen"
restore_from_seed: "Aus Seed wiederherstellen"
import_identity: "Identität importieren"
import_identity_hint: "Nach diesem Schritt bringst du deinen vorhandenen Schlüssel mit (eine .backup-Datei oder nsec)."
name_hint: "Wallet-Name"
password_hint: "Passwort"
repeat_password_hint: "Passwort wiederholen"
@@ -864,6 +866,9 @@ goblin:
claimed_blurb: "Freunde können dich jetzt per Namen bezahlen. Alles bereit — öffne dein Wallet."
open_wallet: "Mein Wallet öffnen"
skip_for_now: "Vorerst überspringen"
import_existing: "Schon eine Goblin-Identität? Importieren"
import_title: "Identität importieren"
import_blurb: "Füge deinen nsec ein oder wähle eine .backup-Datei, um deinen vorhandenen Schlüssel und Benutzernamen zu behalten statt diesen neuen."
errors:
cant_open: "Wallet konnte nicht geöffnet werden: %{err}"
cant_create: "Wallet konnte nicht erstellt werden: %{err}"
+5
View File
@@ -820,6 +820,8 @@ goblin:
title: "Set up your wallet"
create_new: "Create new"
restore_from_seed: "Restore from seed"
import_identity: "Import identity"
import_identity_hint: "You'll bring in your existing key (a .backup file or nsec) after this step."
name_hint: "Wallet name"
password_hint: "Password"
repeat_password_hint: "Repeat password"
@@ -864,6 +866,9 @@ goblin:
claimed_blurb: "Friends can now pay you by name. You're all set — open your wallet."
open_wallet: "Open my wallet"
skip_for_now: "Skip for now"
import_existing: "Already have a Goblin identity? Import it"
import_title: "Import your identity"
import_blurb: "Paste your nsec or pick a .backup file to keep your existing key and username instead of this new one."
errors:
cant_open: "Couldn't open the wallet: %{err}"
cant_create: "Couldn't create the wallet: %{err}"
+5
View File
@@ -820,6 +820,8 @@ goblin:
title: "Configura tu billetera"
create_new: "Crear nueva"
restore_from_seed: "Restaurar desde la frase semilla"
import_identity: "Importar identidad"
import_identity_hint: "Traerás tu clave existente (un archivo .backup o nsec) después de este paso."
name_hint: "Nombre de la billetera"
password_hint: "Contraseña"
repeat_password_hint: "Repite la contraseña"
@@ -864,6 +866,9 @@ goblin:
claimed_blurb: "Tus amigos ya pueden pagarte por nombre. Todo listo — abre tu billetera."
open_wallet: "Abrir mi billetera"
skip_for_now: "Saltar por ahora"
import_existing: "¿Ya tienes una identidad de Goblin? Impórtala"
import_title: "Importa tu identidad"
import_blurb: "Pega tu nsec o elige un archivo .backup para conservar tu clave y nombre de usuario existentes en lugar de este nuevo."
errors:
cant_open: "No se pudo abrir la billetera: %{err}"
cant_create: "No se pudo crear la billetera: %{err}"
+5
View File
@@ -820,6 +820,8 @@ goblin:
title: "Configurez votre portefeuille"
create_new: "Créer un nouveau"
restore_from_seed: "Restaurer depuis la phrase"
import_identity: "Importer une identité"
import_identity_hint: "Vous importerez votre clé existante (un fichier .backup ou un nsec) après cette étape."
name_hint: "Nom du portefeuille"
password_hint: "Mot de passe"
repeat_password_hint: "Répéter le mot de passe"
@@ -864,6 +866,9 @@ goblin:
claimed_blurb: "Vos amis peuvent désormais vous payer par votre nom. Tout est prêt — ouvrez votre portefeuille."
open_wallet: "Ouvrir mon portefeuille"
skip_for_now: "Passer pour l'instant"
import_existing: "Vous avez déjà une identité Goblin ? Importez-la"
import_title: "Importer votre identité"
import_blurb: "Collez votre nsec ou choisissez un fichier .backup pour conserver votre clé et votre nom existants au lieu de ce nouveau."
errors:
cant_open: "Impossible d'ouvrir le portefeuille : %{err}"
cant_create: "Impossible de créer le portefeuille : %{err}"
+5
View File
@@ -820,6 +820,8 @@ goblin:
title: "지갑을 설정하세요"
create_new: "새로 만들기"
restore_from_seed: "복구 문구로 복원"
import_identity: "신원 가져오기"
import_identity_hint: "이 단계 이후에 기존 키(.backup 파일 또는 nsec)를 가져오게 됩니다."
name_hint: "지갑 이름"
password_hint: "비밀번호"
repeat_password_hint: "비밀번호 확인"
@@ -864,6 +866,9 @@ goblin:
claimed_blurb: "이제 친구들이 이름으로 결제할 수 있습니다. 모든 준비가 끝났습니다 — 지갑을 열어 보세요."
open_wallet: "내 지갑 열기"
skip_for_now: "지금은 건너뛰기"
import_existing: "이미 Goblin 신원이 있나요? 가져오기"
import_title: "신원 가져오기"
import_blurb: "nsec을 붙여넣거나 .backup 파일을 선택해서, 새 신원 대신 기존 키와 사용자 이름을 유지하세요."
errors:
cant_open: "지갑을 열 수 없습니다: %{err}"
cant_create: "지갑을 만들 수 없습니다: %{err}"
+5
View File
@@ -820,6 +820,8 @@ goblin:
title: "Настройте кошелёк"
create_new: "Создать новый"
restore_from_seed: "Восстановить из seed"
import_identity: "Импорт личности"
import_identity_hint: "После этого шага вы добавите свой существующий ключ (файл .backup или nsec)."
name_hint: "Имя кошелька"
password_hint: "Пароль"
repeat_password_hint: "Повторите пароль"
@@ -864,6 +866,9 @@ goblin:
claimed_blurb: "Друзья теперь могут платить вам по имени. Всё готово — откройте кошелёк."
open_wallet: "Открыть кошелёк"
skip_for_now: "Пропустить пока"
import_existing: "Уже есть личность Goblin? Импортируйте её"
import_title: "Импорт личности"
import_blurb: "Вставьте свой nsec или выберите файл .backup, чтобы сохранить существующий ключ и имя вместо нового."
errors:
cant_open: "Не удалось открыть кошелёк: %{err}"
cant_create: "Не удалось создать кошелёк: %{err}"
+5
View File
@@ -820,6 +820,8 @@ goblin:
title: "Cüzdanını kur"
create_new: "Yeni oluştur"
restore_from_seed: "Tohumdan geri yükle"
import_identity: "Kimliği içe aktar"
import_identity_hint: "Bu adımdan sonra mevcut anahtarını (bir .backup dosyası veya nsec) getireceksin."
name_hint: "Cüzdan adı"
password_hint: "Parola"
repeat_password_hint: "Parolayı tekrarla"
@@ -864,6 +866,9 @@ goblin:
claimed_blurb: "Arkadaşların artık sana adınla ödeme yapabilir. Her şey hazır — cüzdanını aç."
open_wallet: "Cüzdanımı aç"
skip_for_now: "Şimdilik atla"
import_existing: "Zaten bir Goblin kimliğin var mı? İçe aktar"
import_title: "Kimliğini içe aktar"
import_blurb: "Bu yeni anahtar yerine mevcut anahtarını ve kullanıcı adını korumak için nsec'ini yapıştır veya bir .backup dosyası seç."
errors:
cant_open: "Cüzdan açılamadı: %{err}"
cant_create: "Cüzdan oluşturulamadı: %{err}"
+5
View File
@@ -820,6 +820,8 @@ goblin:
title: "设置你的钱包"
create_new: "新建"
restore_from_seed: "从助记词恢复"
import_identity: "导入身份"
import_identity_hint: "此步骤之后,你将导入现有密钥(.backup 文件或 nsec)。"
name_hint: "钱包名称"
password_hint: "密码"
repeat_password_hint: "重复密码"
@@ -864,6 +866,9 @@ goblin:
claimed_blurb: "朋友现在可以用你的用户名向你付款。一切就绪——打开钱包吧。"
open_wallet: "打开我的钱包"
skip_for_now: "暂时跳过"
import_existing: "已有 Goblin 身份?导入它"
import_title: "导入你的身份"
import_blurb: "粘贴你的 nsec 或选择一个 .backup 文件,保留你现有的密钥和用户名,而不是这个新的。"
errors:
cant_open: "无法打开钱包:%{err}"
cant_create: "无法创建钱包:%{err}"
+275 -1
View File
@@ -28,6 +28,7 @@ use crate::gui::views::types::{ContentContainer, ModalPosition, QrScanResult};
use crate::gui::views::wallets::creation::MnemonicSetup;
use crate::gui::views::{CameraScanContent, Content, Modal, TextEdit, View};
use crate::node::Node;
use crate::nostr::NostrIdentity;
use crate::wallet::types::{ConnectionMethod, PhraseMode, PhraseSize};
use crate::wallet::{ConnectionsConfig, ExternalConnection, Wallet, WalletList};
@@ -57,6 +58,11 @@ pub struct OnboardingContent {
ext_url: String,
/// Wallet setup inputs.
restore: bool,
/// The user chose "Import identity" on the wallet step: after the wallet is
/// created the identity step opens the import panel straight away (bringing
/// over an existing key from a `.backup` or nsec), instead of the fresh
/// random key. Composes with either seed choice above it.
want_import: bool,
name: String,
pass: String,
pass2: String,
@@ -70,10 +76,34 @@ pub struct OnboardingContent {
wallet: Option<Wallet>,
/// Optional username claim state (same machinery as Settings).
claim: ClaimState,
/// Optional "import an existing identity" sub-flow, opened from the identity
/// step so a returning user can keep their old npub + username instead of the
/// freshly-generated random key.
import: Option<OnbImport>,
/// Moment the recovery phrase was copied, for the transient "Copied" check.
words_copied: Option<std::time::Instant>,
}
/// Onboarding identity-import state. Reuses the wallet password the user just
/// set, so it only needs the backup file / nsec (and the backup's own password
/// when restoring a sealed `.backup`).
#[derive(Default)]
struct OnbImport {
/// 0 = form, 1 = working, 2 = error.
stage: u8,
/// Pasted nsec or the read-in contents of a `.backup` / identity JSON file.
nsec: String,
/// Password the backup was sealed under (blank for a bare nsec, or when it
/// matches this wallet's password).
backup_password: String,
/// Last import error, shown on stage 2.
error: String,
/// A native file pick is in flight (Android resolves the path asynchronously).
picking: bool,
/// Worker result: Ok(new npub) or Err(message).
result: std::sync::Arc<std::sync::Mutex<Option<Result<String, String>>>>,
}
impl Default for OnboardingContent {
fn default() -> Self {
Self {
@@ -83,6 +113,7 @@ impl Default for OnboardingContent {
integrated: false,
ext_url: "https://grincoin.org".to_string(),
restore: false,
want_import: false,
name: "Main wallet".to_string(),
pass: String::new(),
pass2: String::new(),
@@ -91,6 +122,7 @@ impl Default for OnboardingContent {
scan_modal: None,
wallet: None,
claim: ClaimState::default(),
import: None,
words_copied: None,
}
}
@@ -388,6 +420,37 @@ impl OnboardingContent {
ui.add_space(10.0);
}
});
ui.add_space(10.0);
// First-class "Import identity" option, standing alongside Create/Restore:
// bring an existing key over (from a .backup file or a bare nsec). It
// composes with either seed choice above — the import panel itself opens
// on the identity step, once the wallet exists.
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(ui.available_width(), 44.0),
)),
|ui| {
if w::chip(
ui,
&t!("goblin.onboarding.wallet.import_identity"),
self.want_import,
)
.clicked()
{
self.want_import = !self.want_import;
}
},
);
if self.want_import {
ui.add_space(6.0);
ui.label(
RichText::new(t!("goblin.onboarding.wallet.import_identity_hint"))
.font(FontId::new(12.5, fonts::regular()))
.color(t.text_mute),
);
}
ui.add_space(14.0);
w::field_well(ui, |ui| {
@@ -838,7 +901,17 @@ impl OnboardingContent {
.nostr_service()
.map(|s| s.identity.read().nip05.is_some())
.unwrap_or(false);
if !registered {
// Came in via the wallet step's "Import identity" button: open the import
// panel straight away (offers both a .backup file and a bare nsec), rather
// than the fresh-key claim card.
if self.want_import && self.import.is_none() {
self.import = Some(OnbImport::default());
self.want_import = false;
}
if self.import.is_some() {
// Returning user is swapping the random key for their existing identity.
self.import_ui(ui, &wallet, cb);
} else if !registered {
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
ui.label(
@@ -916,6 +989,14 @@ impl OnboardingContent {
}
}
});
ui.add_space(10.0);
// Returning user? A centered, first-class "Import identity" button
// (was a left-aligned text link) restores their existing identity
// from a .backup file or a bare nsec instead of the fresh random key.
// The .backup-or-nsec choice lives behind it, in import_ui.
if w::big_action(ui, &t!("goblin.onboarding.wallet.import_identity"), true).clicked() {
self.import = Some(OnbImport::default());
}
ui.add_space(16.0);
} else {
// Claimed: show a clear success confirmation so the user knows the
@@ -967,6 +1048,199 @@ impl OnboardingContent {
None
}
/// Onboarding identity-import sub-flow: paste an nsec or pick a `.backup`
/// file to swap the freshly-generated random key for the user's existing
/// identity (keeping their npub and any claimed username). Reuses the wallet
/// password the user just set; a sealed `.backup` may carry its own password.
fn import_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) {
let t = theme::tokens();
// Poll the worker first, WITHOUT holding a borrow across the reset below.
if self.import.as_ref().map(|i| i.stage) == Some(1) {
let res = self.import.as_ref().unwrap().result.lock().unwrap().take();
if let Some(res) = res {
match res {
// Identity replaced: drop the sub-flow; the identity card and the
// claim/success state re-render from the new service next frame.
Ok(_) => {
self.import = None;
return;
}
Err(e) => {
let imp = self.import.as_mut().unwrap();
imp.error = e;
imp.stage = 2;
}
}
}
}
let wallet_pass = self.pass.clone();
let imp = self.import.as_mut().unwrap();
let mut close = false;
w::card(ui, |ui| {
ui.set_min_width(ui.available_width());
match imp.stage {
1 => {
ui.horizontal(|ui| {
View::small_loading_spinner(ui);
ui.add_space(8.0);
ui.label(
RichText::new(t!("goblin.settings.importing"))
.font(FontId::new(13.0, fonts::regular()))
.color(t.surface_text_dim),
);
});
ui.ctx().request_repaint();
}
2 => {
ui.label(
RichText::new(t!("goblin.settings.import_failed"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.neg),
);
ui.add_space(4.0);
ui.label(
RichText::new(&imp.error)
.font(FontId::new(13.0, fonts::regular()))
.color(t.surface_text_dim),
);
ui.add_space(10.0);
if w::big_action_on_card(ui, &t!("goblin.settings.close")).clicked() {
close = true;
}
}
_ => {
ui.label(
RichText::new(t!("goblin.onboarding.identity.import_title"))
.font(FontId::new(15.0, fonts::semibold()))
.color(t.surface_text),
);
ui.add_space(6.0);
ui.label(
RichText::new(t!("goblin.onboarding.identity.import_blurb"))
.font(FontId::new(12.5, fonts::regular()))
.color(t.surface_text_dim),
);
ui.add_space(10.0);
// Native ".backup file" picker. Desktop returns the path now;
// Android resolves it asynchronously (poll picked_file()).
if imp.picking {
if let Some(path) = cb.picked_file() {
imp.picking = false;
if !path.is_empty() {
match std::fs::read_to_string(&path) {
Ok(contents) => imp.nsec = contents.trim().to_string(),
Err(_) => {
imp.error =
t!("goblin.settings.backup_read_failed").to_string();
}
}
}
} else {
ui.ctx().request_repaint();
}
}
if w::big_action_on_card(ui, &t!("goblin.settings.choose_backup_file"))
.clicked()
{
imp.error.clear();
match cb.pick_file() {
Some(path) if !path.is_empty() => {
match std::fs::read_to_string(&path) {
Ok(contents) => imp.nsec = contents.trim().to_string(),
Err(_) => {
imp.error =
t!("goblin.settings.backup_read_failed").to_string();
}
}
}
// Empty string = Android async pick in flight.
Some(_) => imp.picking = true,
None => {}
}
}
ui.add_space(8.0);
w::field_well(ui, |ui| {
TextEdit::new(egui::Id::from("onb_import_nsec"))
.focus(false)
.hint_text(t!("goblin.settings.import_nsec_hint"))
.password()
.text_color(t.surface_text)
.body()
.ui(ui, &mut imp.nsec, cb);
});
ui.add_space(8.0);
w::field_well(ui, |ui| {
TextEdit::new(egui::Id::from("onb_import_bpw"))
.focus(false)
.hint_text(t!("goblin.settings.backup_password_hint"))
.password()
.text_color(t.surface_text)
.body()
.ui(ui, &mut imp.backup_password, cb);
});
if !imp.error.is_empty() {
ui.add_space(6.0);
ui.label(
RichText::new(&imp.error)
.font(FontId::new(12.5, fonts::regular()))
.color(t.neg),
);
}
ui.add_space(10.0);
let pasted = imp.nsec.trim();
// Only an nsec paste or a sealed .backup file — nothing else.
let armed =
pasted.starts_with("nsec1") || NostrIdentity::is_encrypted_backup(pasted);
ui.horizontal(|ui| {
let half = (ui.available_width() - 10.0) / 2.0;
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(half, 44.0),
)),
|ui| {
if w::big_action_on_card(ui, &t!("goblin.settings.cancel"))
.clicked()
{
close = true;
}
},
);
ui.add_space(10.0);
ui.scope_builder(
egui::UiBuilder::new().max_rect(egui::Rect::from_min_size(
ui.cursor().min,
Vec2::new(half, 44.0),
)),
|ui| {
ui.add_enabled_ui(armed, |ui| {
if w::big_action(ui, &t!("goblin.settings.import_btn"), false)
.clicked()
{
imp.stage = 1;
let slot = imp.result.clone();
let nsec = std::mem::take(&mut imp.nsec);
let bpw = std::mem::take(&mut imp.backup_password);
let bpw = if bpw.is_empty() { None } else { Some(bpw) };
let wallet = wallet.clone();
let pass = wallet_pass.clone();
std::thread::spawn(move || {
let res = wallet.import_nostr_identity(nsec, pass, bpw);
*slot.lock().unwrap() = Some(res);
});
}
});
},
);
});
}
}
});
if close {
self.import = None;
}
}
/// Recovery-phrase QR scan modal content.
fn scan_modal_ui(&mut self, ui: &mut egui::Ui, _: &Modal, cb: &dyn PlatformCallbacks) {
if let Some(content) = self.scan_modal.as_mut() {