@formstr/signer 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -104,6 +104,35 @@ declare function normalizeNip55Identifier(rawIdentifier: unknown): {
104
104
  *
105
105
  * @see https://github.com/nostr-protocol/nips/blob/master/55.md
106
106
  */
107
+ /** Why the browser NIP-55 flow cannot run here. */
108
+ type Nip55WebSupportReason = 'native' | 'not-android' | 'no-clipboard' | 'firefox';
109
+ /**
110
+ * Whether the browser NIP-55 option should be shown, plus an advisory
111
+ * warning where it is known to be flaky.
112
+ *
113
+ * Visibility and warnings are deliberately separate, and nothing is
114
+ * hard-blocked. The option is hidden only where the mechanism cannot exist
115
+ * at all — a Capacitor native shell (the plugin path is strictly better)
116
+ * and non-Android platforms (the `nostrsigner` intent cannot resolve).
117
+ *
118
+ * Firefox for Android is **shown with a warning** rather than blocked. It
119
+ * advertises `readText` but never grants a persistent permission, so every
120
+ * read needs transient activation and the poll loop cannot complete —
121
+ * verified on an emulator (Firefox 125). We still let the user try, because
122
+ * blocking a browser outright is a worse failure than a warning, and
123
+ * browser behaviour changes.
124
+ */
125
+ interface Nip55WebSupport {
126
+ /** Render the option at all. False in native shells and on non-Android. */
127
+ visible: boolean;
128
+ /**
129
+ * Advisory, non-blocking message to show beside the option. The flow
130
+ * remains attemptable; this only sets expectations.
131
+ */
132
+ warning?: string;
133
+ /** Machine-readable reason for the warning / hidden state. */
134
+ reason?: Nip55WebSupportReason;
135
+ }
107
136
  interface Nip55WebTransport {
108
137
  /**
109
138
  * Whether this environment can run the intent + clipboard flow at all.
@@ -112,6 +141,13 @@ interface Nip55WebTransport {
112
141
  * dead intent.
113
142
  */
114
143
  isSupported(): boolean;
144
+ /**
145
+ * Richer form of {@link isSupported}: whether to *offer* the option and
146
+ * whether it actually works. Hosts should prefer this so they can show an
147
+ * explanatory hint (e.g. "does not work on Firefox") rather than hide a
148
+ * capability that other browsers on the same device do have.
149
+ */
150
+ supportStatus?(): Nip55WebSupport;
115
151
  /** Hand the intent URI to the OS (a browser typically `window.open`s it). */
116
152
  open(intent: string): void;
117
153
  /** Read the current clipboard text. */
@@ -197,6 +233,12 @@ declare class Nip55WebSigner implements ActiveSigner {
197
233
  constructor(options?: Nip55WebOptions);
198
234
  /** True when the configured transport can actually open a signer app. */
199
235
  isSupported(): boolean;
236
+ /**
237
+ * Whether to offer the option and whether it works. Prefer this over
238
+ * {@link isSupported} so a host can surface `message` — notably the
239
+ * Firefox case, which is shown but cannot complete.
240
+ */
241
+ supportStatus(): Nip55WebSupport;
200
242
  getPublicKey(): Promise<string>;
201
243
  signEvent(event: EventTemplate): Promise<Event>;
202
244
  nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string>;
@@ -550,6 +592,18 @@ declare class Signer {
550
592
  * browser flow where it cannot work, not to promise that it will.
551
593
  */
552
594
  supportsNip55Web(transport?: Nip55WebTransport): boolean;
595
+ /**
596
+ * Whether to offer the browser NIP-55 option, plus an advisory `warning`
597
+ * where it is known to be flaky. Prefer this in UI code.
598
+ *
599
+ * Nothing here is a hard block. `visible: false` covers only environments
600
+ * where the mechanism cannot exist — a Capacitor native shell (the plugin
601
+ * path is better) and non-Android platforms (no `nostrsigner` handler).
602
+ * Firefox for Android is `visible: true` **with a warning**, because it
603
+ * advertises `readText` but never grants a persistent permission, so the
604
+ * poll loop usually cannot complete. Users are still allowed to try.
605
+ */
606
+ nip55WebSupport(transport?: Nip55WebTransport): Nip55WebSupport;
553
607
  /**
554
608
  * Sign in via a NIP-55 Android external signer (Amber, etc) **from a
555
609
  * plain browser**, with no Capacitor/native bridge. Opens the installed
@@ -658,4 +712,4 @@ declare class Signer {
658
712
  /** Convenience wrapper around `new Signer(config)`. */
659
713
  declare function createSigner(config?: SignerConfig): Signer;
660
714
 
661
- export { type ActiveSigner as A, type BunkerLoginOptions as B, type LoginMethod as L, type Nip55WebLoginOptions as N, type RelayMismatchHandler as R, Signer as S, type UnlockOptions as U, type AndroidLoginOptions as a, type AndroidLoginResult as b, AndroidSigner as c, type AndroidSignerAppInfo as d, type AndroidSignerPlugin as e, type Nip55WebOptions as f, Nip55WebSigner as g, type Nip55WebTransport as h, type NostrConnectOptions as i, type RelayMismatchInfo as j, type SignerConfig as k, type SignerEvent as l, type StorageAdapter as m, type StoredAccount as n, browserNip55Transport as o, createSigner as p, localStorageAdapter as q, loginWithAndroidSigner as r, normalizeNip55Identifier as s };
715
+ export { type ActiveSigner as A, type BunkerLoginOptions as B, type LoginMethod as L, type Nip55WebLoginOptions as N, type RelayMismatchHandler as R, Signer as S, type UnlockOptions as U, type AndroidLoginOptions as a, type AndroidLoginResult as b, AndroidSigner as c, type AndroidSignerAppInfo as d, type AndroidSignerPlugin as e, type Nip55WebOptions as f, Nip55WebSigner as g, type Nip55WebSupport as h, type Nip55WebSupportReason as i, type Nip55WebTransport as j, type NostrConnectOptions as k, type RelayMismatchInfo as l, type SignerConfig as m, type SignerEvent as n, type StorageAdapter as o, type StoredAccount as p, browserNip55Transport as q, createSigner as r, localStorageAdapter as s, loginWithAndroidSigner as t, normalizeNip55Identifier as u };
@@ -104,6 +104,35 @@ declare function normalizeNip55Identifier(rawIdentifier: unknown): {
104
104
  *
105
105
  * @see https://github.com/nostr-protocol/nips/blob/master/55.md
106
106
  */
107
+ /** Why the browser NIP-55 flow cannot run here. */
108
+ type Nip55WebSupportReason = 'native' | 'not-android' | 'no-clipboard' | 'firefox';
109
+ /**
110
+ * Whether the browser NIP-55 option should be shown, plus an advisory
111
+ * warning where it is known to be flaky.
112
+ *
113
+ * Visibility and warnings are deliberately separate, and nothing is
114
+ * hard-blocked. The option is hidden only where the mechanism cannot exist
115
+ * at all — a Capacitor native shell (the plugin path is strictly better)
116
+ * and non-Android platforms (the `nostrsigner` intent cannot resolve).
117
+ *
118
+ * Firefox for Android is **shown with a warning** rather than blocked. It
119
+ * advertises `readText` but never grants a persistent permission, so every
120
+ * read needs transient activation and the poll loop cannot complete —
121
+ * verified on an emulator (Firefox 125). We still let the user try, because
122
+ * blocking a browser outright is a worse failure than a warning, and
123
+ * browser behaviour changes.
124
+ */
125
+ interface Nip55WebSupport {
126
+ /** Render the option at all. False in native shells and on non-Android. */
127
+ visible: boolean;
128
+ /**
129
+ * Advisory, non-blocking message to show beside the option. The flow
130
+ * remains attemptable; this only sets expectations.
131
+ */
132
+ warning?: string;
133
+ /** Machine-readable reason for the warning / hidden state. */
134
+ reason?: Nip55WebSupportReason;
135
+ }
107
136
  interface Nip55WebTransport {
108
137
  /**
109
138
  * Whether this environment can run the intent + clipboard flow at all.
@@ -112,6 +141,13 @@ interface Nip55WebTransport {
112
141
  * dead intent.
113
142
  */
114
143
  isSupported(): boolean;
144
+ /**
145
+ * Richer form of {@link isSupported}: whether to *offer* the option and
146
+ * whether it actually works. Hosts should prefer this so they can show an
147
+ * explanatory hint (e.g. "does not work on Firefox") rather than hide a
148
+ * capability that other browsers on the same device do have.
149
+ */
150
+ supportStatus?(): Nip55WebSupport;
115
151
  /** Hand the intent URI to the OS (a browser typically `window.open`s it). */
116
152
  open(intent: string): void;
117
153
  /** Read the current clipboard text. */
@@ -197,6 +233,12 @@ declare class Nip55WebSigner implements ActiveSigner {
197
233
  constructor(options?: Nip55WebOptions);
198
234
  /** True when the configured transport can actually open a signer app. */
199
235
  isSupported(): boolean;
236
+ /**
237
+ * Whether to offer the option and whether it works. Prefer this over
238
+ * {@link isSupported} so a host can surface `message` — notably the
239
+ * Firefox case, which is shown but cannot complete.
240
+ */
241
+ supportStatus(): Nip55WebSupport;
200
242
  getPublicKey(): Promise<string>;
201
243
  signEvent(event: EventTemplate): Promise<Event>;
202
244
  nip04Encrypt(peerPubkey: string, plaintext: string): Promise<string>;
@@ -550,6 +592,18 @@ declare class Signer {
550
592
  * browser flow where it cannot work, not to promise that it will.
551
593
  */
552
594
  supportsNip55Web(transport?: Nip55WebTransport): boolean;
595
+ /**
596
+ * Whether to offer the browser NIP-55 option, plus an advisory `warning`
597
+ * where it is known to be flaky. Prefer this in UI code.
598
+ *
599
+ * Nothing here is a hard block. `visible: false` covers only environments
600
+ * where the mechanism cannot exist — a Capacitor native shell (the plugin
601
+ * path is better) and non-Android platforms (no `nostrsigner` handler).
602
+ * Firefox for Android is `visible: true` **with a warning**, because it
603
+ * advertises `readText` but never grants a persistent permission, so the
604
+ * poll loop usually cannot complete. Users are still allowed to try.
605
+ */
606
+ nip55WebSupport(transport?: Nip55WebTransport): Nip55WebSupport;
553
607
  /**
554
608
  * Sign in via a NIP-55 Android external signer (Amber, etc) **from a
555
609
  * plain browser**, with no Capacitor/native bridge. Opens the installed
@@ -658,4 +712,4 @@ declare class Signer {
658
712
  /** Convenience wrapper around `new Signer(config)`. */
659
713
  declare function createSigner(config?: SignerConfig): Signer;
660
714
 
661
- export { type ActiveSigner as A, type BunkerLoginOptions as B, type LoginMethod as L, type Nip55WebLoginOptions as N, type RelayMismatchHandler as R, Signer as S, type UnlockOptions as U, type AndroidLoginOptions as a, type AndroidLoginResult as b, AndroidSigner as c, type AndroidSignerAppInfo as d, type AndroidSignerPlugin as e, type Nip55WebOptions as f, Nip55WebSigner as g, type Nip55WebTransport as h, type NostrConnectOptions as i, type RelayMismatchInfo as j, type SignerConfig as k, type SignerEvent as l, type StorageAdapter as m, type StoredAccount as n, browserNip55Transport as o, createSigner as p, localStorageAdapter as q, loginWithAndroidSigner as r, normalizeNip55Identifier as s };
715
+ export { type ActiveSigner as A, type BunkerLoginOptions as B, type LoginMethod as L, type Nip55WebLoginOptions as N, type RelayMismatchHandler as R, Signer as S, type UnlockOptions as U, type AndroidLoginOptions as a, type AndroidLoginResult as b, AndroidSigner as c, type AndroidSignerAppInfo as d, type AndroidSignerPlugin as e, type Nip55WebOptions as f, Nip55WebSigner as g, type Nip55WebSupport as h, type Nip55WebSupportReason as i, type Nip55WebTransport as j, type NostrConnectOptions as k, type RelayMismatchInfo as l, type SignerConfig as m, type SignerEvent as n, type StorageAdapter as o, type StoredAccount as p, browserNip55Transport as q, createSigner as r, localStorageAdapter as s, loginWithAndroidSigner as t, normalizeNip55Identifier as u };
package/dist/ui/index.cjs CHANGED
@@ -35,6 +35,9 @@ __export(ui_exports, {
35
35
  });
36
36
  module.exports = __toCommonJS(ui_exports);
37
37
  var import_qrcode = __toESM(require("qrcode"), 1);
38
+ function escapeHtml(text) {
39
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
40
+ }
38
41
  var TABS = [
39
42
  { id: "create", label: "Create" },
40
43
  { id: "ncryptsec", label: "Existing key" },
@@ -45,7 +48,8 @@ var TABS = [
45
48
  { id: "android", label: "Android" }
46
49
  ];
47
50
  function renderLoginHtml(options = {}) {
48
- const includeNip55Web = options.includeNip55Web ?? true;
51
+ const nip55Web = options.nip55Web ?? (options.includeNip55Web === false ? { visible: false } : { visible: true });
52
+ const includeNip55Web = nip55Web.visible;
49
53
  const tabs = TABS.filter((t) => t.id !== "nip55web" || includeNip55Web).map(
50
54
  (t) => `<button class="nostr-signer__tab nostr-signer__tab--${t.id}" type="button" data-tab="${t.id}">${t.label}</button>`
51
55
  ).join("");
@@ -105,6 +109,7 @@ function renderLoginHtml(options = {}) {
105
109
  </section>
106
110
  ${includeNip55Web ? `<section class="nostr-signer__panel nostr-signer__panel--nip55web" data-panel="nip55web" hidden>
107
111
  <p class="nostr-signer__hint">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You&rsquo;ll be asked to approve each signature in the app.</p>
112
+ ${nip55Web.warning ? `<p class="nostr-signer__warn" data-region="nip55web-warning" role="status">${escapeHtml(nip55Web.warning)}</p>` : ""}
108
113
  <button class="nostr-signer__button nostr-signer__button--primary" type="button" data-action="nip55web-login">Open signer app</button>
109
114
  </section>` : ""}
110
115
  <section class="nostr-signer__panel nostr-signer__panel--android" data-panel="android" hidden>
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/ui/index.ts"],"sourcesContent":["import QRCode from 'qrcode';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Signer } from '../core/signer.js';\nimport type { RelayMismatchHandler, StoredAccount } from '../core/types.js';\n\nexport type LoginTab =\n | 'create'\n | 'ncryptsec'\n | 'extension'\n | 'bunker'\n | 'nostrconnect'\n | 'nip55web'\n | 'android';\n\nexport interface LoginUiHandlers {\n onLogin?: (account: StoredAccount) => void;\n onCancel?: () => void;\n onError?: (error: Error) => void;\n /** Optional: passed through to NIP-46 login methods. */\n pool?: AbstractSimplePool;\n /** Optional: passed through to NIP-46 login methods. */\n onRelayMismatch?: RelayMismatchHandler;\n /** Initial tab to show. Defaults to 'create'. */\n defaultTab?: LoginTab;\n /**\n * Optional diagnostic stream for the browser NIP-55 (intent/clipboard)\n * flow. Wire it to an on-screen log when testing on a phone — the flow\n * navigates away, so its behaviour is otherwise hard to observe.\n */\n onNip55WebDebug?: (message: string) => void;\n}\n\nexport interface LoginUiBinding {\n detach(): void;\n selectTab(tab: LoginTab): void;\n}\n\nconst TABS: ReadonlyArray<{ id: LoginTab; label: string }> = [\n { id: 'create', label: 'Create' },\n { id: 'ncryptsec', label: 'Existing key' },\n { id: 'extension', label: 'Extension' },\n { id: 'bunker', label: 'Bunker URI' },\n { id: 'nostrconnect', label: 'Remote (QR)' },\n { id: 'nip55web', label: 'Signer app' },\n { id: 'android', label: 'Android' },\n];\n\nexport interface RenderLoginOptions {\n /**\n * Include the \"Signer app\" tab (browser NIP-55). Defaults to `true`.\n *\n * Pass `signer.supportsNip55Web()` to hide it where it cannot work —\n * notably inside a Capacitor native shell, where\n * `loginWithAndroidSigner` is the right path. Omit it in a native build\n * and the tab would only ever error.\n */\n includeNip55Web?: boolean;\n}\n\nexport function renderLoginHtml(options: RenderLoginOptions = {}): string {\n const includeNip55Web = options.includeNip55Web ?? true;\n const tabs = TABS.filter((t) => t.id !== 'nip55web' || includeNip55Web)\n .map(\n (t) =>\n `<button class=\"nostr-signer__tab nostr-signer__tab--${t.id}\" type=\"button\" data-tab=\"${t.id}\">${t.label}</button>`,\n )\n .join('');\n\n return `<div class=\"nostr-signer__root\">\n <div class=\"nostr-signer__modal\">\n <header class=\"nostr-signer__header\">\n <h2 class=\"nostr-signer__title\">Sign in</h2>\n <button class=\"nostr-signer__close\" type=\"button\" data-action=\"cancel\" aria-label=\"Close\">&times;</button>\n </header>\n <nav class=\"nostr-signer__tabs\" role=\"tablist\">${tabs}</nav>\n <main class=\"nostr-signer__body\">\n <section class=\"nostr-signer__panel nostr-signer__panel--create\" data-panel=\"create\">\n <p class=\"nostr-signer__hint\">Generate a new nsec encrypted with your passphrase (NIP-49). Back up the ncryptsec on the next step — it's the only way back in.</p>\n <form class=\"nostr-signer__form\" data-form=\"create\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-create-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-create-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"new-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Create account</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--ncryptsec\" data-panel=\"ncryptsec\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"ncryptsec\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec\">ncryptsec1...</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--ncryptsec\" id=\"nostr-signer-ncryptsec\" name=\"ncryptsec\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-ncryptsec-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"current-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Sign in</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--extension\" data-panel=\"extension\" hidden>\n <p class=\"nostr-signer__hint\">Requires a NIP-07 browser extension (nos2x, Alby, etc.).</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"extension-login\">Sign in with extension</button>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--bunker\" data-panel=\"bunker\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"bunker\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-uri\">bunker:// URI</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--bunker-uri\" id=\"nostr-signer-bunker-uri\" name=\"uri\" placeholder=\"bunker://&lt;pubkey&gt;?relay=wss://...\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-bunker-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Connect</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--nostrconnect\" data-panel=\"nostrconnect\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"nostrconnect\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-relays\">Relays (comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--relays\" id=\"nostr-signer-nc-relays\" name=\"relays\" placeholder=\"wss://relay.example, wss://other\" required>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-nc-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Generate URI</button>\n </form>\n <div class=\"nostr-signer__qr\" data-region=\"nostrconnect-qr\" hidden>\n <p class=\"nostr-signer__hint\">Scan or paste this into your remote signer:</p>\n <div class=\"nostr-signer__qr-image\" data-region=\"nostrconnect-qr-image\" aria-hidden=\"true\"></div>\n <pre class=\"nostr-signer__qr-uri\" data-region=\"nostrconnect-uri\"></pre>\n <p class=\"nostr-signer__status\" data-region=\"nostrconnect-status\">Waiting for remote signer to pair...</p>\n <button class=\"nostr-signer__button nostr-signer__button--secondary\" type=\"button\" data-action=\"nostrconnect-cancel\">Cancel</button>\n </div>\n </section>\n ${includeNip55Web ? `<section class=\"nostr-signer__panel nostr-signer__panel--nip55web\" data-panel=\"nip55web\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You&rsquo;ll be asked to approve each signature in the app.</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"nip55web-login\">Open signer app</button>\n </section>` : ''}\n <section class=\"nostr-signer__panel nostr-signer__panel--android\" data-panel=\"android\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with an Android external signer app (NIP-55) such as Amber.</p>\n <p class=\"nostr-signer__status\" data-region=\"android-status\">Loading installed signers&hellip;</p>\n <ul class=\"nostr-signer__android-apps\" data-region=\"android-apps\" hidden></ul>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--created\" data-panel=\"created\" hidden>\n <p class=\"nostr-signer__hint\">Account created. Back up this encrypted secret key — without it (and your passphrase) you cannot sign in again.</p>\n <pre class=\"nostr-signer__ncryptsec-display\" data-region=\"created-ncryptsec\"></pre>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"created-ack\">I've backed it up</button>\n </section>\n </main>\n <div class=\"nostr-signer__error\" data-region=\"error\" hidden></div>\n </div>\n</div>`;\n}\n\nexport function attachLoginListeners(\n rootEl: HTMLElement,\n signer: Signer,\n handlers: LoginUiHandlers = {},\n): LoginUiBinding {\n const detachers: Array<() => void> = [];\n let nostrConnectAbort: AbortController | null = null;\n let nip55WebAbort: AbortController | null = null;\n\n const on = <K extends keyof HTMLElementEventMap>(\n el: Element,\n event: K,\n handler: (ev: HTMLElementEventMap[K]) => void,\n ): void => {\n const wrapped = handler as EventListener;\n el.addEventListener(event, wrapped);\n detachers.push(() => el.removeEventListener(event, wrapped));\n };\n\n const q = <T extends Element = Element>(selector: string): T =>\n rootEl.querySelector(selector) as T;\n\n const errorEl = q<HTMLDivElement>('[data-region=\"error\"]');\n const showError = (err: Error | string): void => {\n const msg = typeof err === 'string' ? err : err.message;\n errorEl.hidden = false;\n errorEl.textContent = msg;\n if (typeof err !== 'string') handlers.onError?.(err);\n };\n const clearError = (): void => {\n errorEl.hidden = true;\n errorEl.textContent = '';\n };\n\n // ---- android (NIP-55) — declared before selectTab so it can be invoked\n // when the user opens with defaultTab: 'android'.\n const androidStatus = q<HTMLElement>('[data-region=\"android-status\"]');\n const androidList = q<HTMLUListElement>('[data-region=\"android-apps\"]');\n let androidFetchToken = 0;\n\n const refreshAndroidApps = async (): Promise<void> => {\n const token = ++androidFetchToken;\n androidList.hidden = true;\n androidList.innerHTML = '';\n androidStatus.hidden = false;\n androidStatus.textContent = 'Loading installed signers…';\n try {\n const apps = await signer.listAndroidSignerApps();\n if (token !== androidFetchToken) return;\n if (apps.length === 0) {\n androidStatus.textContent =\n 'No NIP-55 signer apps installed. Install Amber (or similar) and try again.';\n return;\n }\n androidStatus.hidden = true;\n for (const app of apps) {\n const li = document.createElement('li');\n li.className = 'nostr-signer__android-app';\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'nostr-signer__button nostr-signer__button--primary';\n btn.dataset.packageName = app.packageName;\n btn.textContent = `${app.name} (${app.packageName})`;\n on(btn, 'click', async () => {\n clearError();\n btn.disabled = true;\n try {\n const account = await signer.loginWithAndroidSigner({\n packageName: app.packageName,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n showError(err as Error);\n }\n });\n li.appendChild(btn);\n androidList.appendChild(li);\n }\n androidList.hidden = false;\n } catch (err) {\n if (token !== androidFetchToken) return;\n androidStatus.textContent = (err as Error).message;\n }\n };\n\n const selectTab = (tab: LoginTab): void => {\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {\n panel.hidden = panel.dataset.panel !== tab;\n });\n rootEl.querySelectorAll<HTMLElement>('[data-tab]').forEach((btn) => {\n btn.classList.toggle('nostr-signer__tab--active', btn.dataset.tab === tab);\n });\n clearError();\n if (tab === 'android') void refreshAndroidApps();\n };\n\n selectTab(handlers.defaultTab ?? 'create');\n\n rootEl.querySelectorAll<HTMLButtonElement>('[data-tab]').forEach((btn) => {\n on(btn, 'click', () => selectTab(btn.dataset.tab as LoginTab));\n });\n\n on(q('[data-action=\"cancel\"]'), 'click', () => handlers.onCancel?.());\n\n // ---- create ----\n const createForm = q<HTMLFormElement>('[data-form=\"create\"]');\n on(createForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const passphrase = (createForm.elements.namedItem('passphrase') as HTMLInputElement).value;\n try {\n const { ncryptsec } = await signer.createAccount(passphrase);\n q<HTMLElement>('[data-region=\"created-ncryptsec\"]').textContent = ncryptsec;\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((p) => {\n p.hidden = p.dataset.panel !== 'created';\n });\n } catch (err) {\n showError(err as Error);\n }\n });\n\n on(q('[data-action=\"created-ack\"]'), 'click', () => {\n const active = signer.getActiveAccount();\n if (active) handlers.onLogin?.(active);\n });\n\n // ---- ncryptsec ----\n const ncryptsecForm = q<HTMLFormElement>('[data-form=\"ncryptsec\"]');\n on(ncryptsecForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const ncryptsec = (\n ncryptsecForm.elements.namedItem('ncryptsec') as HTMLTextAreaElement\n ).value.trim();\n const passphrase = (\n ncryptsecForm.elements.namedItem('passphrase') as HTMLInputElement\n ).value;\n try {\n const account = await signer.loginWithNcryptsec(ncryptsec, passphrase);\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- extension ----\n on(q('[data-action=\"extension-login\"]'), 'click', async () => {\n clearError();\n try {\n const account = await signer.loginWithExtension();\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- signer app (NIP-55 over the web) ----\n // Absent when renderLoginHtml({ includeNip55Web: false }) omitted the tab.\n const nip55WebBtn = rootEl.querySelector<HTMLButtonElement>(\n '[data-action=\"nip55web-login\"]',\n );\n if (nip55WebBtn) on(nip55WebBtn, 'click', async () => {\n clearError();\n const btn = nip55WebBtn;\n btn.disabled = true;\n nip55WebAbort = new AbortController();\n try {\n const account = await signer.loginWithNip55Web({\n signal: nip55WebAbort.signal,\n debug: handlers.onNip55WebDebug,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n // A user cancelling (detach) is not an error worth surfacing.\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nip55WebAbort = null;\n }\n });\n\n // ---- bunker URI ----\n const bunkerForm = q<HTMLFormElement>('[data-form=\"bunker\"]');\n on(bunkerForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const uri = (\n bunkerForm.elements.namedItem('uri') as HTMLTextAreaElement\n ).value.trim();\n const perms = (bunkerForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n try {\n const account = await signer.loginWithBunkerUri(uri, {\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n perms: perms.length > 0 ? perms : undefined,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- nostrconnect ----\n const ncForm = q<HTMLFormElement>('[data-form=\"nostrconnect\"]');\n const ncQrRegion = q<HTMLDivElement>('[data-region=\"nostrconnect-qr\"]');\n const ncQrUri = q<HTMLElement>('[data-region=\"nostrconnect-uri\"]');\n const ncQrImage = q<HTMLDivElement>('[data-region=\"nostrconnect-qr-image\"]');\n const ncStatus = q<HTMLElement>('[data-region=\"nostrconnect-status\"]');\n let ncQrToken = 0;\n\n const resetNostrConnect = (): void => {\n ncForm.hidden = false;\n ncQrRegion.hidden = true;\n ncQrUri.textContent = '';\n ncQrImage.innerHTML = '';\n ncStatus.textContent = 'Waiting for remote signer to pair...';\n ncQrToken++;\n };\n\n const renderNostrConnectQr = async (uri: string): Promise<void> => {\n const token = ++ncQrToken;\n try {\n const svg = await QRCode.toString(uri, {\n type: 'svg',\n margin: 1,\n errorCorrectionLevel: 'M',\n });\n if (token === ncQrToken) ncQrImage.innerHTML = svg;\n } catch {\n // QR rendering is a nice-to-have — the URI text remains usable.\n }\n };\n\n on(ncForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const relays = (ncForm.elements.namedItem('relays') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (relays.length === 0) {\n showError('At least one relay is required.');\n return;\n }\n const perms = (ncForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n ncForm.hidden = true;\n ncQrRegion.hidden = false;\n nostrConnectAbort = new AbortController();\n try {\n const account = await signer.loginWithNostrConnect({\n relays,\n perms: perms.length > 0 ? perms : undefined,\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n signal: nostrConnectAbort.signal,\n onUri: (uri) => {\n ncQrUri.textContent = uri;\n void renderNostrConnectQr(uri);\n },\n });\n handlers.onLogin?.(account);\n } catch (err) {\n resetNostrConnect();\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nostrConnectAbort = null;\n }\n });\n\n on(q('[data-action=\"nostrconnect-cancel\"]'), 'click', () => {\n nostrConnectAbort?.abort();\n resetNostrConnect();\n });\n\n return {\n detach: () => {\n nostrConnectAbort?.abort();\n nip55WebAbort?.abort();\n for (const d of detachers) d();\n },\n selectTab,\n };\n}\n"],"mappings":"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","names":["QRCode"]}
1
+ {"version":3,"sources":["../../src/ui/index.ts"],"sourcesContent":["import QRCode from 'qrcode';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Signer } from '../core/signer.js';\nimport type { Nip55WebSupport } from '../nip55Web.js';\nimport type { RelayMismatchHandler, StoredAccount } from '../core/types.js';\n\n/** Escape text before interpolating it into the HTML template. */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\nexport type LoginTab =\n | 'create'\n | 'ncryptsec'\n | 'extension'\n | 'bunker'\n | 'nostrconnect'\n | 'nip55web'\n | 'android';\n\nexport interface LoginUiHandlers {\n onLogin?: (account: StoredAccount) => void;\n onCancel?: () => void;\n onError?: (error: Error) => void;\n /** Optional: passed through to NIP-46 login methods. */\n pool?: AbstractSimplePool;\n /** Optional: passed through to NIP-46 login methods. */\n onRelayMismatch?: RelayMismatchHandler;\n /** Initial tab to show. Defaults to 'create'. */\n defaultTab?: LoginTab;\n /**\n * Optional diagnostic stream for the browser NIP-55 (intent/clipboard)\n * flow. Wire it to an on-screen log when testing on a phone — the flow\n * navigates away, so its behaviour is otherwise hard to observe.\n */\n onNip55WebDebug?: (message: string) => void;\n}\n\nexport interface LoginUiBinding {\n detach(): void;\n selectTab(tab: LoginTab): void;\n}\n\nconst TABS: ReadonlyArray<{ id: LoginTab; label: string }> = [\n { id: 'create', label: 'Create' },\n { id: 'ncryptsec', label: 'Existing key' },\n { id: 'extension', label: 'Extension' },\n { id: 'bunker', label: 'Bunker URI' },\n { id: 'nostrconnect', label: 'Remote (QR)' },\n { id: 'nip55web', label: 'Signer app' },\n { id: 'android', label: 'Android' },\n];\n\nexport interface RenderLoginOptions {\n /**\n * The browser NIP-55 (\"Signer app\") status, from\n * `signer.nip55WebSupport()`. `visible` controls whether the tab is\n * rendered; `warning` renders an advisory note above the button without\n * disabling it.\n *\n * Defaults to shown without a warning. Pass the real status so a native\n * build hides the tab and Firefox for Android shows its warning.\n */\n nip55Web?: Nip55WebSupport;\n /**\n * Include the \"Signer app\" tab (browser NIP-55). Shorthand for the\n * `visible` half of {@link nip55Web}; defaults to `true`. Ignored when\n * `nip55Web` is supplied.\n *\n * @deprecated Prefer `nip55Web`, which can also carry a warning.\n */\n includeNip55Web?: boolean;\n}\n\nexport function renderLoginHtml(options: RenderLoginOptions = {}): string {\n const nip55Web: Nip55WebSupport =\n options.nip55Web ??\n (options.includeNip55Web === false\n ? { visible: false }\n : { visible: true });\n const includeNip55Web = nip55Web.visible;\n const tabs = TABS.filter((t) => t.id !== 'nip55web' || includeNip55Web)\n .map(\n (t) =>\n `<button class=\"nostr-signer__tab nostr-signer__tab--${t.id}\" type=\"button\" data-tab=\"${t.id}\">${t.label}</button>`,\n )\n .join('');\n\n return `<div class=\"nostr-signer__root\">\n <div class=\"nostr-signer__modal\">\n <header class=\"nostr-signer__header\">\n <h2 class=\"nostr-signer__title\">Sign in</h2>\n <button class=\"nostr-signer__close\" type=\"button\" data-action=\"cancel\" aria-label=\"Close\">&times;</button>\n </header>\n <nav class=\"nostr-signer__tabs\" role=\"tablist\">${tabs}</nav>\n <main class=\"nostr-signer__body\">\n <section class=\"nostr-signer__panel nostr-signer__panel--create\" data-panel=\"create\">\n <p class=\"nostr-signer__hint\">Generate a new nsec encrypted with your passphrase (NIP-49). Back up the ncryptsec on the next step — it's the only way back in.</p>\n <form class=\"nostr-signer__form\" data-form=\"create\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-create-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-create-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"new-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Create account</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--ncryptsec\" data-panel=\"ncryptsec\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"ncryptsec\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec\">ncryptsec1...</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--ncryptsec\" id=\"nostr-signer-ncryptsec\" name=\"ncryptsec\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-ncryptsec-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"current-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Sign in</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--extension\" data-panel=\"extension\" hidden>\n <p class=\"nostr-signer__hint\">Requires a NIP-07 browser extension (nos2x, Alby, etc.).</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"extension-login\">Sign in with extension</button>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--bunker\" data-panel=\"bunker\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"bunker\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-uri\">bunker:// URI</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--bunker-uri\" id=\"nostr-signer-bunker-uri\" name=\"uri\" placeholder=\"bunker://&lt;pubkey&gt;?relay=wss://...\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-bunker-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Connect</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--nostrconnect\" data-panel=\"nostrconnect\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"nostrconnect\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-relays\">Relays (comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--relays\" id=\"nostr-signer-nc-relays\" name=\"relays\" placeholder=\"wss://relay.example, wss://other\" required>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-nc-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Generate URI</button>\n </form>\n <div class=\"nostr-signer__qr\" data-region=\"nostrconnect-qr\" hidden>\n <p class=\"nostr-signer__hint\">Scan or paste this into your remote signer:</p>\n <div class=\"nostr-signer__qr-image\" data-region=\"nostrconnect-qr-image\" aria-hidden=\"true\"></div>\n <pre class=\"nostr-signer__qr-uri\" data-region=\"nostrconnect-uri\"></pre>\n <p class=\"nostr-signer__status\" data-region=\"nostrconnect-status\">Waiting for remote signer to pair...</p>\n <button class=\"nostr-signer__button nostr-signer__button--secondary\" type=\"button\" data-action=\"nostrconnect-cancel\">Cancel</button>\n </div>\n </section>\n ${includeNip55Web ? `<section class=\"nostr-signer__panel nostr-signer__panel--nip55web\" data-panel=\"nip55web\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You&rsquo;ll be asked to approve each signature in the app.</p>\n ${nip55Web.warning ? `<p class=\"nostr-signer__warn\" data-region=\"nip55web-warning\" role=\"status\">${escapeHtml(nip55Web.warning)}</p>` : ''}\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"nip55web-login\">Open signer app</button>\n </section>` : ''}\n <section class=\"nostr-signer__panel nostr-signer__panel--android\" data-panel=\"android\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with an Android external signer app (NIP-55) such as Amber.</p>\n <p class=\"nostr-signer__status\" data-region=\"android-status\">Loading installed signers&hellip;</p>\n <ul class=\"nostr-signer__android-apps\" data-region=\"android-apps\" hidden></ul>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--created\" data-panel=\"created\" hidden>\n <p class=\"nostr-signer__hint\">Account created. Back up this encrypted secret key — without it (and your passphrase) you cannot sign in again.</p>\n <pre class=\"nostr-signer__ncryptsec-display\" data-region=\"created-ncryptsec\"></pre>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"created-ack\">I've backed it up</button>\n </section>\n </main>\n <div class=\"nostr-signer__error\" data-region=\"error\" hidden></div>\n </div>\n</div>`;\n}\n\nexport function attachLoginListeners(\n rootEl: HTMLElement,\n signer: Signer,\n handlers: LoginUiHandlers = {},\n): LoginUiBinding {\n const detachers: Array<() => void> = [];\n let nostrConnectAbort: AbortController | null = null;\n let nip55WebAbort: AbortController | null = null;\n\n const on = <K extends keyof HTMLElementEventMap>(\n el: Element,\n event: K,\n handler: (ev: HTMLElementEventMap[K]) => void,\n ): void => {\n const wrapped = handler as EventListener;\n el.addEventListener(event, wrapped);\n detachers.push(() => el.removeEventListener(event, wrapped));\n };\n\n const q = <T extends Element = Element>(selector: string): T =>\n rootEl.querySelector(selector) as T;\n\n const errorEl = q<HTMLDivElement>('[data-region=\"error\"]');\n const showError = (err: Error | string): void => {\n const msg = typeof err === 'string' ? err : err.message;\n errorEl.hidden = false;\n errorEl.textContent = msg;\n if (typeof err !== 'string') handlers.onError?.(err);\n };\n const clearError = (): void => {\n errorEl.hidden = true;\n errorEl.textContent = '';\n };\n\n // ---- android (NIP-55) — declared before selectTab so it can be invoked\n // when the user opens with defaultTab: 'android'.\n const androidStatus = q<HTMLElement>('[data-region=\"android-status\"]');\n const androidList = q<HTMLUListElement>('[data-region=\"android-apps\"]');\n let androidFetchToken = 0;\n\n const refreshAndroidApps = async (): Promise<void> => {\n const token = ++androidFetchToken;\n androidList.hidden = true;\n androidList.innerHTML = '';\n androidStatus.hidden = false;\n androidStatus.textContent = 'Loading installed signers…';\n try {\n const apps = await signer.listAndroidSignerApps();\n if (token !== androidFetchToken) return;\n if (apps.length === 0) {\n androidStatus.textContent =\n 'No NIP-55 signer apps installed. Install Amber (or similar) and try again.';\n return;\n }\n androidStatus.hidden = true;\n for (const app of apps) {\n const li = document.createElement('li');\n li.className = 'nostr-signer__android-app';\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'nostr-signer__button nostr-signer__button--primary';\n btn.dataset.packageName = app.packageName;\n btn.textContent = `${app.name} (${app.packageName})`;\n on(btn, 'click', async () => {\n clearError();\n btn.disabled = true;\n try {\n const account = await signer.loginWithAndroidSigner({\n packageName: app.packageName,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n showError(err as Error);\n }\n });\n li.appendChild(btn);\n androidList.appendChild(li);\n }\n androidList.hidden = false;\n } catch (err) {\n if (token !== androidFetchToken) return;\n androidStatus.textContent = (err as Error).message;\n }\n };\n\n const selectTab = (tab: LoginTab): void => {\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {\n panel.hidden = panel.dataset.panel !== tab;\n });\n rootEl.querySelectorAll<HTMLElement>('[data-tab]').forEach((btn) => {\n btn.classList.toggle('nostr-signer__tab--active', btn.dataset.tab === tab);\n });\n clearError();\n if (tab === 'android') void refreshAndroidApps();\n };\n\n selectTab(handlers.defaultTab ?? 'create');\n\n rootEl.querySelectorAll<HTMLButtonElement>('[data-tab]').forEach((btn) => {\n on(btn, 'click', () => selectTab(btn.dataset.tab as LoginTab));\n });\n\n on(q('[data-action=\"cancel\"]'), 'click', () => handlers.onCancel?.());\n\n // ---- create ----\n const createForm = q<HTMLFormElement>('[data-form=\"create\"]');\n on(createForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const passphrase = (createForm.elements.namedItem('passphrase') as HTMLInputElement).value;\n try {\n const { ncryptsec } = await signer.createAccount(passphrase);\n q<HTMLElement>('[data-region=\"created-ncryptsec\"]').textContent = ncryptsec;\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((p) => {\n p.hidden = p.dataset.panel !== 'created';\n });\n } catch (err) {\n showError(err as Error);\n }\n });\n\n on(q('[data-action=\"created-ack\"]'), 'click', () => {\n const active = signer.getActiveAccount();\n if (active) handlers.onLogin?.(active);\n });\n\n // ---- ncryptsec ----\n const ncryptsecForm = q<HTMLFormElement>('[data-form=\"ncryptsec\"]');\n on(ncryptsecForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const ncryptsec = (\n ncryptsecForm.elements.namedItem('ncryptsec') as HTMLTextAreaElement\n ).value.trim();\n const passphrase = (\n ncryptsecForm.elements.namedItem('passphrase') as HTMLInputElement\n ).value;\n try {\n const account = await signer.loginWithNcryptsec(ncryptsec, passphrase);\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- extension ----\n on(q('[data-action=\"extension-login\"]'), 'click', async () => {\n clearError();\n try {\n const account = await signer.loginWithExtension();\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- signer app (NIP-55 over the web) ----\n // Absent when renderLoginHtml({ includeNip55Web: false }) omitted the tab.\n const nip55WebBtn = rootEl.querySelector<HTMLButtonElement>(\n '[data-action=\"nip55web-login\"]',\n );\n if (nip55WebBtn) on(nip55WebBtn, 'click', async () => {\n clearError();\n const btn = nip55WebBtn;\n btn.disabled = true;\n nip55WebAbort = new AbortController();\n try {\n const account = await signer.loginWithNip55Web({\n signal: nip55WebAbort.signal,\n debug: handlers.onNip55WebDebug,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n // A user cancelling (detach) is not an error worth surfacing.\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nip55WebAbort = null;\n }\n });\n\n // ---- bunker URI ----\n const bunkerForm = q<HTMLFormElement>('[data-form=\"bunker\"]');\n on(bunkerForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const uri = (\n bunkerForm.elements.namedItem('uri') as HTMLTextAreaElement\n ).value.trim();\n const perms = (bunkerForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n try {\n const account = await signer.loginWithBunkerUri(uri, {\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n perms: perms.length > 0 ? perms : undefined,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- nostrconnect ----\n const ncForm = q<HTMLFormElement>('[data-form=\"nostrconnect\"]');\n const ncQrRegion = q<HTMLDivElement>('[data-region=\"nostrconnect-qr\"]');\n const ncQrUri = q<HTMLElement>('[data-region=\"nostrconnect-uri\"]');\n const ncQrImage = q<HTMLDivElement>('[data-region=\"nostrconnect-qr-image\"]');\n const ncStatus = q<HTMLElement>('[data-region=\"nostrconnect-status\"]');\n let ncQrToken = 0;\n\n const resetNostrConnect = (): void => {\n ncForm.hidden = false;\n ncQrRegion.hidden = true;\n ncQrUri.textContent = '';\n ncQrImage.innerHTML = '';\n ncStatus.textContent = 'Waiting for remote signer to pair...';\n ncQrToken++;\n };\n\n const renderNostrConnectQr = async (uri: string): Promise<void> => {\n const token = ++ncQrToken;\n try {\n const svg = await QRCode.toString(uri, {\n type: 'svg',\n margin: 1,\n errorCorrectionLevel: 'M',\n });\n if (token === ncQrToken) ncQrImage.innerHTML = svg;\n } catch {\n // QR rendering is a nice-to-have — the URI text remains usable.\n }\n };\n\n on(ncForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const relays = (ncForm.elements.namedItem('relays') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (relays.length === 0) {\n showError('At least one relay is required.');\n return;\n }\n const perms = (ncForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n ncForm.hidden = true;\n ncQrRegion.hidden = false;\n nostrConnectAbort = new AbortController();\n try {\n const account = await signer.loginWithNostrConnect({\n relays,\n perms: perms.length > 0 ? perms : undefined,\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n signal: nostrConnectAbort.signal,\n onUri: (uri) => {\n ncQrUri.textContent = uri;\n void renderNostrConnectQr(uri);\n },\n });\n handlers.onLogin?.(account);\n } catch (err) {\n resetNostrConnect();\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nostrConnectAbort = null;\n }\n });\n\n on(q('[data-action=\"nostrconnect-cancel\"]'), 'click', () => {\n nostrConnectAbort?.abort();\n resetNostrConnect();\n });\n\n return {\n detach: () => {\n nostrConnectAbort?.abort();\n nip55WebAbort?.abort();\n for (const d of detachers) d();\n },\n selectTab,\n };\n}\n"],"mappings":"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","names":["QRCode"]}
@@ -1,5 +1,5 @@
1
1
  import { AbstractSimplePool } from 'nostr-tools/abstract-pool';
2
- import { n as StoredAccount, R as RelayMismatchHandler, S as Signer } from '../signer-oU50RBqS.cjs';
2
+ import { p as StoredAccount, R as RelayMismatchHandler, h as Nip55WebSupport, S as Signer } from '../signer-CQyqBYxq.cjs';
3
3
  import 'nostr-tools';
4
4
 
5
5
  type LoginTab = 'create' | 'ncryptsec' | 'extension' | 'bunker' | 'nostrconnect' | 'nip55web' | 'android';
@@ -26,12 +26,21 @@ interface LoginUiBinding {
26
26
  }
27
27
  interface RenderLoginOptions {
28
28
  /**
29
- * Include the "Signer app" tab (browser NIP-55). Defaults to `true`.
29
+ * The browser NIP-55 ("Signer app") status, from
30
+ * `signer.nip55WebSupport()`. `visible` controls whether the tab is
31
+ * rendered; `warning` renders an advisory note above the button without
32
+ * disabling it.
30
33
  *
31
- * Pass `signer.supportsNip55Web()` to hide it where it cannot work —
32
- * notably inside a Capacitor native shell, where
33
- * `loginWithAndroidSigner` is the right path. Omit it in a native build
34
- * and the tab would only ever error.
34
+ * Defaults to shown without a warning. Pass the real status so a native
35
+ * build hides the tab and Firefox for Android shows its warning.
36
+ */
37
+ nip55Web?: Nip55WebSupport;
38
+ /**
39
+ * Include the "Signer app" tab (browser NIP-55). Shorthand for the
40
+ * `visible` half of {@link nip55Web}; defaults to `true`. Ignored when
41
+ * `nip55Web` is supplied.
42
+ *
43
+ * @deprecated Prefer `nip55Web`, which can also carry a warning.
35
44
  */
36
45
  includeNip55Web?: boolean;
37
46
  }
@@ -1,5 +1,5 @@
1
1
  import { AbstractSimplePool } from 'nostr-tools/abstract-pool';
2
- import { n as StoredAccount, R as RelayMismatchHandler, S as Signer } from '../signer-oU50RBqS.js';
2
+ import { p as StoredAccount, R as RelayMismatchHandler, h as Nip55WebSupport, S as Signer } from '../signer-CQyqBYxq.js';
3
3
  import 'nostr-tools';
4
4
 
5
5
  type LoginTab = 'create' | 'ncryptsec' | 'extension' | 'bunker' | 'nostrconnect' | 'nip55web' | 'android';
@@ -26,12 +26,21 @@ interface LoginUiBinding {
26
26
  }
27
27
  interface RenderLoginOptions {
28
28
  /**
29
- * Include the "Signer app" tab (browser NIP-55). Defaults to `true`.
29
+ * The browser NIP-55 ("Signer app") status, from
30
+ * `signer.nip55WebSupport()`. `visible` controls whether the tab is
31
+ * rendered; `warning` renders an advisory note above the button without
32
+ * disabling it.
30
33
  *
31
- * Pass `signer.supportsNip55Web()` to hide it where it cannot work —
32
- * notably inside a Capacitor native shell, where
33
- * `loginWithAndroidSigner` is the right path. Omit it in a native build
34
- * and the tab would only ever error.
34
+ * Defaults to shown without a warning. Pass the real status so a native
35
+ * build hides the tab and Firefox for Android shows its warning.
36
+ */
37
+ nip55Web?: Nip55WebSupport;
38
+ /**
39
+ * Include the "Signer app" tab (browser NIP-55). Shorthand for the
40
+ * `visible` half of {@link nip55Web}; defaults to `true`. Ignored when
41
+ * `nip55Web` is supplied.
42
+ *
43
+ * @deprecated Prefer `nip55Web`, which can also carry a warning.
35
44
  */
36
45
  includeNip55Web?: boolean;
37
46
  }
package/dist/ui/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  // src/ui/index.ts
2
2
  import QRCode from "qrcode";
3
+ function escapeHtml(text) {
4
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
5
+ }
3
6
  var TABS = [
4
7
  { id: "create", label: "Create" },
5
8
  { id: "ncryptsec", label: "Existing key" },
@@ -10,7 +13,8 @@ var TABS = [
10
13
  { id: "android", label: "Android" }
11
14
  ];
12
15
  function renderLoginHtml(options = {}) {
13
- const includeNip55Web = options.includeNip55Web ?? true;
16
+ const nip55Web = options.nip55Web ?? (options.includeNip55Web === false ? { visible: false } : { visible: true });
17
+ const includeNip55Web = nip55Web.visible;
14
18
  const tabs = TABS.filter((t) => t.id !== "nip55web" || includeNip55Web).map(
15
19
  (t) => `<button class="nostr-signer__tab nostr-signer__tab--${t.id}" type="button" data-tab="${t.id}">${t.label}</button>`
16
20
  ).join("");
@@ -70,6 +74,7 @@ function renderLoginHtml(options = {}) {
70
74
  </section>
71
75
  ${includeNip55Web ? `<section class="nostr-signer__panel nostr-signer__panel--nip55web" data-panel="nip55web" hidden>
72
76
  <p class="nostr-signer__hint">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You&rsquo;ll be asked to approve each signature in the app.</p>
77
+ ${nip55Web.warning ? `<p class="nostr-signer__warn" data-region="nip55web-warning" role="status">${escapeHtml(nip55Web.warning)}</p>` : ""}
73
78
  <button class="nostr-signer__button nostr-signer__button--primary" type="button" data-action="nip55web-login">Open signer app</button>
74
79
  </section>` : ""}
75
80
  <section class="nostr-signer__panel nostr-signer__panel--android" data-panel="android" hidden>
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/ui/index.ts"],"sourcesContent":["import QRCode from 'qrcode';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Signer } from '../core/signer.js';\nimport type { RelayMismatchHandler, StoredAccount } from '../core/types.js';\n\nexport type LoginTab =\n | 'create'\n | 'ncryptsec'\n | 'extension'\n | 'bunker'\n | 'nostrconnect'\n | 'nip55web'\n | 'android';\n\nexport interface LoginUiHandlers {\n onLogin?: (account: StoredAccount) => void;\n onCancel?: () => void;\n onError?: (error: Error) => void;\n /** Optional: passed through to NIP-46 login methods. */\n pool?: AbstractSimplePool;\n /** Optional: passed through to NIP-46 login methods. */\n onRelayMismatch?: RelayMismatchHandler;\n /** Initial tab to show. Defaults to 'create'. */\n defaultTab?: LoginTab;\n /**\n * Optional diagnostic stream for the browser NIP-55 (intent/clipboard)\n * flow. Wire it to an on-screen log when testing on a phone — the flow\n * navigates away, so its behaviour is otherwise hard to observe.\n */\n onNip55WebDebug?: (message: string) => void;\n}\n\nexport interface LoginUiBinding {\n detach(): void;\n selectTab(tab: LoginTab): void;\n}\n\nconst TABS: ReadonlyArray<{ id: LoginTab; label: string }> = [\n { id: 'create', label: 'Create' },\n { id: 'ncryptsec', label: 'Existing key' },\n { id: 'extension', label: 'Extension' },\n { id: 'bunker', label: 'Bunker URI' },\n { id: 'nostrconnect', label: 'Remote (QR)' },\n { id: 'nip55web', label: 'Signer app' },\n { id: 'android', label: 'Android' },\n];\n\nexport interface RenderLoginOptions {\n /**\n * Include the \"Signer app\" tab (browser NIP-55). Defaults to `true`.\n *\n * Pass `signer.supportsNip55Web()` to hide it where it cannot work —\n * notably inside a Capacitor native shell, where\n * `loginWithAndroidSigner` is the right path. Omit it in a native build\n * and the tab would only ever error.\n */\n includeNip55Web?: boolean;\n}\n\nexport function renderLoginHtml(options: RenderLoginOptions = {}): string {\n const includeNip55Web = options.includeNip55Web ?? true;\n const tabs = TABS.filter((t) => t.id !== 'nip55web' || includeNip55Web)\n .map(\n (t) =>\n `<button class=\"nostr-signer__tab nostr-signer__tab--${t.id}\" type=\"button\" data-tab=\"${t.id}\">${t.label}</button>`,\n )\n .join('');\n\n return `<div class=\"nostr-signer__root\">\n <div class=\"nostr-signer__modal\">\n <header class=\"nostr-signer__header\">\n <h2 class=\"nostr-signer__title\">Sign in</h2>\n <button class=\"nostr-signer__close\" type=\"button\" data-action=\"cancel\" aria-label=\"Close\">&times;</button>\n </header>\n <nav class=\"nostr-signer__tabs\" role=\"tablist\">${tabs}</nav>\n <main class=\"nostr-signer__body\">\n <section class=\"nostr-signer__panel nostr-signer__panel--create\" data-panel=\"create\">\n <p class=\"nostr-signer__hint\">Generate a new nsec encrypted with your passphrase (NIP-49). Back up the ncryptsec on the next step — it's the only way back in.</p>\n <form class=\"nostr-signer__form\" data-form=\"create\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-create-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-create-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"new-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Create account</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--ncryptsec\" data-panel=\"ncryptsec\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"ncryptsec\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec\">ncryptsec1...</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--ncryptsec\" id=\"nostr-signer-ncryptsec\" name=\"ncryptsec\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-ncryptsec-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"current-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Sign in</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--extension\" data-panel=\"extension\" hidden>\n <p class=\"nostr-signer__hint\">Requires a NIP-07 browser extension (nos2x, Alby, etc.).</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"extension-login\">Sign in with extension</button>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--bunker\" data-panel=\"bunker\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"bunker\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-uri\">bunker:// URI</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--bunker-uri\" id=\"nostr-signer-bunker-uri\" name=\"uri\" placeholder=\"bunker://&lt;pubkey&gt;?relay=wss://...\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-bunker-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Connect</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--nostrconnect\" data-panel=\"nostrconnect\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"nostrconnect\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-relays\">Relays (comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--relays\" id=\"nostr-signer-nc-relays\" name=\"relays\" placeholder=\"wss://relay.example, wss://other\" required>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-nc-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Generate URI</button>\n </form>\n <div class=\"nostr-signer__qr\" data-region=\"nostrconnect-qr\" hidden>\n <p class=\"nostr-signer__hint\">Scan or paste this into your remote signer:</p>\n <div class=\"nostr-signer__qr-image\" data-region=\"nostrconnect-qr-image\" aria-hidden=\"true\"></div>\n <pre class=\"nostr-signer__qr-uri\" data-region=\"nostrconnect-uri\"></pre>\n <p class=\"nostr-signer__status\" data-region=\"nostrconnect-status\">Waiting for remote signer to pair...</p>\n <button class=\"nostr-signer__button nostr-signer__button--secondary\" type=\"button\" data-action=\"nostrconnect-cancel\">Cancel</button>\n </div>\n </section>\n ${includeNip55Web ? `<section class=\"nostr-signer__panel nostr-signer__panel--nip55web\" data-panel=\"nip55web\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You&rsquo;ll be asked to approve each signature in the app.</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"nip55web-login\">Open signer app</button>\n </section>` : ''}\n <section class=\"nostr-signer__panel nostr-signer__panel--android\" data-panel=\"android\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with an Android external signer app (NIP-55) such as Amber.</p>\n <p class=\"nostr-signer__status\" data-region=\"android-status\">Loading installed signers&hellip;</p>\n <ul class=\"nostr-signer__android-apps\" data-region=\"android-apps\" hidden></ul>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--created\" data-panel=\"created\" hidden>\n <p class=\"nostr-signer__hint\">Account created. Back up this encrypted secret key — without it (and your passphrase) you cannot sign in again.</p>\n <pre class=\"nostr-signer__ncryptsec-display\" data-region=\"created-ncryptsec\"></pre>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"created-ack\">I've backed it up</button>\n </section>\n </main>\n <div class=\"nostr-signer__error\" data-region=\"error\" hidden></div>\n </div>\n</div>`;\n}\n\nexport function attachLoginListeners(\n rootEl: HTMLElement,\n signer: Signer,\n handlers: LoginUiHandlers = {},\n): LoginUiBinding {\n const detachers: Array<() => void> = [];\n let nostrConnectAbort: AbortController | null = null;\n let nip55WebAbort: AbortController | null = null;\n\n const on = <K extends keyof HTMLElementEventMap>(\n el: Element,\n event: K,\n handler: (ev: HTMLElementEventMap[K]) => void,\n ): void => {\n const wrapped = handler as EventListener;\n el.addEventListener(event, wrapped);\n detachers.push(() => el.removeEventListener(event, wrapped));\n };\n\n const q = <T extends Element = Element>(selector: string): T =>\n rootEl.querySelector(selector) as T;\n\n const errorEl = q<HTMLDivElement>('[data-region=\"error\"]');\n const showError = (err: Error | string): void => {\n const msg = typeof err === 'string' ? err : err.message;\n errorEl.hidden = false;\n errorEl.textContent = msg;\n if (typeof err !== 'string') handlers.onError?.(err);\n };\n const clearError = (): void => {\n errorEl.hidden = true;\n errorEl.textContent = '';\n };\n\n // ---- android (NIP-55) — declared before selectTab so it can be invoked\n // when the user opens with defaultTab: 'android'.\n const androidStatus = q<HTMLElement>('[data-region=\"android-status\"]');\n const androidList = q<HTMLUListElement>('[data-region=\"android-apps\"]');\n let androidFetchToken = 0;\n\n const refreshAndroidApps = async (): Promise<void> => {\n const token = ++androidFetchToken;\n androidList.hidden = true;\n androidList.innerHTML = '';\n androidStatus.hidden = false;\n androidStatus.textContent = 'Loading installed signers…';\n try {\n const apps = await signer.listAndroidSignerApps();\n if (token !== androidFetchToken) return;\n if (apps.length === 0) {\n androidStatus.textContent =\n 'No NIP-55 signer apps installed. Install Amber (or similar) and try again.';\n return;\n }\n androidStatus.hidden = true;\n for (const app of apps) {\n const li = document.createElement('li');\n li.className = 'nostr-signer__android-app';\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'nostr-signer__button nostr-signer__button--primary';\n btn.dataset.packageName = app.packageName;\n btn.textContent = `${app.name} (${app.packageName})`;\n on(btn, 'click', async () => {\n clearError();\n btn.disabled = true;\n try {\n const account = await signer.loginWithAndroidSigner({\n packageName: app.packageName,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n showError(err as Error);\n }\n });\n li.appendChild(btn);\n androidList.appendChild(li);\n }\n androidList.hidden = false;\n } catch (err) {\n if (token !== androidFetchToken) return;\n androidStatus.textContent = (err as Error).message;\n }\n };\n\n const selectTab = (tab: LoginTab): void => {\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {\n panel.hidden = panel.dataset.panel !== tab;\n });\n rootEl.querySelectorAll<HTMLElement>('[data-tab]').forEach((btn) => {\n btn.classList.toggle('nostr-signer__tab--active', btn.dataset.tab === tab);\n });\n clearError();\n if (tab === 'android') void refreshAndroidApps();\n };\n\n selectTab(handlers.defaultTab ?? 'create');\n\n rootEl.querySelectorAll<HTMLButtonElement>('[data-tab]').forEach((btn) => {\n on(btn, 'click', () => selectTab(btn.dataset.tab as LoginTab));\n });\n\n on(q('[data-action=\"cancel\"]'), 'click', () => handlers.onCancel?.());\n\n // ---- create ----\n const createForm = q<HTMLFormElement>('[data-form=\"create\"]');\n on(createForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const passphrase = (createForm.elements.namedItem('passphrase') as HTMLInputElement).value;\n try {\n const { ncryptsec } = await signer.createAccount(passphrase);\n q<HTMLElement>('[data-region=\"created-ncryptsec\"]').textContent = ncryptsec;\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((p) => {\n p.hidden = p.dataset.panel !== 'created';\n });\n } catch (err) {\n showError(err as Error);\n }\n });\n\n on(q('[data-action=\"created-ack\"]'), 'click', () => {\n const active = signer.getActiveAccount();\n if (active) handlers.onLogin?.(active);\n });\n\n // ---- ncryptsec ----\n const ncryptsecForm = q<HTMLFormElement>('[data-form=\"ncryptsec\"]');\n on(ncryptsecForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const ncryptsec = (\n ncryptsecForm.elements.namedItem('ncryptsec') as HTMLTextAreaElement\n ).value.trim();\n const passphrase = (\n ncryptsecForm.elements.namedItem('passphrase') as HTMLInputElement\n ).value;\n try {\n const account = await signer.loginWithNcryptsec(ncryptsec, passphrase);\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- extension ----\n on(q('[data-action=\"extension-login\"]'), 'click', async () => {\n clearError();\n try {\n const account = await signer.loginWithExtension();\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- signer app (NIP-55 over the web) ----\n // Absent when renderLoginHtml({ includeNip55Web: false }) omitted the tab.\n const nip55WebBtn = rootEl.querySelector<HTMLButtonElement>(\n '[data-action=\"nip55web-login\"]',\n );\n if (nip55WebBtn) on(nip55WebBtn, 'click', async () => {\n clearError();\n const btn = nip55WebBtn;\n btn.disabled = true;\n nip55WebAbort = new AbortController();\n try {\n const account = await signer.loginWithNip55Web({\n signal: nip55WebAbort.signal,\n debug: handlers.onNip55WebDebug,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n // A user cancelling (detach) is not an error worth surfacing.\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nip55WebAbort = null;\n }\n });\n\n // ---- bunker URI ----\n const bunkerForm = q<HTMLFormElement>('[data-form=\"bunker\"]');\n on(bunkerForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const uri = (\n bunkerForm.elements.namedItem('uri') as HTMLTextAreaElement\n ).value.trim();\n const perms = (bunkerForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n try {\n const account = await signer.loginWithBunkerUri(uri, {\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n perms: perms.length > 0 ? perms : undefined,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- nostrconnect ----\n const ncForm = q<HTMLFormElement>('[data-form=\"nostrconnect\"]');\n const ncQrRegion = q<HTMLDivElement>('[data-region=\"nostrconnect-qr\"]');\n const ncQrUri = q<HTMLElement>('[data-region=\"nostrconnect-uri\"]');\n const ncQrImage = q<HTMLDivElement>('[data-region=\"nostrconnect-qr-image\"]');\n const ncStatus = q<HTMLElement>('[data-region=\"nostrconnect-status\"]');\n let ncQrToken = 0;\n\n const resetNostrConnect = (): void => {\n ncForm.hidden = false;\n ncQrRegion.hidden = true;\n ncQrUri.textContent = '';\n ncQrImage.innerHTML = '';\n ncStatus.textContent = 'Waiting for remote signer to pair...';\n ncQrToken++;\n };\n\n const renderNostrConnectQr = async (uri: string): Promise<void> => {\n const token = ++ncQrToken;\n try {\n const svg = await QRCode.toString(uri, {\n type: 'svg',\n margin: 1,\n errorCorrectionLevel: 'M',\n });\n if (token === ncQrToken) ncQrImage.innerHTML = svg;\n } catch {\n // QR rendering is a nice-to-have — the URI text remains usable.\n }\n };\n\n on(ncForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const relays = (ncForm.elements.namedItem('relays') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (relays.length === 0) {\n showError('At least one relay is required.');\n return;\n }\n const perms = (ncForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n ncForm.hidden = true;\n ncQrRegion.hidden = false;\n nostrConnectAbort = new AbortController();\n try {\n const account = await signer.loginWithNostrConnect({\n relays,\n perms: perms.length > 0 ? perms : undefined,\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n signal: nostrConnectAbort.signal,\n onUri: (uri) => {\n ncQrUri.textContent = uri;\n void renderNostrConnectQr(uri);\n },\n });\n handlers.onLogin?.(account);\n } catch (err) {\n resetNostrConnect();\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nostrConnectAbort = null;\n }\n });\n\n on(q('[data-action=\"nostrconnect-cancel\"]'), 'click', () => {\n nostrConnectAbort?.abort();\n resetNostrConnect();\n });\n\n return {\n detach: () => {\n nostrConnectAbort?.abort();\n nip55WebAbort?.abort();\n for (const d of detachers) d();\n },\n selectTab,\n };\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/ui/index.ts"],"sourcesContent":["import QRCode from 'qrcode';\nimport type { AbstractSimplePool } from 'nostr-tools/abstract-pool';\nimport type { Signer } from '../core/signer.js';\nimport type { Nip55WebSupport } from '../nip55Web.js';\nimport type { RelayMismatchHandler, StoredAccount } from '../core/types.js';\n\n/** Escape text before interpolating it into the HTML template. */\nfunction escapeHtml(text: string): string {\n return text\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;');\n}\n\nexport type LoginTab =\n | 'create'\n | 'ncryptsec'\n | 'extension'\n | 'bunker'\n | 'nostrconnect'\n | 'nip55web'\n | 'android';\n\nexport interface LoginUiHandlers {\n onLogin?: (account: StoredAccount) => void;\n onCancel?: () => void;\n onError?: (error: Error) => void;\n /** Optional: passed through to NIP-46 login methods. */\n pool?: AbstractSimplePool;\n /** Optional: passed through to NIP-46 login methods. */\n onRelayMismatch?: RelayMismatchHandler;\n /** Initial tab to show. Defaults to 'create'. */\n defaultTab?: LoginTab;\n /**\n * Optional diagnostic stream for the browser NIP-55 (intent/clipboard)\n * flow. Wire it to an on-screen log when testing on a phone — the flow\n * navigates away, so its behaviour is otherwise hard to observe.\n */\n onNip55WebDebug?: (message: string) => void;\n}\n\nexport interface LoginUiBinding {\n detach(): void;\n selectTab(tab: LoginTab): void;\n}\n\nconst TABS: ReadonlyArray<{ id: LoginTab; label: string }> = [\n { id: 'create', label: 'Create' },\n { id: 'ncryptsec', label: 'Existing key' },\n { id: 'extension', label: 'Extension' },\n { id: 'bunker', label: 'Bunker URI' },\n { id: 'nostrconnect', label: 'Remote (QR)' },\n { id: 'nip55web', label: 'Signer app' },\n { id: 'android', label: 'Android' },\n];\n\nexport interface RenderLoginOptions {\n /**\n * The browser NIP-55 (\"Signer app\") status, from\n * `signer.nip55WebSupport()`. `visible` controls whether the tab is\n * rendered; `warning` renders an advisory note above the button without\n * disabling it.\n *\n * Defaults to shown without a warning. Pass the real status so a native\n * build hides the tab and Firefox for Android shows its warning.\n */\n nip55Web?: Nip55WebSupport;\n /**\n * Include the \"Signer app\" tab (browser NIP-55). Shorthand for the\n * `visible` half of {@link nip55Web}; defaults to `true`. Ignored when\n * `nip55Web` is supplied.\n *\n * @deprecated Prefer `nip55Web`, which can also carry a warning.\n */\n includeNip55Web?: boolean;\n}\n\nexport function renderLoginHtml(options: RenderLoginOptions = {}): string {\n const nip55Web: Nip55WebSupport =\n options.nip55Web ??\n (options.includeNip55Web === false\n ? { visible: false }\n : { visible: true });\n const includeNip55Web = nip55Web.visible;\n const tabs = TABS.filter((t) => t.id !== 'nip55web' || includeNip55Web)\n .map(\n (t) =>\n `<button class=\"nostr-signer__tab nostr-signer__tab--${t.id}\" type=\"button\" data-tab=\"${t.id}\">${t.label}</button>`,\n )\n .join('');\n\n return `<div class=\"nostr-signer__root\">\n <div class=\"nostr-signer__modal\">\n <header class=\"nostr-signer__header\">\n <h2 class=\"nostr-signer__title\">Sign in</h2>\n <button class=\"nostr-signer__close\" type=\"button\" data-action=\"cancel\" aria-label=\"Close\">&times;</button>\n </header>\n <nav class=\"nostr-signer__tabs\" role=\"tablist\">${tabs}</nav>\n <main class=\"nostr-signer__body\">\n <section class=\"nostr-signer__panel nostr-signer__panel--create\" data-panel=\"create\">\n <p class=\"nostr-signer__hint\">Generate a new nsec encrypted with your passphrase (NIP-49). Back up the ncryptsec on the next step — it's the only way back in.</p>\n <form class=\"nostr-signer__form\" data-form=\"create\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-create-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-create-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"new-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Create account</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--ncryptsec\" data-panel=\"ncryptsec\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"ncryptsec\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec\">ncryptsec1...</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--ncryptsec\" id=\"nostr-signer-ncryptsec\" name=\"ncryptsec\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-ncryptsec-passphrase\">Passphrase</label>\n <input class=\"nostr-signer__input nostr-signer__input--passphrase\" id=\"nostr-signer-ncryptsec-passphrase\" type=\"password\" name=\"passphrase\" autocomplete=\"current-password\" required>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Sign in</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--extension\" data-panel=\"extension\" hidden>\n <p class=\"nostr-signer__hint\">Requires a NIP-07 browser extension (nos2x, Alby, etc.).</p>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"extension-login\">Sign in with extension</button>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--bunker\" data-panel=\"bunker\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"bunker\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-uri\">bunker:// URI</label>\n <textarea class=\"nostr-signer__input nostr-signer__input--bunker-uri\" id=\"nostr-signer-bunker-uri\" name=\"uri\" placeholder=\"bunker://&lt;pubkey&gt;?relay=wss://...\" required></textarea>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-bunker-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-bunker-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Connect</button>\n </form>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--nostrconnect\" data-panel=\"nostrconnect\" hidden>\n <form class=\"nostr-signer__form\" data-form=\"nostrconnect\">\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-relays\">Relays (comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--relays\" id=\"nostr-signer-nc-relays\" name=\"relays\" placeholder=\"wss://relay.example, wss://other\" required>\n <label class=\"nostr-signer__label\" for=\"nostr-signer-nc-perms\">Permissions (optional, comma-separated)</label>\n <input class=\"nostr-signer__input nostr-signer__input--perms\" id=\"nostr-signer-nc-perms\" name=\"perms\" placeholder=\"sign_event:1, nip44_encrypt\">\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"submit\">Generate URI</button>\n </form>\n <div class=\"nostr-signer__qr\" data-region=\"nostrconnect-qr\" hidden>\n <p class=\"nostr-signer__hint\">Scan or paste this into your remote signer:</p>\n <div class=\"nostr-signer__qr-image\" data-region=\"nostrconnect-qr-image\" aria-hidden=\"true\"></div>\n <pre class=\"nostr-signer__qr-uri\" data-region=\"nostrconnect-uri\"></pre>\n <p class=\"nostr-signer__status\" data-region=\"nostrconnect-status\">Waiting for remote signer to pair...</p>\n <button class=\"nostr-signer__button nostr-signer__button--secondary\" type=\"button\" data-action=\"nostrconnect-cancel\">Cancel</button>\n </div>\n </section>\n ${includeNip55Web ? `<section class=\"nostr-signer__panel nostr-signer__panel--nip55web\" data-panel=\"nip55web\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with a signer app installed on this Android device, using the browser (NIP-55). You&rsquo;ll be asked to approve each signature in the app.</p>\n ${nip55Web.warning ? `<p class=\"nostr-signer__warn\" data-region=\"nip55web-warning\" role=\"status\">${escapeHtml(nip55Web.warning)}</p>` : ''}\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"nip55web-login\">Open signer app</button>\n </section>` : ''}\n <section class=\"nostr-signer__panel nostr-signer__panel--android\" data-panel=\"android\" hidden>\n <p class=\"nostr-signer__hint\">Sign in with an Android external signer app (NIP-55) such as Amber.</p>\n <p class=\"nostr-signer__status\" data-region=\"android-status\">Loading installed signers&hellip;</p>\n <ul class=\"nostr-signer__android-apps\" data-region=\"android-apps\" hidden></ul>\n </section>\n <section class=\"nostr-signer__panel nostr-signer__panel--created\" data-panel=\"created\" hidden>\n <p class=\"nostr-signer__hint\">Account created. Back up this encrypted secret key — without it (and your passphrase) you cannot sign in again.</p>\n <pre class=\"nostr-signer__ncryptsec-display\" data-region=\"created-ncryptsec\"></pre>\n <button class=\"nostr-signer__button nostr-signer__button--primary\" type=\"button\" data-action=\"created-ack\">I've backed it up</button>\n </section>\n </main>\n <div class=\"nostr-signer__error\" data-region=\"error\" hidden></div>\n </div>\n</div>`;\n}\n\nexport function attachLoginListeners(\n rootEl: HTMLElement,\n signer: Signer,\n handlers: LoginUiHandlers = {},\n): LoginUiBinding {\n const detachers: Array<() => void> = [];\n let nostrConnectAbort: AbortController | null = null;\n let nip55WebAbort: AbortController | null = null;\n\n const on = <K extends keyof HTMLElementEventMap>(\n el: Element,\n event: K,\n handler: (ev: HTMLElementEventMap[K]) => void,\n ): void => {\n const wrapped = handler as EventListener;\n el.addEventListener(event, wrapped);\n detachers.push(() => el.removeEventListener(event, wrapped));\n };\n\n const q = <T extends Element = Element>(selector: string): T =>\n rootEl.querySelector(selector) as T;\n\n const errorEl = q<HTMLDivElement>('[data-region=\"error\"]');\n const showError = (err: Error | string): void => {\n const msg = typeof err === 'string' ? err : err.message;\n errorEl.hidden = false;\n errorEl.textContent = msg;\n if (typeof err !== 'string') handlers.onError?.(err);\n };\n const clearError = (): void => {\n errorEl.hidden = true;\n errorEl.textContent = '';\n };\n\n // ---- android (NIP-55) — declared before selectTab so it can be invoked\n // when the user opens with defaultTab: 'android'.\n const androidStatus = q<HTMLElement>('[data-region=\"android-status\"]');\n const androidList = q<HTMLUListElement>('[data-region=\"android-apps\"]');\n let androidFetchToken = 0;\n\n const refreshAndroidApps = async (): Promise<void> => {\n const token = ++androidFetchToken;\n androidList.hidden = true;\n androidList.innerHTML = '';\n androidStatus.hidden = false;\n androidStatus.textContent = 'Loading installed signers…';\n try {\n const apps = await signer.listAndroidSignerApps();\n if (token !== androidFetchToken) return;\n if (apps.length === 0) {\n androidStatus.textContent =\n 'No NIP-55 signer apps installed. Install Amber (or similar) and try again.';\n return;\n }\n androidStatus.hidden = true;\n for (const app of apps) {\n const li = document.createElement('li');\n li.className = 'nostr-signer__android-app';\n const btn = document.createElement('button');\n btn.type = 'button';\n btn.className = 'nostr-signer__button nostr-signer__button--primary';\n btn.dataset.packageName = app.packageName;\n btn.textContent = `${app.name} (${app.packageName})`;\n on(btn, 'click', async () => {\n clearError();\n btn.disabled = true;\n try {\n const account = await signer.loginWithAndroidSigner({\n packageName: app.packageName,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n showError(err as Error);\n }\n });\n li.appendChild(btn);\n androidList.appendChild(li);\n }\n androidList.hidden = false;\n } catch (err) {\n if (token !== androidFetchToken) return;\n androidStatus.textContent = (err as Error).message;\n }\n };\n\n const selectTab = (tab: LoginTab): void => {\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((panel) => {\n panel.hidden = panel.dataset.panel !== tab;\n });\n rootEl.querySelectorAll<HTMLElement>('[data-tab]').forEach((btn) => {\n btn.classList.toggle('nostr-signer__tab--active', btn.dataset.tab === tab);\n });\n clearError();\n if (tab === 'android') void refreshAndroidApps();\n };\n\n selectTab(handlers.defaultTab ?? 'create');\n\n rootEl.querySelectorAll<HTMLButtonElement>('[data-tab]').forEach((btn) => {\n on(btn, 'click', () => selectTab(btn.dataset.tab as LoginTab));\n });\n\n on(q('[data-action=\"cancel\"]'), 'click', () => handlers.onCancel?.());\n\n // ---- create ----\n const createForm = q<HTMLFormElement>('[data-form=\"create\"]');\n on(createForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const passphrase = (createForm.elements.namedItem('passphrase') as HTMLInputElement).value;\n try {\n const { ncryptsec } = await signer.createAccount(passphrase);\n q<HTMLElement>('[data-region=\"created-ncryptsec\"]').textContent = ncryptsec;\n rootEl.querySelectorAll<HTMLElement>('[data-panel]').forEach((p) => {\n p.hidden = p.dataset.panel !== 'created';\n });\n } catch (err) {\n showError(err as Error);\n }\n });\n\n on(q('[data-action=\"created-ack\"]'), 'click', () => {\n const active = signer.getActiveAccount();\n if (active) handlers.onLogin?.(active);\n });\n\n // ---- ncryptsec ----\n const ncryptsecForm = q<HTMLFormElement>('[data-form=\"ncryptsec\"]');\n on(ncryptsecForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const ncryptsec = (\n ncryptsecForm.elements.namedItem('ncryptsec') as HTMLTextAreaElement\n ).value.trim();\n const passphrase = (\n ncryptsecForm.elements.namedItem('passphrase') as HTMLInputElement\n ).value;\n try {\n const account = await signer.loginWithNcryptsec(ncryptsec, passphrase);\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- extension ----\n on(q('[data-action=\"extension-login\"]'), 'click', async () => {\n clearError();\n try {\n const account = await signer.loginWithExtension();\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- signer app (NIP-55 over the web) ----\n // Absent when renderLoginHtml({ includeNip55Web: false }) omitted the tab.\n const nip55WebBtn = rootEl.querySelector<HTMLButtonElement>(\n '[data-action=\"nip55web-login\"]',\n );\n if (nip55WebBtn) on(nip55WebBtn, 'click', async () => {\n clearError();\n const btn = nip55WebBtn;\n btn.disabled = true;\n nip55WebAbort = new AbortController();\n try {\n const account = await signer.loginWithNip55Web({\n signal: nip55WebAbort.signal,\n debug: handlers.onNip55WebDebug,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n btn.disabled = false;\n // A user cancelling (detach) is not an error worth surfacing.\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nip55WebAbort = null;\n }\n });\n\n // ---- bunker URI ----\n const bunkerForm = q<HTMLFormElement>('[data-form=\"bunker\"]');\n on(bunkerForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const uri = (\n bunkerForm.elements.namedItem('uri') as HTMLTextAreaElement\n ).value.trim();\n const perms = (bunkerForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n try {\n const account = await signer.loginWithBunkerUri(uri, {\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n perms: perms.length > 0 ? perms : undefined,\n });\n handlers.onLogin?.(account);\n } catch (err) {\n showError(err as Error);\n }\n });\n\n // ---- nostrconnect ----\n const ncForm = q<HTMLFormElement>('[data-form=\"nostrconnect\"]');\n const ncQrRegion = q<HTMLDivElement>('[data-region=\"nostrconnect-qr\"]');\n const ncQrUri = q<HTMLElement>('[data-region=\"nostrconnect-uri\"]');\n const ncQrImage = q<HTMLDivElement>('[data-region=\"nostrconnect-qr-image\"]');\n const ncStatus = q<HTMLElement>('[data-region=\"nostrconnect-status\"]');\n let ncQrToken = 0;\n\n const resetNostrConnect = (): void => {\n ncForm.hidden = false;\n ncQrRegion.hidden = true;\n ncQrUri.textContent = '';\n ncQrImage.innerHTML = '';\n ncStatus.textContent = 'Waiting for remote signer to pair...';\n ncQrToken++;\n };\n\n const renderNostrConnectQr = async (uri: string): Promise<void> => {\n const token = ++ncQrToken;\n try {\n const svg = await QRCode.toString(uri, {\n type: 'svg',\n margin: 1,\n errorCorrectionLevel: 'M',\n });\n if (token === ncQrToken) ncQrImage.innerHTML = svg;\n } catch {\n // QR rendering is a nice-to-have — the URI text remains usable.\n }\n };\n\n on(ncForm, 'submit', async (ev) => {\n ev.preventDefault();\n clearError();\n const relays = (ncForm.elements.namedItem('relays') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n if (relays.length === 0) {\n showError('At least one relay is required.');\n return;\n }\n const perms = (ncForm.elements.namedItem('perms') as HTMLInputElement).value\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean);\n\n ncForm.hidden = true;\n ncQrRegion.hidden = false;\n nostrConnectAbort = new AbortController();\n try {\n const account = await signer.loginWithNostrConnect({\n relays,\n perms: perms.length > 0 ? perms : undefined,\n pool: handlers.pool,\n onRelayMismatch: handlers.onRelayMismatch,\n signal: nostrConnectAbort.signal,\n onUri: (uri) => {\n ncQrUri.textContent = uri;\n void renderNostrConnectQr(uri);\n },\n });\n handlers.onLogin?.(account);\n } catch (err) {\n resetNostrConnect();\n if ((err as Error).name !== 'AbortError') showError(err as Error);\n } finally {\n nostrConnectAbort = null;\n }\n });\n\n on(q('[data-action=\"nostrconnect-cancel\"]'), 'click', () => {\n nostrConnectAbort?.abort();\n resetNostrConnect();\n });\n\n return {\n detach: () => {\n nostrConnectAbort?.abort();\n nip55WebAbort?.abort();\n for (const d of detachers) d();\n },\n selectTab,\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formstr/signer",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Nostr signer with login UI for NIP-07, NIP-46, NIP-49 (ncryptsec), and NIP-55 (Android apps, native or browser)",
5
5
  "license": "MIT",
6
6
  "author": "Abhay <toabhayraizada@gmail.com>",
package/styles/signer.css CHANGED
@@ -162,6 +162,16 @@ textarea.nostr-signer__input {
162
162
  font-size: 0.85rem;
163
163
  }
164
164
 
165
+ /* Advisory, non-blocking — the action below it stays enabled. */
166
+ .nostr-signer__warn {
167
+ margin: 0 0 12px 0;
168
+ padding: 8px 12px;
169
+ border-radius: 6px;
170
+ background: #fef3c7;
171
+ color: #92400e;
172
+ font-size: 0.85rem;
173
+ }
174
+
165
175
  .nostr-signer__qr {
166
176
  margin-top: 12px;
167
177
  }