goblin: Authorize Sessions GUI (trust grant, money prompt, Trusted Sites) + 6 locales
- Trust-grant modal: password-gated hold-to-confirm 'Trust <domain>?' showing identity + truncated npub, the granted low-tier categories (human labels, caution lines for unknown/login-excluded kinds), and the fixed money-asks line. Folds login in; creates the session on login-POST success. - Money-tier per-action modal: the v1-authorize-shaped password + hold-to-confirm prompt raised over the channel for a sign or a pay-committing encrypt; declines on cancel/timeout. - Trusted Sites settings page: active sessions, what each signs silently, time left, pause/resume, and one-tap end (revocation). Row + count in Settings. - Dispatch + per-frame router for trust/money; PENDING_TRUST wired. - goblin.trust.*, goblin.money.*, goblin.trusted_sites.*, settings.trusted_sites in all six locales; drift test green.
This commit is contained in:
@@ -476,6 +476,7 @@ goblin:
|
||||
node_advanced: "Erweitert"
|
||||
slatepacks: "Slatepacks"
|
||||
slatepacks_value: "Manuelle Transaktion"
|
||||
trusted_sites: "Vertrauenswürdige Seiten"
|
||||
lock_wallet: "Wallet sperren"
|
||||
switch_wallet: "Wallet wechseln"
|
||||
advanced: "Erweitert"
|
||||
@@ -716,6 +717,44 @@ goblin:
|
||||
by_author: "von %{id}"
|
||||
reacts_to: "reagiert auf %{id}"
|
||||
article_title: "Titel: %{title}"
|
||||
trust:
|
||||
title: "Vertrauen"
|
||||
headline: "%{domain} vertrauen?"
|
||||
identity: "Anmeldung als"
|
||||
grant_intro: "%{domain} kann diese für dich signieren, ohne erneut zu fragen, bis du dich abmeldest oder die Sitzung beendest:"
|
||||
cat_social: "Beiträge und Reaktionen"
|
||||
cat_dm: "Direktnachrichten"
|
||||
cat_market: "Angebote"
|
||||
cat_identity: "Profil und Listen"
|
||||
cat_delete: "Löschungen"
|
||||
cat_http: "Uploads und HTTP-Authentifizierung"
|
||||
cat_unknown: "Anderer Ereignistyp (Art %{n})"
|
||||
login_excluded: "Anmeldung wird einer Seite nie gewährt."
|
||||
money_line: "Alles, was Geld ausgibt oder empfängt, fragt weiterhin jedes Mal nach deinem Passwort. Kauf-, Verkaufs- und Zahlungsbestätigungen werden nie stillschweigend signiert."
|
||||
duration: "Dies gilt nur für diese Sitzung. Du kannst sie jederzeit unter Einstellungen, Vertrauenswürdige Seiten beenden. Dein Schlüssel verlässt nie deine Wallet."
|
||||
pass_prompt: "Gib dein Wallet-Passwort ein, um dieser Seite zu vertrauen."
|
||||
confirm_hold: "Halten, um für diese Sitzung zu vertrauen"
|
||||
sent: "%{domain} wird für diese Sitzung vertraut"
|
||||
failed: "%{domain} konnte nicht erreicht werden. Die Sitzung wurde nicht gestartet."
|
||||
notice_volume: "Eine vertrauenswürdige Seite signiert sehr viel."
|
||||
money:
|
||||
title: "Bestätigen"
|
||||
headline: "In %{domain} bestätigen?"
|
||||
identity: "Signiert als %{id}"
|
||||
explain: "Dies bewegt oder bindet Werte, daher wird immer gefragt. Mit dem Genehmigen wird diese eine Aktion signiert."
|
||||
pass_prompt: "Gib dein Wallet-Passwort zum Bestätigen ein."
|
||||
confirm_hold: "Halten zum Bestätigen"
|
||||
encrypt_desc: "Eine verschlüsselte Bestellnachricht, die eine Zahlung zusichert."
|
||||
trusted_sites:
|
||||
title: "Vertrauenswürdige Seiten"
|
||||
intro: "Seiten, denen du in dieser Sitzung vertraut hast, können risikoarme Aktionen ohne Nachfrage signieren. Bei Geld wird immer gefragt. Beende jede Sitzung hier."
|
||||
empty: "Keine vertrauenswürdigen Seiten. Vertraue einer Seite bei der Anmeldung, um ihr eine Signatur-Sitzung zu geben."
|
||||
none: "nichts"
|
||||
can_sign: "Kann still signieren: %{cats}"
|
||||
time_left: "Endet in %{mins} Min."
|
||||
paused: "Pausiert, signiert sehr viel"
|
||||
resume: "Fortsetzen"
|
||||
end: "Sitzung beenden"
|
||||
identities:
|
||||
title: "Identitäten"
|
||||
switch_hint: "Identität wechseln"
|
||||
|
||||
@@ -476,6 +476,7 @@ goblin:
|
||||
node_advanced: "Advanced"
|
||||
slatepacks: "Slatepacks"
|
||||
slatepacks_value: "Manual transaction"
|
||||
trusted_sites: "Trusted Sites"
|
||||
lock_wallet: "Lock wallet"
|
||||
switch_wallet: "Switch wallet"
|
||||
advanced: "Advanced"
|
||||
@@ -716,6 +717,44 @@ goblin:
|
||||
by_author: "by %{id}"
|
||||
reacts_to: "reacts to %{id}"
|
||||
article_title: "Title: %{title}"
|
||||
trust:
|
||||
title: "Trust"
|
||||
headline: "Trust %{domain}?"
|
||||
identity: "Signing in as"
|
||||
grant_intro: "%{domain} will be able to sign these for you, without asking again, until you sign out or end the session:"
|
||||
cat_social: "Posts and reactions"
|
||||
cat_dm: "Direct messages"
|
||||
cat_market: "Listings"
|
||||
cat_identity: "Profile and lists"
|
||||
cat_delete: "Deletes"
|
||||
cat_http: "Uploads and HTTP auth"
|
||||
cat_unknown: "Other event type (kind %{n})"
|
||||
login_excluded: "Login is never granted to a site."
|
||||
money_line: "Anything that spends or receives money will still ask for your password, every time. Buying, selling, and payment confirmations are never signed silently."
|
||||
duration: "This lasts for this session only. You can end it any time from Settings, Trusted Sites. Your key never leaves your wallet."
|
||||
pass_prompt: "Enter your wallet password to trust this site."
|
||||
confirm_hold: "Hold to trust for this session"
|
||||
sent: "Trusting %{domain} for this session"
|
||||
failed: "Could not reach %{domain}. The session was not started."
|
||||
notice_volume: "A trusted site is signing a lot."
|
||||
money:
|
||||
title: "Confirm"
|
||||
headline: "Confirm in %{domain}?"
|
||||
identity: "Signing as %{id}"
|
||||
explain: "This moves or commits value, so it always asks. Approving signs this one action."
|
||||
pass_prompt: "Enter your wallet password to confirm."
|
||||
confirm_hold: "Hold to confirm"
|
||||
encrypt_desc: "An encrypted order message that commits to a payment."
|
||||
trusted_sites:
|
||||
title: "Trusted Sites"
|
||||
intro: "Sites you trusted this session can sign low-risk actions without asking. Money always asks. End any session here."
|
||||
empty: "No trusted sites. Trust a site when you sign in to give it a signing session."
|
||||
none: "nothing"
|
||||
can_sign: "Can sign silently: %{cats}"
|
||||
time_left: "Ends in %{mins} min"
|
||||
paused: "Paused, signing a lot"
|
||||
resume: "Resume"
|
||||
end: "End session"
|
||||
identities:
|
||||
title: "Identities"
|
||||
switch_hint: "Switch identity"
|
||||
|
||||
@@ -476,6 +476,7 @@ goblin:
|
||||
node_advanced: "Avancé"
|
||||
slatepacks: "Slatepacks"
|
||||
slatepacks_value: "Transaction manuelle"
|
||||
trusted_sites: "Sites de confiance"
|
||||
lock_wallet: "Verrouiller le portefeuille"
|
||||
switch_wallet: "Changer de portefeuille"
|
||||
advanced: "Avancé"
|
||||
@@ -716,6 +717,44 @@ goblin:
|
||||
by_author: "par %{id}"
|
||||
reacts_to: "réagit à %{id}"
|
||||
article_title: "Titre : %{title}"
|
||||
trust:
|
||||
title: "Confiance"
|
||||
headline: "Faire confiance à %{domain} ?"
|
||||
identity: "Connexion en tant que"
|
||||
grant_intro: "%{domain} pourra les signer pour vous, sans redemander, jusqu'à ce que vous vous déconnectiez ou terminiez la session :"
|
||||
cat_social: "Publications et réactions"
|
||||
cat_dm: "Messages privés"
|
||||
cat_market: "Annonces"
|
||||
cat_identity: "Profil et listes"
|
||||
cat_delete: "Suppressions"
|
||||
cat_http: "Envois et authentification HTTP"
|
||||
cat_unknown: "Autre type d'événement (type %{n})"
|
||||
login_excluded: "La connexion n'est jamais accordée à un site."
|
||||
money_line: "Tout ce qui dépense ou reçoit de l'argent demandera toujours votre mot de passe, à chaque fois. Les achats, ventes et confirmations de paiement ne sont jamais signés silencieusement."
|
||||
duration: "Cela ne dure que le temps de cette session. Vous pouvez y mettre fin à tout moment depuis Réglages, Sites de confiance. Votre clé ne quitte jamais votre portefeuille."
|
||||
pass_prompt: "Saisissez le mot de passe de votre portefeuille pour faire confiance à ce site."
|
||||
confirm_hold: "Maintenez pour faire confiance pour cette session"
|
||||
sent: "Confiance accordée à %{domain} pour cette session"
|
||||
failed: "Impossible de joindre %{domain}. La session n'a pas démarré."
|
||||
notice_volume: "Un site de confiance signe beaucoup."
|
||||
money:
|
||||
title: "Confirmer"
|
||||
headline: "Confirmer dans %{domain} ?"
|
||||
identity: "Signature en tant que %{id}"
|
||||
explain: "Ceci déplace ou engage une valeur, donc une confirmation est toujours demandée. Approuver signe cette seule action."
|
||||
pass_prompt: "Saisissez le mot de passe de votre portefeuille pour confirmer."
|
||||
confirm_hold: "Maintenez pour confirmer"
|
||||
encrypt_desc: "Un message de commande chiffré qui engage un paiement."
|
||||
trusted_sites:
|
||||
title: "Sites de confiance"
|
||||
intro: "Les sites auxquels vous avez fait confiance cette session peuvent signer des actions à faible risque sans demander. L'argent demande toujours. Terminez toute session ici."
|
||||
empty: "Aucun site de confiance. Faites confiance à un site lors de la connexion pour lui donner une session de signature."
|
||||
none: "rien"
|
||||
can_sign: "Peut signer en silence : %{cats}"
|
||||
time_left: "Se termine dans %{mins} min"
|
||||
paused: "En pause, signe beaucoup"
|
||||
resume: "Reprendre"
|
||||
end: "Terminer la session"
|
||||
identities:
|
||||
title: "Identités"
|
||||
switch_hint: "Changer d'identité"
|
||||
|
||||
@@ -476,6 +476,7 @@ goblin:
|
||||
node_advanced: "Дополнительно"
|
||||
slatepacks: "Slatepacks"
|
||||
slatepacks_value: "Ручная транзакция"
|
||||
trusted_sites: "Доверенные сайты"
|
||||
lock_wallet: "Заблокировать кошелёк"
|
||||
switch_wallet: "Сменить кошелёк"
|
||||
advanced: "Дополнительно"
|
||||
@@ -716,6 +717,44 @@ goblin:
|
||||
by_author: "автор %{id}"
|
||||
reacts_to: "реакция на %{id}"
|
||||
article_title: "Заголовок: %{title}"
|
||||
trust:
|
||||
title: "Доверие"
|
||||
headline: "Доверять %{domain}?"
|
||||
identity: "Вход как"
|
||||
grant_intro: "%{domain} сможет подписывать это за вас без повторных запросов, пока вы не выйдете или не завершите сессию:"
|
||||
cat_social: "Публикации и реакции"
|
||||
cat_dm: "Личные сообщения"
|
||||
cat_market: "Объявления"
|
||||
cat_identity: "Профиль и списки"
|
||||
cat_delete: "Удаления"
|
||||
cat_http: "Загрузки и HTTP-аутентификация"
|
||||
cat_unknown: "Другой тип события (вид %{n})"
|
||||
login_excluded: "Вход в систему сайту никогда не предоставляется."
|
||||
money_line: "Всё, что тратит или получает деньги, каждый раз будет запрашивать ваш пароль. Покупки, продажи и подтверждения платежей никогда не подписываются молча."
|
||||
duration: "Это действует только для текущей сессии. Вы можете завершить её в любой момент в Настройках, Доверенные сайты. Ваш ключ никогда не покидает кошелёк."
|
||||
pass_prompt: "Введите пароль кошелька, чтобы доверять этому сайту."
|
||||
confirm_hold: "Удерживайте, чтобы доверять на эту сессию"
|
||||
sent: "Доверие %{domain} на эту сессию"
|
||||
failed: "Не удалось связаться с %{domain}. Сессия не начата."
|
||||
notice_volume: "Доверенный сайт подписывает очень много."
|
||||
money:
|
||||
title: "Подтвердить"
|
||||
headline: "Подтвердить в %{domain}?"
|
||||
identity: "Подпись как %{id}"
|
||||
explain: "Это перемещает или фиксирует ценность, поэтому запрос выводится всегда. Одобрение подписывает это одно действие."
|
||||
pass_prompt: "Введите пароль кошелька для подтверждения."
|
||||
confirm_hold: "Удерживайте для подтверждения"
|
||||
encrypt_desc: "Зашифрованное сообщение заказа, фиксирующее платёж."
|
||||
trusted_sites:
|
||||
title: "Доверенные сайты"
|
||||
intro: "Сайты, которым вы доверились в этой сессии, могут подписывать действия с низким риском без запроса. Деньги всегда запрашивают подтверждение. Завершите любую сессию здесь."
|
||||
empty: "Нет доверенных сайтов. Доверьте сайту при входе, чтобы дать ему сессию подписи."
|
||||
none: "ничего"
|
||||
can_sign: "Может подписывать без запроса: %{cats}"
|
||||
time_left: "Завершится через %{mins} мин"
|
||||
paused: "Приостановлено, подписывает много"
|
||||
resume: "Возобновить"
|
||||
end: "Завершить сессию"
|
||||
identities:
|
||||
title: "Личности"
|
||||
switch_hint: "Сменить личность"
|
||||
|
||||
@@ -476,6 +476,7 @@ goblin:
|
||||
node_advanced: "Gelişmiş"
|
||||
slatepacks: "Slatepackler"
|
||||
slatepacks_value: "Manuel işlem"
|
||||
trusted_sites: "Güvenilen Siteler"
|
||||
lock_wallet: "Cüzdanı kilitle"
|
||||
switch_wallet: "Cüzdan değiştir"
|
||||
advanced: "Gelişmiş"
|
||||
@@ -716,6 +717,44 @@ goblin:
|
||||
by_author: "yazan %{id}"
|
||||
reacts_to: "%{id} tepki veriliyor"
|
||||
article_title: "Başlık: %{title}"
|
||||
trust:
|
||||
title: "Güven"
|
||||
headline: "%{domain} sitesine güvenilsin mi?"
|
||||
identity: "Giriş yapan"
|
||||
grant_intro: "%{domain}, siz çıkış yapana veya oturumu sonlandırana kadar bunları tekrar sormadan sizin için imzalayabilecek:"
|
||||
cat_social: "Gönderiler ve tepkiler"
|
||||
cat_dm: "Doğrudan mesajlar"
|
||||
cat_market: "İlanlar"
|
||||
cat_identity: "Profil ve listeler"
|
||||
cat_delete: "Silmeler"
|
||||
cat_http: "Yüklemeler ve HTTP kimlik doğrulama"
|
||||
cat_unknown: "Diğer olay türü (tür %{n})"
|
||||
login_excluded: "Bir siteye asla giriş izni verilmez."
|
||||
money_line: "Para harcayan veya alan her şey, her seferinde parolanızı isteyecek. Alım, satım ve ödeme onayları asla sessizce imzalanmaz."
|
||||
duration: "Bu yalnızca bu oturum için geçerlidir. İstediğiniz zaman Ayarlar, Güvenilen Siteler'den sonlandırabilirsiniz. Anahtarınız cüzdanınızdan asla çıkmaz."
|
||||
pass_prompt: "Bu siteye güvenmek için cüzdan parolanızı girin."
|
||||
confirm_hold: "Bu oturum için güvenmek üzere basılı tutun"
|
||||
sent: "%{domain} bu oturum için güveniliyor"
|
||||
failed: "%{domain} sitesine ulaşılamadı. Oturum başlatılmadı."
|
||||
notice_volume: "Güvenilen bir site çok fazla imzalıyor."
|
||||
money:
|
||||
title: "Onayla"
|
||||
headline: "%{domain} içinde onaylansın mı?"
|
||||
identity: "%{id} olarak imzalanıyor"
|
||||
explain: "Bu, değeri taşır veya bağlar, bu yüzden her zaman sorar. Onaylamak bu tek eylemi imzalar."
|
||||
pass_prompt: "Onaylamak için cüzdan parolanızı girin."
|
||||
confirm_hold: "Onaylamak için basılı tutun"
|
||||
encrypt_desc: "Bir ödemeyi bağlayan şifreli sipariş mesajı."
|
||||
trusted_sites:
|
||||
title: "Güvenilen Siteler"
|
||||
intro: "Bu oturumda güvendiğiniz siteler, düşük riskli eylemleri sormadan imzalayabilir. Para her zaman sorar. Herhangi bir oturumu buradan sonlandırın."
|
||||
empty: "Güvenilen site yok. Ona bir imza oturumu vermek için giriş yaparken bir siteye güvenin."
|
||||
none: "hiçbir şey"
|
||||
can_sign: "Sessizce imzalayabilir: %{cats}"
|
||||
time_left: "%{mins} dk içinde sona erer"
|
||||
paused: "Duraklatıldı, çok imzalıyor"
|
||||
resume: "Devam et"
|
||||
end: "Oturumu sonlandır"
|
||||
identities:
|
||||
title: "Kimlikler"
|
||||
switch_hint: "Kimlik değiştir"
|
||||
|
||||
@@ -476,6 +476,7 @@ goblin:
|
||||
node_advanced: "高级"
|
||||
slatepacks: "Slatepack"
|
||||
slatepacks_value: "手动交易"
|
||||
trusted_sites: "受信任网站"
|
||||
lock_wallet: "锁定钱包"
|
||||
switch_wallet: "切换钱包"
|
||||
advanced: "高级"
|
||||
@@ -716,6 +717,44 @@ goblin:
|
||||
by_author: "作者 %{id}"
|
||||
reacts_to: "回应 %{id}"
|
||||
article_title: "标题:%{title}"
|
||||
trust:
|
||||
title: "信任"
|
||||
headline: "信任 %{domain}?"
|
||||
identity: "登录身份"
|
||||
grant_intro: "在你退出登录或结束会话之前,%{domain} 可以为你签署以下内容,无需再次询问:"
|
||||
cat_social: "帖子和互动"
|
||||
cat_dm: "私信"
|
||||
cat_market: "商品列表"
|
||||
cat_identity: "资料和列表"
|
||||
cat_delete: "删除"
|
||||
cat_http: "上传和 HTTP 认证"
|
||||
cat_unknown: "其他事件类型(kind %{n})"
|
||||
login_excluded: "登录权限绝不会授予网站。"
|
||||
money_line: "任何花费或收取资金的操作每次都会要求输入密码。买入、卖出和付款确认绝不会被静默签署。"
|
||||
duration: "此权限仅在本次会话有效。你可随时在设置,受信任网站中结束。你的密钥绝不离开钱包。"
|
||||
pass_prompt: "输入钱包密码以信任此网站。"
|
||||
confirm_hold: "长按以在本次会话中信任"
|
||||
sent: "已在本次会话中信任 %{domain}"
|
||||
failed: "无法连接 %{domain}。会话未启动。"
|
||||
notice_volume: "某个受信任网站签署量过大。"
|
||||
money:
|
||||
title: "确认"
|
||||
headline: "在 %{domain} 中确认?"
|
||||
identity: "以 %{id} 签署"
|
||||
explain: "此操作转移或锁定价值,因此每次都会询问。批准将签署这一操作。"
|
||||
pass_prompt: "输入钱包密码以确认。"
|
||||
confirm_hold: "长按以确认"
|
||||
encrypt_desc: "一条锁定付款的加密订单消息。"
|
||||
trusted_sites:
|
||||
title: "受信任网站"
|
||||
intro: "本次会话中你信任的网站可在不询问的情况下签署低风险操作。资金操作始终询问。可在此结束任意会话。"
|
||||
empty: "暂无受信任网站。登录时信任某个网站即可为其开启签署会话。"
|
||||
none: "无"
|
||||
can_sign: "可静默签署:%{cats}"
|
||||
time_left: "%{mins} 分钟后结束"
|
||||
paused: "已暂停,签署量过大"
|
||||
resume: "恢复"
|
||||
end: "结束会话"
|
||||
identities:
|
||||
title: "身份"
|
||||
switch_hint: "切换身份"
|
||||
|
||||
@@ -149,6 +149,17 @@ pub struct GoblinWalletView {
|
||||
/// Quiet toast for the login outcome: text and when it appeared. Reused for
|
||||
/// the authorize outcome (same quiet pill, different strings).
|
||||
login_toast: Option<(String, std::time::Instant)>,
|
||||
/// A "Trust with Goblin" (Authorize Sessions) grant awaiting approval. While
|
||||
/// this OR `login`/`authorize`/`money` is `Some`, new incoming requests are
|
||||
/// ignored (one approval at a time).
|
||||
trust: Option<TrustState>,
|
||||
/// The hold-to-confirm gesture for the single high-value trust decision.
|
||||
trust_hold: w::HoldToSend,
|
||||
/// A money-tier sign or encrypt arriving mid-session that must be
|
||||
/// password-approved per action (the v1-style prompt raised over the channel).
|
||||
money: Option<MoneyState>,
|
||||
/// The hold-to-confirm gesture for a money-tier approval.
|
||||
money_hold: w::HoldToSend,
|
||||
}
|
||||
|
||||
/// Whether the per-identity cue is drawn on activity rows (owner-approved). The
|
||||
@@ -193,6 +204,9 @@ enum SettingsPage {
|
||||
Advanced,
|
||||
/// The identity switcher: one wallet, one balance, many nostr identities.
|
||||
Identities,
|
||||
/// Trusted Sites: the active Authorize Sessions, what each can sign, and a
|
||||
/// one-tap end (revocation).
|
||||
TrustedSites,
|
||||
}
|
||||
|
||||
/// Inline state for the Advanced (wallet-recovery) settings page: the
|
||||
@@ -284,6 +298,10 @@ impl Default for GoblinWalletView {
|
||||
login: None,
|
||||
authorize: None,
|
||||
login_toast: None,
|
||||
trust: None,
|
||||
trust_hold: w::HoldToSend::default(),
|
||||
money: None,
|
||||
money_hold: w::HoldToSend::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,6 +447,54 @@ struct AuthorizeState {
|
||||
result: std::sync::Arc<std::sync::Mutex<Option<Result<(), String>>>>,
|
||||
}
|
||||
|
||||
/// The "Trust with Goblin" (Authorize Sessions) grant modal id. Its verb is
|
||||
/// "Trust", kept distinct from login's "Sign in" and authorize's "Authorize" so
|
||||
/// the user always knows which decision they are making.
|
||||
const TRUST_MODAL: &str = "goblin_trust_modal";
|
||||
/// The money-tier per-action approval modal id (a channel request the wallet
|
||||
/// classified as money, raised mid-session with the same shape as v1 authorize).
|
||||
const MONEY_MODAL: &str = "goblin_money_modal";
|
||||
|
||||
/// One pending "Trust with Goblin" grant. Mirrors [`LoginState`] (it folds login
|
||||
/// in as its identity step, then establishes the session), plus the freshly
|
||||
/// generated wallet channel keypair carried into session creation on success.
|
||||
struct TrustState {
|
||||
/// The validated request (nonce, domain, callback, channel key, relay, kinds).
|
||||
uri: crate::nostr::trusturi::TrustUri,
|
||||
/// The CHOSEN signing identity (pubkey hex); defaults to the active one.
|
||||
selected: String,
|
||||
/// When the request arrived, for the [`LOGIN_EXPIRY_SECS`] deadline.
|
||||
created: std::time::Instant,
|
||||
/// Wallet password typed into the modal; cleared as soon as consumed.
|
||||
pass: String,
|
||||
/// The typed password did not verify; the request stays pending.
|
||||
wrong_pass: bool,
|
||||
/// The login event is signed and its callback POST is in flight; on success
|
||||
/// the session is created, on failure the whole grant fails (spec 4.3).
|
||||
posting: bool,
|
||||
/// The wallet's ephemeral channel keypair for this session, generated at
|
||||
/// approval and carried into [`crate::nostr::session::Session::new`].
|
||||
channel: nostr_sdk::Keys,
|
||||
/// A friendly label for the identity (for the Trusted Sites list).
|
||||
label: String,
|
||||
/// Result slot the login-POST worker fills.
|
||||
result: std::sync::Arc<std::sync::Mutex<Option<Result<(), String>>>>,
|
||||
}
|
||||
|
||||
/// One pending money-tier approval arriving over a live session channel. The
|
||||
/// user sees exactly what they are committing to and gates it with the wallet
|
||||
/// password, hold-to-confirm, every time.
|
||||
struct MoneyState {
|
||||
/// The request awaiting the user's decision (sign or pay-committing encrypt).
|
||||
pending: crate::nostr::session::PendingMoney,
|
||||
/// When it arrived, for the [`LOGIN_EXPIRY_SECS`] deadline.
|
||||
created: std::time::Instant,
|
||||
/// Wallet password typed into the modal; cleared as soon as consumed.
|
||||
pass: String,
|
||||
/// The typed password did not verify; the request stays pending.
|
||||
wrong_pass: bool,
|
||||
}
|
||||
|
||||
/// A password-gated identity action the modal executes. Switching no longer uses
|
||||
/// the modal (it is instant and local); only these need the wallet password.
|
||||
#[derive(Clone)]
|
||||
@@ -901,6 +967,160 @@ impl GoblinWalletView {
|
||||
});
|
||||
}
|
||||
|
||||
// A pending "Trust with Goblin" (Authorize Sessions) grant. One approval
|
||||
// at a time: while login/authorize/trust/money is pending, drop new URIs.
|
||||
if let Some(trust) = crate::take_pending_trust() {
|
||||
if self.login.is_none()
|
||||
&& self.authorize.is_none()
|
||||
&& self.trust.is_none()
|
||||
&& self.money.is_none()
|
||||
{
|
||||
if let Some(active) = wallet.active_nostr_pubkey() {
|
||||
self.send = None;
|
||||
let label = wallet
|
||||
.nostr_identities()
|
||||
.into_iter()
|
||||
.find(|i| i.pubkey_hex == active)
|
||||
.map(|i| i.display())
|
||||
.unwrap_or_else(|| data::short_npub(&active));
|
||||
self.trust = Some(TrustState {
|
||||
uri: trust,
|
||||
selected: active,
|
||||
created: std::time::Instant::now(),
|
||||
pass: String::new(),
|
||||
wrong_pass: false,
|
||||
posting: false,
|
||||
channel: nostr_sdk::Keys::generate(),
|
||||
label,
|
||||
result: std::sync::Arc::new(std::sync::Mutex::new(None)),
|
||||
});
|
||||
self.trust_hold = w::HoldToSend::default();
|
||||
Modal::new(TRUST_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("goblin.trust.title"))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
// An untouched grant dies after 2 minutes (posting is governed by the POST
|
||||
// timeout, so it is exempt here).
|
||||
let trust_expired = matches!(&self.trust, Some(st)
|
||||
if !st.posting && st.created.elapsed().as_secs() >= LOGIN_EXPIRY_SECS);
|
||||
if trust_expired {
|
||||
self.trust = None;
|
||||
if Modal::opened() == Some(TRUST_MODAL) {
|
||||
Modal::close();
|
||||
}
|
||||
}
|
||||
// Poll the login-callback POST. On success the session is created together
|
||||
// with the identity; on failure the whole grant fails (spec 4.3).
|
||||
let mut trust_outcome = None;
|
||||
if let Some(st) = &self.trust {
|
||||
if st.posting {
|
||||
trust_outcome = st
|
||||
.result
|
||||
.lock()
|
||||
.unwrap()
|
||||
.take()
|
||||
.map(|res| (res, st.uri.domain.clone()));
|
||||
if trust_outcome.is_none() {
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((res, domain)) = trust_outcome {
|
||||
let st = self.trust.take();
|
||||
let text = match (res, st) {
|
||||
(Ok(()), Some(st)) => {
|
||||
if let Some(svc) = wallet.nostr_service() {
|
||||
if let (Ok(site_pk), Ok(id_pk)) = (
|
||||
nostr_sdk::PublicKey::from_hex(&st.uri.site_session_pubkey),
|
||||
nostr_sdk::PublicKey::from_hex(&st.selected),
|
||||
) {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_secs())
|
||||
.unwrap_or(0);
|
||||
let session = crate::nostr::session::Session::new(
|
||||
st.uri.domain.clone(),
|
||||
id_pk,
|
||||
st.label.clone(),
|
||||
&st.uri.requested_kinds,
|
||||
&st.channel,
|
||||
site_pk,
|
||||
vec![st.uri.relay.clone()],
|
||||
now,
|
||||
);
|
||||
svc.add_session(session);
|
||||
}
|
||||
}
|
||||
t!("goblin.trust.sent", domain => domain).to_string()
|
||||
}
|
||||
(Err(e), _) => {
|
||||
log::warn!("trust login callback failed: {e}");
|
||||
t!("goblin.trust.failed", domain => domain).to_string()
|
||||
}
|
||||
(Ok(()), None) => t!("goblin.trust.sent", domain => domain).to_string(),
|
||||
};
|
||||
self.login_toast = Some((text, std::time::Instant::now()));
|
||||
}
|
||||
|
||||
// A money-tier request arriving over a live session channel raises the
|
||||
// per-action approval, every time (never silent). One at a time.
|
||||
if self.money.is_none()
|
||||
&& self.login.is_none()
|
||||
&& self.authorize.is_none()
|
||||
&& self.trust.is_none()
|
||||
{
|
||||
if let Some(pending) = wallet.nostr_service().and_then(|s| s.peek_money_prompt()) {
|
||||
self.money = Some(MoneyState {
|
||||
pending,
|
||||
created: std::time::Instant::now(),
|
||||
pass: String::new(),
|
||||
wrong_pass: false,
|
||||
});
|
||||
self.money_hold = w::HoldToSend::default();
|
||||
Modal::new(MONEY_MODAL)
|
||||
.position(ModalPosition::CenterTop)
|
||||
.title(t!("goblin.money.title"))
|
||||
.show();
|
||||
}
|
||||
}
|
||||
// An unanswered money prompt times out and declines the action.
|
||||
let money_expired = matches!(&self.money, Some(st)
|
||||
if st.created.elapsed().as_secs() >= LOGIN_EXPIRY_SECS);
|
||||
if money_expired {
|
||||
if let (Some(st), Some(svc)) = (self.money.as_ref(), wallet.nostr_service()) {
|
||||
svc.answer_money_prompt(st.pending.id(), false);
|
||||
}
|
||||
self.money = None;
|
||||
if Modal::opened() == Some(MONEY_MODAL) {
|
||||
Modal::close();
|
||||
}
|
||||
}
|
||||
// A session "signing a lot" notice surfaces as the quiet toast.
|
||||
if wallet
|
||||
.nostr_service()
|
||||
.and_then(|s| s.take_session_notice())
|
||||
.is_some()
|
||||
{
|
||||
self.login_toast = Some((
|
||||
t!("goblin.trust.notice_volume").to_string(),
|
||||
std::time::Instant::now(),
|
||||
));
|
||||
}
|
||||
// Draw the trust and money modals.
|
||||
if Modal::opened() == Some(TRUST_MODAL) {
|
||||
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
|
||||
self.trust_modal_content(ui, modal, wallet, cb);
|
||||
});
|
||||
}
|
||||
if Modal::opened() == Some(MONEY_MODAL) {
|
||||
Modal::ui(ui.ctx(), cb, |ui, modal, cb| {
|
||||
self.money_modal_content(ui, modal, wallet, cb);
|
||||
});
|
||||
}
|
||||
|
||||
// Send flow takes the full surface when active.
|
||||
if let Some(send) = &mut self.send {
|
||||
let done = send.ui(ui, wallet, cb, &mut self.avatars);
|
||||
@@ -2969,6 +3189,7 @@ impl GoblinWalletView {
|
||||
SettingsPage::Privacy => return self.privacy_ui(ui),
|
||||
SettingsPage::Advanced => return self.advanced_ui(ui, wallet, cb),
|
||||
SettingsPage::Identities => return self.identities_ui(ui, wallet, cb),
|
||||
SettingsPage::TrustedSites => return self.trusted_sites_ui(ui, wallet, cb),
|
||||
SettingsPage::Main => {}
|
||||
}
|
||||
// Minimum-confirmations edit modal (GRIM parity), opened from the
|
||||
@@ -3273,6 +3494,7 @@ impl GoblinWalletView {
|
||||
ui.add_space(16.0);
|
||||
let mut open_node = false;
|
||||
let mut open_slatepack = false;
|
||||
let mut open_trusted = false;
|
||||
settings_group(ui, &t!("goblin.settings.wallet"), |ui| {
|
||||
if settings_row_nav(ui, &t!("goblin.settings.node"), &node_summary(wallet)) {
|
||||
open_node = true;
|
||||
@@ -3302,11 +3524,27 @@ impl GoblinWalletView {
|
||||
) {
|
||||
open_slatepack = true;
|
||||
}
|
||||
// Trusted Sites: the active Authorize Sessions, with a one-tap
|
||||
// end. Shows the live count as the row value.
|
||||
let session_count = wallet
|
||||
.nostr_service()
|
||||
.map(|s| s.session_summaries().len())
|
||||
.unwrap_or(0);
|
||||
if settings_row_nav(
|
||||
ui,
|
||||
&t!("goblin.settings.trusted_sites"),
|
||||
&session_count.to_string(),
|
||||
) {
|
||||
open_trusted = true;
|
||||
}
|
||||
});
|
||||
if open_slatepack {
|
||||
self.slatepack = SlatepackManual::default();
|
||||
self.settings_page = SettingsPage::Slatepack;
|
||||
}
|
||||
if open_trusted {
|
||||
self.settings_page = SettingsPage::TrustedSites;
|
||||
}
|
||||
if open_relays {
|
||||
// The ACTIVE set (override or per-identity advertised set),
|
||||
// so the editor shows what is really in use.
|
||||
@@ -6815,6 +7053,501 @@ impl GoblinWalletView {
|
||||
}
|
||||
}
|
||||
|
||||
/// The "Trust with Goblin" (Authorize Sessions) grant modal: proves identity
|
||||
/// (folds login in) AND establishes the session in one password-gated,
|
||||
/// hold-to-confirm decision. Shows the identity, the low-tier categories being
|
||||
/// granted for silent signing, and the fixed line that money always asks.
|
||||
fn trust_modal_content(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
) {
|
||||
let Some(st) = self.trust.as_mut() else {
|
||||
Modal::close();
|
||||
return;
|
||||
};
|
||||
if st.posting {
|
||||
Modal::close();
|
||||
return;
|
||||
}
|
||||
let domain = st.uri.domain.clone();
|
||||
let identities = wallet.nostr_identities();
|
||||
let display = crate::nostr::session::render_grant(&st.uri.requested_kinds);
|
||||
let mut go = false;
|
||||
let mut cancel = false;
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trust.headline", domain => domain.clone()))
|
||||
.size(17.0)
|
||||
.color(Colors::title(false)),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trust.identity"))
|
||||
.size(13.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
// Identity picker (defaults to the active identity), the truncated
|
||||
// npub always shown as the anchor.
|
||||
if identities.len() > 1 {
|
||||
for id in &identities {
|
||||
let selected = st.selected == id.pubkey_hex;
|
||||
let title = id.display();
|
||||
let short = data::short_npub(&id.pubkey_hex);
|
||||
let row = ui
|
||||
.scope(|ui| {
|
||||
ui.horizontal(|ui| {
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(if selected {
|
||||
crate::gui::icons::CHECK_CIRCLE
|
||||
} else {
|
||||
crate::gui::icons::CIRCLE
|
||||
})
|
||||
.size(18.0)
|
||||
.color(if selected {
|
||||
Colors::green()
|
||||
} else {
|
||||
Colors::gray()
|
||||
}),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.vertical(|ui| {
|
||||
if title != short {
|
||||
ui.label(
|
||||
RichText::new(&title)
|
||||
.size(15.0)
|
||||
.color(Colors::text(false)),
|
||||
);
|
||||
}
|
||||
ui.label(
|
||||
RichText::new(&short).size(12.5).color(Colors::gray()),
|
||||
);
|
||||
});
|
||||
});
|
||||
})
|
||||
.response
|
||||
.rect;
|
||||
let hit = ui.interact(
|
||||
row,
|
||||
egui::Id::from(modal.id).with(("trust_id", id.pubkey_hex.as_str())),
|
||||
Sense::click(),
|
||||
);
|
||||
if hit
|
||||
.on_hover_cursor(egui::CursorIcon::PointingHand)
|
||||
.clicked()
|
||||
{
|
||||
st.selected = id.pubkey_hex.clone();
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
}
|
||||
} else if let Some(id) = identities.first() {
|
||||
let title = id.display();
|
||||
let short = data::short_npub(&id.pubkey_hex);
|
||||
if title != short {
|
||||
ui.label(RichText::new(&title).size(15.0).color(Colors::text(false)));
|
||||
}
|
||||
ui.label(RichText::new(&short).size(12.5).color(Colors::gray()));
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
// What the silent grant covers, as human categories.
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trust.grant_intro", domain => domain.clone()))
|
||||
.size(13.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
ui.add_space(6.0);
|
||||
for cat in &display.categories {
|
||||
ui.label(
|
||||
RichText::new(format!("• {}", t!(cat.key())))
|
||||
.size(13.5)
|
||||
.color(Colors::text(false)),
|
||||
);
|
||||
}
|
||||
for kind in &display.unknown_kinds {
|
||||
ui.label(
|
||||
RichText::new(format!(
|
||||
"• {}",
|
||||
t!("goblin.trust.cat_unknown", n => kind.to_string())
|
||||
))
|
||||
.size(13.5)
|
||||
.color(Colors::red()),
|
||||
);
|
||||
}
|
||||
if display.stripped_login {
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trust.login_excluded"))
|
||||
.size(12.5)
|
||||
.color(Colors::red()),
|
||||
);
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
// The fixed money line: the low-tier grant is not a money grant.
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trust.money_line"))
|
||||
.size(13.5)
|
||||
.color(Colors::title(false)),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trust.duration"))
|
||||
.size(12.5)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
ui.add_space(12.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trust.pass_prompt"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
let mut field = TextEdit::new(egui::Id::from(modal.id).with("trust_pass")).password();
|
||||
field.ui(ui, &mut st.pass, cb);
|
||||
if st.pass.is_empty() {
|
||||
st.wrong_pass = false;
|
||||
} else if st.wrong_pass {
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.advanced.wrong_password"))
|
||||
.size(16.0)
|
||||
.color(Colors::red()),
|
||||
);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
});
|
||||
// Hold-to-confirm: the single high-value decision cannot be a stray tap.
|
||||
if self.trust_hold.ui(ui, &t!("goblin.trust.confirm_hold")) {
|
||||
go = true;
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.scope(|ui| {
|
||||
ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(
|
||||
ui,
|
||||
t!("modal.cancel"),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
cancel = true;
|
||||
},
|
||||
);
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
});
|
||||
if cancel {
|
||||
self.trust = None;
|
||||
Modal::close();
|
||||
return;
|
||||
}
|
||||
if go {
|
||||
let (pass, selected) = match self.trust.as_ref() {
|
||||
Some(st) => (st.pass.clone(), st.selected.clone()),
|
||||
None => return,
|
||||
};
|
||||
if pass.is_empty() {
|
||||
self.trust_hold = w::HoldToSend::default();
|
||||
return;
|
||||
}
|
||||
if !wallet.verify_nostr_password(&pass) {
|
||||
if let Some(st) = self.trust.as_mut() {
|
||||
st.wrong_pass = true;
|
||||
}
|
||||
self.trust_hold = w::HoldToSend::default();
|
||||
return;
|
||||
}
|
||||
let keys = wallet.nostr_service().and_then(|s| {
|
||||
s.recv_snapshot()
|
||||
.into_iter()
|
||||
.find(|h| h.keys.public_key().to_hex() == selected)
|
||||
.map(|h| h.keys)
|
||||
});
|
||||
let Some(keys) = keys else {
|
||||
self.trust = None;
|
||||
Modal::close();
|
||||
return;
|
||||
};
|
||||
let st = self.trust.as_mut().unwrap();
|
||||
st.pass.clear();
|
||||
st.wrong_pass = false;
|
||||
// Sign and POST the kind-22242 login event exactly as Build 150 does;
|
||||
// the session is created by the router once this POST succeeds.
|
||||
match crate::nostr::loginuri::build_login_event(
|
||||
&keys,
|
||||
&st.uri.challenge,
|
||||
&st.uri.domain,
|
||||
) {
|
||||
Ok(event) => {
|
||||
st.posting = true;
|
||||
let callback = st.uri.callback.clone();
|
||||
let slot = st.result.clone();
|
||||
std::thread::spawn(move || {
|
||||
let res = match tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
{
|
||||
Ok(rt) => rt.block_on(async {
|
||||
let post =
|
||||
crate::nostr::loginuri::post_login_event(&callback, &event);
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(LOGIN_POST_TIMEOUT_SECS),
|
||||
post,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(r) => r,
|
||||
Err(_) => Err("timeout".to_string()),
|
||||
}
|
||||
}),
|
||||
Err(e) => Err(e.to_string()),
|
||||
};
|
||||
*slot.lock().unwrap() = Some(res);
|
||||
});
|
||||
Modal::close();
|
||||
cb.return_to_caller();
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("trust login event signing failed: {e}");
|
||||
self.login_toast = Some((
|
||||
t!("goblin.trust.failed", domain => domain).to_string(),
|
||||
std::time::Instant::now(),
|
||||
));
|
||||
self.trust = None;
|
||||
Modal::close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The money-tier per-action approval modal: a value-moving sign (or a
|
||||
/// pay-committing encrypt) arriving over a live session channel. Identical
|
||||
/// gravity to a v1 authorize — what is being done, which identity, a masked
|
||||
/// password, hold-to-confirm — raised every time, never silent.
|
||||
fn money_modal_content(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
modal: &Modal,
|
||||
wallet: &Wallet,
|
||||
cb: &dyn PlatformCallbacks,
|
||||
) {
|
||||
use crate::nostr::session::ChannelOp;
|
||||
let Some(st) = self.money.as_mut() else {
|
||||
Modal::close();
|
||||
return;
|
||||
};
|
||||
let domain = st.pending.domain.clone();
|
||||
let req_id = st.pending.id().to_string();
|
||||
let short_id = data::short_npub(&st.pending.identity_pubkey.to_hex());
|
||||
// A one-line description of exactly what is being committed to.
|
||||
let what = match &st.pending.op {
|
||||
ChannelOp::Sign(req) => {
|
||||
let label = crate::nostr::authuri::kind_label(req.event.kind);
|
||||
let (preview, _) = crate::nostr::authuri::content_preview(&req.event.content);
|
||||
let preview = crate::nostr::authuri::escape_for_display(&preview);
|
||||
if preview.trim().is_empty() {
|
||||
t!(label.key(), n => req.event.kind.to_string()).to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{}: {}",
|
||||
t!(label.key(), n => req.event.kind.to_string()),
|
||||
preview
|
||||
)
|
||||
}
|
||||
}
|
||||
ChannelOp::Encrypt(_) => t!("goblin.money.encrypt_desc").to_string(),
|
||||
};
|
||||
let mut approve = false;
|
||||
let mut decline = false;
|
||||
ui.vertical_centered(|ui| {
|
||||
ui.add_space(6.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.money.headline", domain => domain.clone()))
|
||||
.size(17.0)
|
||||
.color(Colors::title(false)),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.label(RichText::new(&what).size(14.0).color(Colors::text(false)));
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.money.identity", id => short_id.clone()))
|
||||
.size(12.5)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
ui.add_space(8.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.money.explain"))
|
||||
.size(13.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.money.pass_prompt"))
|
||||
.size(16.0)
|
||||
.color(Colors::gray()),
|
||||
);
|
||||
ui.add_space(10.0);
|
||||
let mut field = TextEdit::new(egui::Id::from(modal.id).with("money_pass")).password();
|
||||
field.ui(ui, &mut st.pass, cb);
|
||||
if st.pass.is_empty() {
|
||||
st.wrong_pass = false;
|
||||
} else if st.wrong_pass {
|
||||
ui.add_space(10.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.advanced.wrong_password"))
|
||||
.size(16.0)
|
||||
.color(Colors::red()),
|
||||
);
|
||||
}
|
||||
ui.add_space(12.0);
|
||||
});
|
||||
if self.money_hold.ui(ui, &t!("goblin.money.confirm_hold")) {
|
||||
approve = true;
|
||||
}
|
||||
ui.add_space(6.0);
|
||||
ui.vertical_centered_justified(|ui| {
|
||||
View::button(
|
||||
ui,
|
||||
t!("modal.cancel"),
|
||||
Colors::white_or_black(false),
|
||||
|| {
|
||||
decline = true;
|
||||
},
|
||||
);
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
if decline {
|
||||
if let Some(svc) = wallet.nostr_service() {
|
||||
svc.answer_money_prompt(&req_id, false);
|
||||
}
|
||||
self.money = None;
|
||||
Modal::close();
|
||||
return;
|
||||
}
|
||||
if approve {
|
||||
let pass = self
|
||||
.money
|
||||
.as_ref()
|
||||
.map(|s| s.pass.clone())
|
||||
.unwrap_or_default();
|
||||
if pass.is_empty() || !wallet.verify_nostr_password(&pass) {
|
||||
if let Some(st) = self.money.as_mut() {
|
||||
st.wrong_pass = true;
|
||||
}
|
||||
self.money_hold = w::HoldToSend::default();
|
||||
return;
|
||||
}
|
||||
if let Some(svc) = wallet.nostr_service() {
|
||||
svc.answer_money_prompt(&req_id, true);
|
||||
}
|
||||
self.money = None;
|
||||
Modal::close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trusted Sites: the active Authorize Sessions, what each can sign silently,
|
||||
/// time remaining, and a one-tap end (immediate, unilateral revocation).
|
||||
fn trusted_sites_ui(
|
||||
&mut self,
|
||||
ui: &mut egui::Ui,
|
||||
wallet: &Wallet,
|
||||
_cb: &dyn PlatformCallbacks,
|
||||
) {
|
||||
let t = theme::tokens();
|
||||
if self.sub_header(ui, &t!("goblin.trusted_sites.title")) {
|
||||
self.settings_page = SettingsPage::Main;
|
||||
return;
|
||||
}
|
||||
let summaries = wallet
|
||||
.nostr_service()
|
||||
.map(|s| s.session_summaries())
|
||||
.unwrap_or_default();
|
||||
ScrollArea::vertical()
|
||||
.id_salt("goblin_trusted_sites_scroll")
|
||||
.auto_shrink([false; 2])
|
||||
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden)
|
||||
.show(ui, |ui| {
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trusted_sites.intro"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.add_space(12.0);
|
||||
if summaries.is_empty() {
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trusted_sites.empty"))
|
||||
.font(FontId::new(13.0, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
return;
|
||||
}
|
||||
let mut to_end: Option<String> = None;
|
||||
let mut to_resume: Option<String> = None;
|
||||
for s in &summaries {
|
||||
settings_group(ui, &s.domain, |ui| {
|
||||
// The low-tier categories this session signs silently.
|
||||
let cats: Vec<String> = s
|
||||
.categories
|
||||
.iter()
|
||||
.map(|c| t!(c.key()).to_string())
|
||||
.collect();
|
||||
let cats = if cats.is_empty() {
|
||||
t!("goblin.trusted_sites.none").to_string()
|
||||
} else {
|
||||
cats.join(", ")
|
||||
};
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trusted_sites.can_sign", cats => cats))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
ui.add_space(4.0);
|
||||
let mins = s.ttl_remaining_secs / 60;
|
||||
ui.label(
|
||||
RichText::new(
|
||||
t!("goblin.trusted_sites.time_left", mins => mins.to_string()),
|
||||
)
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(t.text_dim),
|
||||
);
|
||||
if s.paused {
|
||||
ui.add_space(4.0);
|
||||
ui.label(
|
||||
RichText::new(t!("goblin.trusted_sites.paused"))
|
||||
.font(FontId::new(12.5, fonts::regular()))
|
||||
.color(Colors::red()),
|
||||
);
|
||||
if settings_row_btn(
|
||||
ui,
|
||||
&t!("goblin.trusted_sites.resume"),
|
||||
crate::gui::icons::PLAY,
|
||||
) {
|
||||
to_resume = Some(s.domain.clone());
|
||||
}
|
||||
}
|
||||
if settings_row_btn(
|
||||
ui,
|
||||
&t!("goblin.trusted_sites.end"),
|
||||
crate::gui::icons::X_CIRCLE,
|
||||
) {
|
||||
to_end = Some(s.domain.clone());
|
||||
}
|
||||
});
|
||||
ui.add_space(8.0);
|
||||
}
|
||||
if let Some(svc) = wallet.nostr_service() {
|
||||
if let Some(d) = to_end {
|
||||
svc.end_session(&d);
|
||||
}
|
||||
if let Some(d) = to_resume {
|
||||
svc.resume_session(&d);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Draw the transient login-outcome toast: the same quiet pill as the back
|
||||
/// hint (solid soft pill, no border, small dim text), bottom-anchored,
|
||||
/// non-blocking, fading out.
|
||||
|
||||
Reference in New Issue
Block a user