Add nsigner account probe diagnostics and fix modal visibility during chooser flow

This commit is contained in:
Laan Tungir
2026-05-11 07:51:09 -04:00
parent 1fd3862324
commit bdc9513e52
4 changed files with 209 additions and 67 deletions
+102 -31
View File
@@ -8,7 +8,7 @@
* Two-file architecture:
* 1. Load nostr.bundle.js (official nostr-tools bundle)
* 2. Load nostr-lite.js (this file - NOSTR_LOGIN_LITE library with CSS-only themes)
* Generated on: 2026-05-10T22:25:58.153Z
* Generated on: 2026-05-11T11:51:09.384Z
*/
// Verify dependencies are loaded
@@ -436,7 +436,7 @@ class Modal {
modalContent.appendChild(modalHeader);
// Add version element in bottom-right corner aligned with modal body
const versionElement = document.createElement('div');
versionElement.textContent = 'v0.1.16';
versionElement.textContent = 'v0.1.17';
versionElement.style.cssText = `
position: absolute;
bottom: 8px;
@@ -715,7 +715,7 @@ class Modal {
this._showConnectScreen();
break;
case 'nsigner':
this._showNSignerScreen();
this._showNSignerScreen({ autoPrompt: true });
break;
case 'readonly':
this._handleReadonly();
@@ -1735,7 +1735,7 @@ class Modal {
title.style.cssText = 'margin: 0 0 12px 0; font-size: 18px; font-weight: 600; color: var(--nl-primary-color);';
const description = document.createElement('p');
description.textContent = 'Connect your n_signer Feather S3 device over USB, then pick the account index to sign in with.';
description.textContent = 'Select your USB n_signer device when prompted, then choose one of the discovered accounts.';
description.style.cssText = 'margin-bottom: 14px; color: #6b7280; font-size: 14px;';
const status = document.createElement('div');
@@ -1761,19 +1761,8 @@ class Modal {
return;
}
const indexLabel = document.createElement('label');
indexLabel.textContent = 'Starting key index';
indexLabel.style.cssText = 'display:block; margin-bottom: 6px; font-weight: 500;';
const indexInput = document.createElement('input');
indexInput.type = 'number';
indexInput.min = '0';
indexInput.step = '1';
indexInput.value = '0';
indexInput.style.cssText = 'width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; margin-bottom: 12px; box-sizing: border-box;';
const connectButton = document.createElement('button');
connectButton.textContent = 'Connect Device';
connectButton.textContent = 'Choose Device';
connectButton.style.cssText = this._getButtonStyle();
const accountList = document.createElement('div');
@@ -1785,20 +1774,70 @@ class Modal {
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 10px;';
const shortHex = (v) => `${v.slice(0, 12)}...${v.slice(-8)}`;
const prePromptDisplay = this.container.style.display;
let hiddenForChooser = false;
const renderAccountButtons = (connected) => {
accountList.innerHTML = '';
// Once device is selected and accounts are loaded, switch to pure account selection UI
// matching the seed phrase flow (no reconnect header/buttons).
this.modalBody.innerHTML = '';
const label = document.createElement('div');
label.textContent = 'Select account:';
label.style.cssText = 'margin-bottom: 8px; font-weight: 600; color: var(--nl-primary-color);';
accountList.appendChild(label);
const selectionDescription = document.createElement('p');
selectionDescription.textContent = `Select which account to use (${connected.accounts.length} accounts discovered from hardware signer):`;
selectionDescription.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
this.modalBody.appendChild(selectionDescription);
const table = document.createElement('table');
table.style.cssText = `
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
font-family: var(--nl-font-family, 'Courier New', monospace);
font-size: 12px;
`;
const thead = document.createElement('thead');
thead.innerHTML = `
<tr style="background: #f3f4f6;">
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">#</th>
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Use</th>
</tr>
`;
table.appendChild(thead);
const tbody = document.createElement('tbody');
connected.accounts.forEach((acct) => {
const rowButton = document.createElement('button');
rowButton.textContent = `Index ${acct.index}: ${shortHex(acct.pubkey)}`;
rowButton.style.cssText = this._getButtonStyle('secondary') + 'margin-bottom: 8px;';
rowButton.onclick = () => {
const row = document.createElement('tr');
row.style.cssText = 'border: 1px solid #d1d5db;';
const indexCell = document.createElement('td');
indexCell.textContent = acct.index;
indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;';
const actionCell = document.createElement('td');
actionCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db;';
const selectButton = document.createElement('button');
selectButton.textContent = shortHex(acct.pubkey);
selectButton.style.cssText = `
width: 100%;
padding: 8px 12px;
font-size: 11px;
background: var(--nl-secondary-color);
color: var(--nl-primary-color);
border: 1px solid var(--nl-primary-color);
border-radius: 4px;
cursor: pointer;
font-family: 'Courier New', monospace;
text-align: center;
`;
selectButton.onmouseover = () => {
selectButton.style.borderColor = 'var(--nl-accent-color)';
};
selectButton.onmouseout = () => {
selectButton.style.borderColor = 'var(--nl-primary-color)';
};
selectButton.onclick = () => {
connected.driver.nostrIndex = acct.index;
this._setAuthMethod('nsigner', {
pubkey: acct.pubkey,
@@ -1813,13 +1852,26 @@ class Modal {
}
});
};
accountList.appendChild(rowButton);
actionCell.appendChild(selectButton);
row.appendChild(indexCell);
row.appendChild(actionCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
this.modalBody.appendChild(table);
const selectionBackButton = document.createElement('button');
selectionBackButton.textContent = 'Back';
selectionBackButton.onclick = () => this._renderLoginOptions();
selectionBackButton.style.cssText = this._getButtonStyle('secondary');
this.modalBody.appendChild(selectionBackButton);
};
const connectNow = async () => {
try {
const startIndex = Math.max(0, Number.parseInt(indexInput.value || '0', 10) || 0);
const startIndex = 0;
status.textContent = 'Connecting...';
accountList.innerHTML = '';
@@ -1830,6 +1882,7 @@ class Modal {
}
const selectedDevice = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x303a }] });
const driver = new window.NSignerWebUSB(selectedDevice, {
callerSecretKey: caller.secret,
nostrIndex: startIndex
@@ -1842,15 +1895,19 @@ class Modal {
for (let i = startIndex; i < startIndex + 6; i++) {
try {
driver.nostrIndex = i;
console.info('NSigner probe index', i, 'nostrIndex=', driver.nostrIndex);
const pubkey = await driver.getPublicKey();
console.info('NSigner probe index', i, 'pubkey=', pubkey);
if (/^[0-9a-f]{64}$/.test(pubkey) && !accounts.find(a => a.pubkey === pubkey)) {
accounts.push({ index: i, pubkey });
}
} catch (e) {
console.warn('NSigner account probe failed:', i, e?.message || e);
console.warn('NSigner account probe failed for index', i, ':', e?.message || e);
}
}
console.info('NSigner probe complete, accounts found:', accounts.length, accounts);
if (!accounts.length) {
throw new Error('No account pubkeys returned by signer');
}
@@ -1883,7 +1940,11 @@ class Modal {
}));
});
status.textContent = `Connected. Found ${accounts.length} account(s). Choose one to continue.`;
// Ensure modal is visible before rendering account selection.
if (hiddenForChooser && this.isVisible) {
this.container.style.display = prePromptDisplay || 'block';
hiddenForChooser = false;
}
renderAccountButtons(connected);
} catch (error) {
const msg = String(error?.message || error);
@@ -1895,21 +1956,31 @@ class Modal {
status.textContent = `Connect failed: ${msg}`;
}
}
// If chooser was hidden and we failed before account list render,
// bring modal back with status text instead of leaving blank body.
if (hiddenForChooser && this.isVisible) {
this.container.style.display = prePromptDisplay || 'block';
hiddenForChooser = false;
}
};
connectButton.onclick = connectNow;
// Keep scaffold consistent so auto-prompt failures still show status/back UI.
this.modalBody.appendChild(title);
this.modalBody.appendChild(description);
this.modalBody.appendChild(indexLabel);
this.modalBody.appendChild(indexInput);
this.modalBody.appendChild(connectButton);
this.modalBody.appendChild(status);
this.modalBody.appendChild(accountList);
this.modalBody.appendChild(backButton);
// For tile-click flow, hide modal only while chooser is visible.
if (autoPrompt) {
hiddenForChooser = true;
this.container.style.display = 'none';
connectNow();
return;
}
}
+6 -6
View File
@@ -34,12 +34,12 @@
await window.NOSTR_LOGIN_LITE.init({
theme: 'default',
methods: {
extension: true,
local: true,
readonly: true,
connect: true,
nsigner: true,
otp: false
// extension: true,
// local: true,
// readonly: true,
// connect: true,
// nsigner: true,
// otp: false
},
floatingTab: { enabled: false }
});
+1 -1
View File
@@ -1 +1 @@
0.1.16
0.1.17
+100 -29
View File
@@ -403,7 +403,7 @@ class Modal {
this._showConnectScreen();
break;
case 'nsigner':
this._showNSignerScreen();
this._showNSignerScreen({ autoPrompt: true });
break;
case 'readonly':
this._handleReadonly();
@@ -1423,7 +1423,7 @@ class Modal {
title.style.cssText = 'margin: 0 0 12px 0; font-size: 18px; font-weight: 600; color: var(--nl-primary-color);';
const description = document.createElement('p');
description.textContent = 'Connect your n_signer Feather S3 device over USB, then pick the account index to sign in with.';
description.textContent = 'Select your USB n_signer device when prompted, then choose one of the discovered accounts.';
description.style.cssText = 'margin-bottom: 14px; color: #6b7280; font-size: 14px;';
const status = document.createElement('div');
@@ -1449,19 +1449,8 @@ class Modal {
return;
}
const indexLabel = document.createElement('label');
indexLabel.textContent = 'Starting key index';
indexLabel.style.cssText = 'display:block; margin-bottom: 6px; font-weight: 500;';
const indexInput = document.createElement('input');
indexInput.type = 'number';
indexInput.min = '0';
indexInput.step = '1';
indexInput.value = '0';
indexInput.style.cssText = 'width: 100%; padding: 10px; border: 1px solid #d1d5db; border-radius: 6px; margin-bottom: 12px; box-sizing: border-box;';
const connectButton = document.createElement('button');
connectButton.textContent = 'Connect Device';
connectButton.textContent = 'Choose Device';
connectButton.style.cssText = this._getButtonStyle();
const accountList = document.createElement('div');
@@ -1473,20 +1462,70 @@ class Modal {
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 10px;';
const shortHex = (v) => `${v.slice(0, 12)}...${v.slice(-8)}`;
const prePromptDisplay = this.container.style.display;
let hiddenForChooser = false;
const renderAccountButtons = (connected) => {
accountList.innerHTML = '';
// Once device is selected and accounts are loaded, switch to pure account selection UI
// matching the seed phrase flow (no reconnect header/buttons).
this.modalBody.innerHTML = '';
const label = document.createElement('div');
label.textContent = 'Select account:';
label.style.cssText = 'margin-bottom: 8px; font-weight: 600; color: var(--nl-primary-color);';
accountList.appendChild(label);
const selectionDescription = document.createElement('p');
selectionDescription.textContent = `Select which account to use (${connected.accounts.length} accounts discovered from hardware signer):`;
selectionDescription.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
this.modalBody.appendChild(selectionDescription);
const table = document.createElement('table');
table.style.cssText = `
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
font-family: var(--nl-font-family, 'Courier New', monospace);
font-size: 12px;
`;
const thead = document.createElement('thead');
thead.innerHTML = `
<tr style="background: #f3f4f6;">
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">#</th>
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Use</th>
</tr>
`;
table.appendChild(thead);
const tbody = document.createElement('tbody');
connected.accounts.forEach((acct) => {
const rowButton = document.createElement('button');
rowButton.textContent = `Index ${acct.index}: ${shortHex(acct.pubkey)}`;
rowButton.style.cssText = this._getButtonStyle('secondary') + 'margin-bottom: 8px;';
rowButton.onclick = () => {
const row = document.createElement('tr');
row.style.cssText = 'border: 1px solid #d1d5db;';
const indexCell = document.createElement('td');
indexCell.textContent = acct.index;
indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;';
const actionCell = document.createElement('td');
actionCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db;';
const selectButton = document.createElement('button');
selectButton.textContent = shortHex(acct.pubkey);
selectButton.style.cssText = `
width: 100%;
padding: 8px 12px;
font-size: 11px;
background: var(--nl-secondary-color);
color: var(--nl-primary-color);
border: 1px solid var(--nl-primary-color);
border-radius: 4px;
cursor: pointer;
font-family: 'Courier New', monospace;
text-align: center;
`;
selectButton.onmouseover = () => {
selectButton.style.borderColor = 'var(--nl-accent-color)';
};
selectButton.onmouseout = () => {
selectButton.style.borderColor = 'var(--nl-primary-color)';
};
selectButton.onclick = () => {
connected.driver.nostrIndex = acct.index;
this._setAuthMethod('nsigner', {
pubkey: acct.pubkey,
@@ -1501,13 +1540,26 @@ class Modal {
}
});
};
accountList.appendChild(rowButton);
actionCell.appendChild(selectButton);
row.appendChild(indexCell);
row.appendChild(actionCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
this.modalBody.appendChild(table);
const selectionBackButton = document.createElement('button');
selectionBackButton.textContent = 'Back';
selectionBackButton.onclick = () => this._renderLoginOptions();
selectionBackButton.style.cssText = this._getButtonStyle('secondary');
this.modalBody.appendChild(selectionBackButton);
};
const connectNow = async () => {
try {
const startIndex = Math.max(0, Number.parseInt(indexInput.value || '0', 10) || 0);
const startIndex = 0;
status.textContent = 'Connecting...';
accountList.innerHTML = '';
@@ -1518,6 +1570,7 @@ class Modal {
}
const selectedDevice = await navigator.usb.requestDevice({ filters: [{ vendorId: 0x303a }] });
const driver = new window.NSignerWebUSB(selectedDevice, {
callerSecretKey: caller.secret,
nostrIndex: startIndex
@@ -1530,15 +1583,19 @@ class Modal {
for (let i = startIndex; i < startIndex + 6; i++) {
try {
driver.nostrIndex = i;
console.info('NSigner probe index', i, 'nostrIndex=', driver.nostrIndex);
const pubkey = await driver.getPublicKey();
console.info('NSigner probe index', i, 'pubkey=', pubkey);
if (/^[0-9a-f]{64}$/.test(pubkey) && !accounts.find(a => a.pubkey === pubkey)) {
accounts.push({ index: i, pubkey });
}
} catch (e) {
console.warn('NSigner account probe failed:', i, e?.message || e);
console.warn('NSigner account probe failed for index', i, ':', e?.message || e);
}
}
console.info('NSigner probe complete, accounts found:', accounts.length, accounts);
if (!accounts.length) {
throw new Error('No account pubkeys returned by signer');
}
@@ -1571,7 +1628,11 @@ class Modal {
}));
});
status.textContent = `Connected. Found ${accounts.length} account(s). Choose one to continue.`;
// Ensure modal is visible before rendering account selection.
if (hiddenForChooser && this.isVisible) {
this.container.style.display = prePromptDisplay || 'block';
hiddenForChooser = false;
}
renderAccountButtons(connected);
} catch (error) {
const msg = String(error?.message || error);
@@ -1583,21 +1644,31 @@ class Modal {
status.textContent = `Connect failed: ${msg}`;
}
}
// If chooser was hidden and we failed before account list render,
// bring modal back with status text instead of leaving blank body.
if (hiddenForChooser && this.isVisible) {
this.container.style.display = prePromptDisplay || 'block';
hiddenForChooser = false;
}
};
connectButton.onclick = connectNow;
// Keep scaffold consistent so auto-prompt failures still show status/back UI.
this.modalBody.appendChild(title);
this.modalBody.appendChild(description);
this.modalBody.appendChild(indexLabel);
this.modalBody.appendChild(indexInput);
this.modalBody.appendChild(connectButton);
this.modalBody.appendChild(status);
this.modalBody.appendChild(accountList);
this.modalBody.appendChild(backButton);
// For tile-click flow, hide modal only while chooser is visible.
if (autoPrompt) {
hiddenForChooser = true;
this.container.style.display = 'none';
connectNow();
return;
}
}