SkateSpots/src/lib/signerCapabilities.ts
shakespeare.diy 4ed1d9a803 Fix crew card layout, Amber web login hang, Stash encryption gate, and add signup welcome step
- CrewCard: rebuilt to fit any screen width — clip overflow, truncate name/location, single wrapping badge row, consistent avatar sizing; responsive header
- Amber web login: after opening Amber, drop user on manual paste step after 2.5s so the spinner never hangs when the callback opens in a new tab or clipboard is blocked; keep polling on the paste step so a late callback still auto-completes
- Stash: hasNip44/hasNip04 now fall back to the active login when pubkey matching fails, so logged-in nsec/amber users are no longer wrongly told "Encryption Required"
- Signup: added an explicit welcome step that explains account creation and only generates the keypair when the user taps "Create my account" (no more silent auto-creation on dialog open)

Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
2026-08-27 13:27:24 +07:00

78 lines
3.3 KiB
TypeScript

/**
* signerCapabilities — safely probe what a Nostr signer can do.
* ─────────────────────────────────────────────────────────────────────────────
* The NUser extension signer exposes `.nip44` / `.nip04` as GETTERS that call
* `.nostr` (window.nostr) and THROW "Browser extension not available" when the
* extension isn't present. Accessing `user.signer?.nip44` during render will
* therefore crash the whole React tree for extension logins whose extension
* isn't injected yet (page-load race) or is missing entirely.
*
* These helpers determine capability WITHOUT touching the throwing getter:
* we look at the login type + probe `window.nostr` directly (a plain property
* read that never throws). Use `hasNip44(user, logins)` anywhere you'd
* otherwise write `!!user?.signer?.nip44` in a render path.
*/
interface MaybeUser {
pubkey?: string;
}
interface MaybeLogin {
type: string;
pubkey?: string;
}
function windowNostr(): { nip44?: unknown; nip04?: unknown } | undefined {
return (globalThis as { nostr?: { nip44?: unknown; nip04?: unknown } }).nostr;
}
/**
* Resolve the login that backs the current user.
*
* IMPORTANT: we cannot rely solely on `l.pubkey === user.pubkey`. Depending on
* the login method and how Nostrify rehydrates from localStorage, the top-level
* `pubkey` on a stored login object is not always populated (nsec logins in
* particular expose the pubkey nested under the decrypted key material). When a
* pubkey match fails we fall back to the ACTIVE login (`logins[0]`), which is
* exactly the login `useCurrentUser` turns into `users[0]` / `user`. Without
* this fallback, a perfectly capable nsec/amber user gets wrongly told
* "Encryption Required" on the Stash page.
*/
function resolveLogin(
user: MaybeUser | null | undefined,
logins: readonly MaybeLogin[],
): MaybeLogin | undefined {
if (!user?.pubkey || logins.length === 0) return undefined;
return logins.find((l) => l.pubkey === user.pubkey) ?? logins[0];
}
/** True if the user's signer can do NIP-44 encryption. Never throws. */
export function hasNip44(
user: MaybeUser | null | undefined,
logins: readonly MaybeLogin[],
): boolean {
const login = resolveLogin(user, logins);
if (!login) return false;
// nsec + bunker: NIP-44 always available (pure JS / remote signer implements it)
if (login.type === 'nsec' || login.type === 'bunker') return true;
// amber (NIP-55): the Amber signer implements nip44_encrypt / nip44_decrypt
// over native intents (CapacitorAmberSigner) and URL redirects (AmberSigner).
if (login.type === 'x-amber') return true;
// extension: probe window.nostr directly (no throwing getter)
if (login.type === 'extension') return !!windowNostr()?.nip44;
// unknown: no NIP-44
return false;
}
/** True if the user's signer can do NIP-04 encryption. Never throws. */
export function hasNip04(
user: MaybeUser | null | undefined,
logins: readonly MaybeLogin[],
): boolean {
const login = resolveLogin(user, logins);
if (!login) return false;
if (login.type === 'nsec' || login.type === 'bunker') return true;
if (login.type === 'x-amber') return true;
if (login.type === 'extension') return !!windowNostr()?.nip04;
return false;
}