aboutsummaryrefslogtreecommitdiffstats
path: root/src/features
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-26 21:03:07 -0700
committerAdam Soltys <[email protected]>2026-08-26 21:03:07 -0700
commit358ffb856892bff8e99287d3debc5937ed5183ef (patch)
tree21cd9a381ebd1aa620d1db4ae965ca27a338ac1b /src/features
parent181cf4d5d0ffb7ae3ccd337745a16619a35eaae1 (diff)
downloadcoinosv3-358ffb856892bff8e99287d3debc5937ed5183ef.tar.gz
coinosv3-358ffb856892bff8e99287d3debc5937ed5183ef.zip
Two new doors, no new locks: "Sign in with Google" (pomegranate — the nostr key FROST-sharded 2-of-3 across community operators, Google as coordinator and recovery, a plain bunker:// URI feeding the existing signer flow) and passkey sign-in (WebAuthn PRF derives the key deterministically, treated exactly like a pasted nsec, synced wherever the platform carries the passkey) — both lazy chunks, neither touching the first paint
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_019eA8JRq9Wr7z2fRQk99HLi
Diffstat (limited to 'src/features')
-rw-r--r--src/features/nostrlogin.js66
1 files changed, 65 insertions, 1 deletions
diff --git a/src/features/nostrlogin.js b/src/features/nostrlogin.js
index 9c4156f..fdab973 100644
--- a/src/features/nostrlogin.js
+++ b/src/features/nostrlogin.js
@@ -214,10 +214,74 @@ export function nostrLoginFeature(ctx) {
h('button', { class: 'btn-sm', onClick: () => { stopNostrConnect(); ui.nostrConnectOpen = false; render(); } }, t('cancel'))));
}
- // The three ways in, as buttons. `run` receives a signer factory.
+ // "Sign in with Google" — pomegranate: the code (and its FROST dealer)
+ // rides in a lazy chunk, so the popup must open HERE, inside the click's
+ // user activation, and the module navigates it once loaded.
+ async function googleLogin(run) {
+ const popup = window.open('about:blank', 'OAuth', 'width=600,height=600');
+ ui.nostrLoginError = '';
+ try {
+ const pome = await import('../pomegranate.js');
+ const { uri } = await pome.loginWithGoogle(popup, {
+ onStatus: (s) => { ui.nostrLoginStatus = t('nlGoogleStatus_' + s); render(); },
+ });
+ ui.nostrLoginStatus = '';
+ await run(() => bunkerSigner(uri, { onAuth: (url) => { ui.nostrLoginAuthUrl = url; render(); } }));
+ } catch (e) {
+ try { popup && popup.close(); } catch {}
+ ui.nostrLoginStatus = '';
+ ui.nostrLoginError = e.message;
+ render();
+ }
+ }
+
+ // Passkey sign-in: try an existing credential first; when the device has
+ // none (or the user dismisses the picker), offer to create one instead of
+ // showing a bare error.
+ async function passkeyLogin(run) {
+ ui.nostrLoginError = '';
+ const pass = await import('../passkey.js');
+ try {
+ const sk = await pass.passkeySignIn();
+ await run(() => keySigner(sk));
+ } catch (e) {
+ if (/derive a wallet key/.test(e.message)) { ui.nostrLoginError = e.message; render(); return; }
+ ui.passkeyOffer = true; // no credential picked — offer creation
+ render();
+ }
+ }
+ async function passkeyCreateLogin(run) {
+ ui.nostrLoginError = '';
+ ui.passkeyOffer = null;
+ try {
+ const pass = await import('../passkey.js');
+ const sk = await pass.passkeyCreate();
+ await run(() => keySigner(sk));
+ } catch (e) {
+ ui.nostrLoginError = e.message;
+ render();
+ }
+ }
+
+ // The ways in, as buttons. `run` receives a signer factory.
function signerButtons(run) {
const hasExt = typeof window !== 'undefined' && !!window.nostr;
+ const hasPasskey = typeof window !== 'undefined' && !!window.PublicKeyCredential && !!navigator.credentials;
return h('div', { class: 'col', style: 'gap:8px' },
+ h('button', { class: 'btn-block', disabled: ui.nostrLoginBusy,
+ onClick: () => googleLogin(run) }, t('nlGoogle')),
+ ui.nostrLoginStatus ? h('div', { class: 'row gap6', style: 'align-items:center;justify-content:center' },
+ h('span', { class: 'spinner sm' }), h('span', { class: 'small muted' }, ui.nostrLoginStatus)) : null,
+ hasPasskey
+ ? h('button', { class: 'btn-block', disabled: ui.nostrLoginBusy,
+ onClick: () => passkeyLogin(run) }, t('nlPasskey'))
+ : null,
+ ui.passkeyOffer
+ ? h('div', { class: 'row gap6', style: 'align-items:center' },
+ h('span', { class: 'small muted grow' }, t('nlPasskeyNone')),
+ h('button', { class: 'btn-sm', disabled: ui.nostrLoginBusy, onClick: () => passkeyCreateLogin(run) }, t('nlPasskeyCreate')),
+ h('button', { class: 'btn-sm btn-ghost', onClick: () => { ui.passkeyOffer = null; render(); } }, t('cancel')))
+ : null,
hasExt
? h('button', { class: 'btn-block', disabled: ui.nostrLoginBusy,
onClick: () => run(() => extensionSigner()) }, t('nlExtension'))