aboutsummaryrefslogtreecommitdiffstats
path: root/src/features/nostrlogin.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/features/nostrlogin.js')
-rw-r--r--src/features/nostrlogin.js83
1 files changed, 64 insertions, 19 deletions
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js
index f39f9c3..4a811fc 100644
--- a/src/features/nostrlogin.js
+++ b/src/features/nostrlogin.js
@@ -69,7 +69,7 @@ export function nostrLoginFeature(ctx) {
// decrypt that" is a healthy connection doing its job — the DM inbox probes
// it with wraps that turn out to be someone else's, and treating each
// refusal as a dead signer disconnected Amethyst users over and over.
- const SIGNER_DEAD = /did not answer|timed? ?out|connect|closed|socket|relay|network/i;
+ const SIGNER_DEAD = /did not answer|timed? ?out|connect|closed|not open|socket|relay|network/i;
const selfHealing = (signer) => {
if (!signer || signer.kind !== 'bunker') return signer;
const guard = (fn) => async (...args) => {
@@ -364,6 +364,11 @@ export function nostrLoginFeature(ctx) {
await run(() => keySigner(sk));
} catch (e) {
if (/derive a wallet key/.test(e.message) || e.message === pass.LOCK_PASSKEY) { ui.nostrLoginError = e.message; render(); return; }
+ // Only "no passkey here" (NotAllowedError, as browsers report it) rolls
+ // into creating one; anything else is a failure to show, not a reason
+ // to make a second passkey (the Beta's "RP ID cannot be validated" went
+ // on to fail a create too, 2026-11-06).
+ if (e && e.name && e.name !== 'NotAllowedError') { ui.nostrLoginError = e.message || String(e); render(); return; }
try {
const sk = await pass.passkeyCreate();
await run(() => keySigner(sk));
@@ -422,21 +427,25 @@ export function nostrLoginFeature(ctx) {
h('button', { class: 'btn-sm', type: 'button', onClick: () => { if (amberDeliver(e.text)) e.text = ''; else { ui.nostrLoginError = t('nlAmberBadPaste'); render(); } } }, t('nlAmberPasteBtn'))),
h('button', { class: 'btn-ghost btn-sm', type: 'button', onClick: () => amberCancel() }, t('cancel')));
}
- function externalButtons(run, big = false) {
+ // passkeyFirst: the front door lists the passkey right under the seed
+ // phrase, before Google (Adam, 2026-11-06)
+ // short: under a "Sign in" heading the buttons just name the way in
+ function externalButtons(run, big = false, passkeyFirst = false, short = false) {
const hasPasskey = typeof window !== 'undefined' && !!window.PublicKeyCredential && !!navigator.credentials;
const pad = big ? 'padding:14px' : '';
- return [
+ const google = [
h('button', { class: 'btn-block', style: pad, disabled: ui.nostrLoginBusy,
onClick: () => googleLogin(run) },
- markedLabel(GOOGLE_MARK, t('nlGoogle'), ui.nostrLoginVia === 'google')),
+ markedLabel(GOOGLE_MARK, t(short ? 'doorGoogle' : 'nlGoogle'), ui.nostrLoginVia === 'google')),
GOOGLE_CENTRAL !== GOOGLE_LEGACY
? h('button', { class: 'linklike small', style: 'align-self:center', disabled: ui.nostrLoginBusy, onClick: () => googleLogin(run, GOOGLE_LEGACY) }, t('nlGooglePrev'))
: null,
- hasPasskey
- ? h('button', { class: 'btn-block', style: pad, disabled: ui.nostrLoginBusy,
- onClick: () => passkeyLogin(run) }, markedLabel(PASSKEY_MARK, t('nlPasskey'), ui.nostrLoginVia === 'passkey'))
- : null,
];
+ const passkey = hasPasskey
+ ? h('button', { class: 'btn-block', style: pad, disabled: ui.nostrLoginBusy,
+ onClick: () => passkeyLogin(run) }, markedLabel(PASSKEY_MARK, t(short ? 'doorPasskey' : 'nlPasskey'), ui.nostrLoginVia === 'passkey'))
+ : null;
+ return passkeyFirst ? [passkey, ...google] : [...google, passkey];
}
// The ways in, as buttons. `run` receives a signer factory. The expanded
@@ -611,12 +620,18 @@ export function nostrLoginFeature(ctx) {
// time, and a pause after a failure: identity() asks on every send, and a
// signer that isn't answering shouldn't mean a new relay connection per
// keystroke.
- async function resumeLogin() {
+ const tabHidden = () => typeof document !== 'undefined' && document.visibilityState === 'hidden';
+ async function resumeLogin({ force = false } = {}) {
if (liveSigner()) return live;
const st = load();
if (!st.pubkey) return null;
if (resuming) return resuming;
- if (Date.now() - resumeFailedAt < 20_000) return null;
+ if (!force && Date.now() - resumeFailedAt < 20_000) return null;
+ // A phone freezes a hidden tab: a remote signer asked from there can't be
+ // heard, and that silence isn't the session failing — don't count it.
+ let wasHidden = tabHidden();
+ const onVis = () => { if (tabHidden()) wasHidden = true; };
+ if (typeof document !== 'undefined') document.addEventListener('visibilitychange', onVis);
resuming = (async () => {
// a stored passkey-derived key reattaches instantly — no network, no
// dialog; see sessionOf for why this key (alone among keys) is kept
@@ -645,10 +660,13 @@ export function nostrLoginFeature(ctx) {
})();
try {
const s = await resuming;
- if (!s) resumeFailedAt = Date.now();
- else announceLive();
+ if (!s && !wasHidden) resumeFailedAt = Date.now();
+ else if (s) announceLive();
return s;
- } finally { resuming = null; }
+ } finally {
+ resuming = null;
+ if (typeof document !== 'undefined') document.removeEventListener('visibilitychange', onVis);
+ }
}
// Which door this account came through, for the reconnect surfaces. State
@@ -708,8 +726,31 @@ export function nostrLoginFeature(ctx) {
// reattach silently). Same guarded buttons as the settings card — an
// extension, the signer-app deeplink/QR, or a pasted key — and the run
// refuses a different account.
+ // While the reconnect screen is up, keep trying the stored session: the
+ // remote signer authorised this client key long ago and nearly always still
+ // answers — it just wasn't heard the moment the screen opened (tab frozen,
+ // relay socket cut, the signer app asleep until opened). One try and then
+ // nothing meant a brand-new connection every time.
+ let retryT = 0;
+ const retryStored = () => {
+ clearTimeout(retryT); retryT = 0;
+ if (ui.nostrReconnect !== true || liveSigner()) return;
+ const st = load();
+ if (!st.session || st.session.amber) return;
+ if (!tabHidden()) {
+ resumeLogin({ force: true }).then((sg) => {
+ if (sg && ui.nostrReconnect === true) { stopNostrConnect(); ui.nostrReconnect = null; ui.nostrConnectOpen = false; render(); }
+ }).catch(() => {});
+ }
+ retryT = setTimeout(retryStored, 10_000);
+ };
+ if (typeof document !== 'undefined') {
+ document.addEventListener('visibilitychange', () => { if (!tabHidden() && ui.nostrReconnect === true) retryStored(); });
+ }
+
function reconnectScreen() {
const st = load();
+ if (ui.nostrReconnect === true && !retryT && st.session && !st.session.amber) retryT = setTimeout(retryStored, 0);
// 'upgrade': Amber by intent is live but can't read messages — offer
// only its NIP-46 door (nostrconnect), same account enforced
const upgrade = ui.nostrReconnect === 'upgrade';
@@ -756,7 +797,7 @@ export function nostrLoginFeature(ctx) {
if (!st.pubkey || liveSigner()) return false;
ui.nostrReconnect = true;
ui.nostrLoginError = '';
- this.nostrLoginResume().then((sg) => {
+ resumeLogin({ force: true }).then((sg) => {
if (sg && ui.nostrReconnect) { ui.nostrReconnect = null; render(); }
}).catch(() => {});
render();
@@ -820,9 +861,12 @@ export function nostrLoginFeature(ctx) {
.catch(() => { _spendingHinted = false; });
return true;
},
- frontDoorSignin() {
- return h('div', { class: 'col', style: 'gap:8px' },
- ...externalButtons(loginWith, true),
+ // The front door's ways in, loose: the page spaces them evenly with the
+ // seed-phrase and coinos.io doors around them (a wrapper of their own
+ // put 8px between these and 20px between them and the rest).
+ frontDoorSignin({ short = false } = {}) {
+ return [
+ ...externalButtons(loginWith, true, true, short),
h('button', { class: 'btn-block', style: 'padding:14px', onClick: () => {
if (ui.onb) ui.onb.step = 'signin';
ui.nostrLoginOpen = true;
@@ -830,8 +874,9 @@ export function nostrLoginFeature(ctx) {
render();
} }, h('span', { style: 'display:flex;align-items:center;justify-content:center;gap:8px' },
h('span', { style: 'display:flex;flex-shrink:0', html: OSTRICH }),
- t('nlSignInNostr'))),
- ui.nostrLoginError ? h('div', { class: 'notice err' }, ui.nostrLoginError) : null);
+ t(short ? 'doorNostr' : 'nlSignInNostr'))),
+ ui.nostrLoginError ? h('div', { class: 'notice err' }, ui.nostrLoginError) : null,
+ ].filter(Boolean);
},
nostrSettingsCards() { return [settingsCard()]; },
};