Fixed issue with bunker. Made the modal more beautiful.
This commit is contained in:
@@ -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: 2025-09-16T22:12:00.192Z
|
||||
* Generated on: 2025-09-19T16:21:34.133Z
|
||||
*/
|
||||
|
||||
// Verify dependencies are loaded
|
||||
@@ -44,7 +44,7 @@ const THEME_CSS = {
|
||||
--nl-primary-color: #000000;
|
||||
--nl-secondary-color: #ffffff;
|
||||
--nl-accent-color: #ff0000;
|
||||
--nl-muted-color: #666666;
|
||||
--nl-muted-color: #CCCCCC;
|
||||
--nl-font-family: "Courier New", Courier, monospace;
|
||||
--nl-border-radius: 15px;
|
||||
--nl-border-width: 3px;
|
||||
@@ -401,7 +401,7 @@ class Modal {
|
||||
closeButton.style.cssText = `
|
||||
background: var(--nl-secondary-color);
|
||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
border-radius: 4px;
|
||||
font-size: 28px;
|
||||
color: var(--nl-primary-color);
|
||||
cursor: pointer;
|
||||
@@ -585,21 +585,13 @@ class Modal {
|
||||
};
|
||||
|
||||
const iconDiv = document.createElement('div');
|
||||
// Replace emoji icons with text-based ones
|
||||
const iconMap = {
|
||||
'🔌': '[EXT]',
|
||||
'🔑': '[KEY]',
|
||||
'🌱': '[SEED]',
|
||||
'🌐': '[NET]',
|
||||
'👁️': '[VIEW]',
|
||||
'📱': '[SMS]'
|
||||
};
|
||||
iconDiv.textContent = iconMap[option.icon] || option.icon;
|
||||
// Remove the icon entirely - no emojis or text-based icons
|
||||
iconDiv.textContent = '';
|
||||
iconDiv.style.cssText = `
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-right: 16px;
|
||||
width: 50px;
|
||||
width: 0px;
|
||||
text-align: center;
|
||||
color: var(--nl-primary-color);
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
@@ -1494,10 +1486,6 @@ class Modal {
|
||||
_showConnectScreen() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Connect to NIP-46 Remote Signer';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = 'Connect to a remote signer (bunker) server to use its keys for signing.';
|
||||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||||
@@ -1522,12 +1510,67 @@ class Modal {
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Users will enter the complete bunker connection string with relay info
|
||||
// Add real-time bunker key validation
|
||||
const formatHint = document.createElement('div');
|
||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||||
|
||||
const connectButton = document.createElement('button');
|
||||
connectButton.textContent = 'Connect to Bunker';
|
||||
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value);
|
||||
connectButton.style.cssText = this._getButtonStyle();
|
||||
connectButton.disabled = true;
|
||||
connectButton.onclick = () => {
|
||||
if (!connectButton.disabled) {
|
||||
this._handleNip46Connect(pubkeyInput.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial disabled state
|
||||
connectButton.style.cssText = `
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-muted-color);
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
pubkeyInput.oninput = () => {
|
||||
const value = pubkeyInput.value.trim();
|
||||
if (!value) {
|
||||
formatHint.textContent = '';
|
||||
// Disable button
|
||||
connectButton.disabled = true;
|
||||
connectButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
connectButton.style.color = 'var(--nl-muted-color)';
|
||||
connectButton.style.cursor = 'not-allowed';
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = this._validateBunkerKey(value);
|
||||
if (isValid) {
|
||||
formatHint.textContent = '✅ Valid bunker connection format detected';
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
connectButton.disabled = false;
|
||||
connectButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
connectButton.style.color = 'var(--nl-primary-color)';
|
||||
connectButton.style.cursor = 'pointer';
|
||||
} else {
|
||||
formatHint.textContent = '❌ Invalid format - must be bunker://, npub, or 64-char hex';
|
||||
formatHint.style.color = '#dc2626';
|
||||
// Disable button
|
||||
connectButton.disabled = true;
|
||||
connectButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
connectButton.style.color = 'var(--nl-muted-color)';
|
||||
connectButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
@@ -1536,14 +1579,49 @@ class Modal {
|
||||
|
||||
formGroup.appendChild(label);
|
||||
formGroup.appendChild(pubkeyInput);
|
||||
formGroup.appendChild(formatHint);
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
this.modalBody.appendChild(formGroup);
|
||||
this.modalBody.appendChild(connectButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
}
|
||||
|
||||
_validateBunkerKey(bunkerKey) {
|
||||
try {
|
||||
const trimmed = bunkerKey.trim();
|
||||
|
||||
// Check for bunker:// format
|
||||
if (trimmed.startsWith('bunker://')) {
|
||||
// Should have format: bunker://pubkey or bunker://pubkey?param=value
|
||||
const match = trimmed.match(/^bunker:\/\/([0-9a-fA-F]{64})(\?.*)?$/);
|
||||
return !!match;
|
||||
}
|
||||
|
||||
// Check for npub format
|
||||
if (trimmed.startsWith('npub1') && trimmed.length === 63) {
|
||||
try {
|
||||
if (window.NostrTools?.nip19) {
|
||||
const decoded = window.NostrTools.nip19.decode(trimmed);
|
||||
return decoded.type === 'npub';
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for hex format (64 characters, valid hex)
|
||||
if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.log('Bunker key validation failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_handleNip46Connect(bunkerPubkey) {
|
||||
if (!bunkerPubkey || !bunkerPubkey.length) {
|
||||
this._showError('Bunker pubkey is required');
|
||||
@@ -1613,9 +1691,9 @@ class Modal {
|
||||
const localSecretKey = window.NostrTools.generateSecretKey();
|
||||
console.log('Generated local client keypair for NIP-46 session');
|
||||
|
||||
// Use nostr-tools BunkerSigner constructor
|
||||
// Use nostr-tools BunkerSigner factory method (not constructor - it's private)
|
||||
console.log('Creating nip46 BunkerSigner...');
|
||||
const signer = new window.NostrTools.nip46.BunkerSigner(localSecretKey, bunkerPointer, {
|
||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||
onauth: (url) => {
|
||||
console.log('Received auth URL from bunker:', url);
|
||||
// Open auth URL in popup or redirect
|
||||
@@ -1695,12 +1773,8 @@ class Modal {
|
||||
_showSeedPhraseScreen() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Import from Seed Phrase';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts:';
|
||||
description.innerHTML = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts, or <span id="generate-new" style="text-decoration: underline; cursor: pointer; color: var(--nl-primary-color);">generate new</span>.';
|
||||
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
@@ -1723,10 +1797,40 @@ class Modal {
|
||||
const formatHint = document.createElement('div');
|
||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Accounts';
|
||||
importButton.disabled = true;
|
||||
importButton.onclick = () => {
|
||||
if (!importButton.disabled) {
|
||||
this._importFromSeedPhrase(textarea.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial disabled state
|
||||
importButton.style.cssText = `
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-muted-color);
|
||||
`;
|
||||
|
||||
textarea.oninput = () => {
|
||||
const value = textarea.value.trim();
|
||||
if (!value) {
|
||||
formatHint.textContent = '';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1735,35 +1839,46 @@ class Modal {
|
||||
const wordCount = value.split(/\s+/).length;
|
||||
formatHint.textContent = `✅ Valid ${wordCount}-word mnemonic detected`;
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
importButton.disabled = false;
|
||||
importButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
importButton.style.color = 'var(--nl-primary-color)';
|
||||
importButton.style.cursor = 'pointer';
|
||||
} else {
|
||||
formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words';
|
||||
formatHint.style.color = '#dc2626';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
// Generate new seed phrase button
|
||||
const generateButton = document.createElement('button');
|
||||
generateButton.textContent = 'Generate New Seed Phrase';
|
||||
generateButton.onclick = () => this._generateNewSeedPhrase(textarea, formatHint);
|
||||
generateButton.style.cssText = this._getButtonStyle() + 'margin-bottom: 12px;';
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Accounts';
|
||||
importButton.onclick = () => this._importFromSeedPhrase(textarea.value);
|
||||
importButton.style.cssText = this._getButtonStyle();
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
backButton.onclick = () => this._renderLoginOptions();
|
||||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
this.modalBody.appendChild(textarea);
|
||||
this.modalBody.appendChild(formatHint);
|
||||
this.modalBody.appendChild(generateButton);
|
||||
this.modalBody.appendChild(importButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
|
||||
// Add click handler for the "generate new" link
|
||||
const generateLink = document.getElementById('generate-new');
|
||||
if (generateLink) {
|
||||
generateLink.addEventListener('mouseenter', () => {
|
||||
generateLink.style.color = 'var(--nl-accent-color)';
|
||||
});
|
||||
generateLink.addEventListener('mouseleave', () => {
|
||||
generateLink.style.color = 'var(--nl-primary-color)';
|
||||
});
|
||||
generateLink.addEventListener('click', () => {
|
||||
this._generateNewSeedPhrase(textarea, formatHint);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_generateNewSeedPhrase(textarea, formatHint) {
|
||||
@@ -1779,12 +1894,12 @@ class Modal {
|
||||
// Set the generated mnemonic in the textarea
|
||||
textarea.value = mnemonic;
|
||||
|
||||
// Trigger validation to show it's valid
|
||||
const wordCount = mnemonic.split(/\s+/).length;
|
||||
formatHint.textContent = `✅ Generated valid ${wordCount}-word mnemonic`;
|
||||
formatHint.style.color = '#059669';
|
||||
// Trigger the oninput event to properly validate and enable the button
|
||||
if (textarea.oninput) {
|
||||
textarea.oninput();
|
||||
}
|
||||
|
||||
console.log('Generated new seed phrase:', wordCount, 'words');
|
||||
console.log('Generated new seed phrase:', mnemonic.split(/\s+/).length, 'words');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to generate seed phrase:', error);
|
||||
@@ -1866,15 +1981,10 @@ class Modal {
|
||||
_showAccountSelection(accounts) {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Select Account';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = `Select which account to use (${accounts.length} accounts derived from seed phrase):`;
|
||||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
|
||||
// Create table for account selection
|
||||
@@ -1892,8 +2002,7 @@ class Modal {
|
||||
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: left; border: 1px solid #d1d5db; font-weight: bold;">Public Key (npub)</th>
|
||||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Action</th>
|
||||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Use</th>
|
||||
</tr>
|
||||
`;
|
||||
table.appendChild(thead);
|
||||
@@ -1908,31 +2017,26 @@ class Modal {
|
||||
indexCell.textContent = account.index;
|
||||
indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;';
|
||||
|
||||
const pubkeyCell = document.createElement('td');
|
||||
pubkeyCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db; font-family: monospace; word-break: break-all;';
|
||||
|
||||
// Show truncated npub for readability
|
||||
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
|
||||
pubkeyCell.innerHTML = `
|
||||
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 2px;">${truncatedNpub}</code><br>
|
||||
<small style="color: #6b7280;">Full: ${account.npub}</small>
|
||||
`;
|
||||
|
||||
const actionCell = document.createElement('td');
|
||||
actionCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db;';
|
||||
actionCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db;';
|
||||
|
||||
// Show truncated npub in the button
|
||||
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
|
||||
|
||||
const selectButton = document.createElement('button');
|
||||
selectButton.textContent = 'Use';
|
||||
selectButton.textContent = truncatedNpub;
|
||||
selectButton.onclick = () => this._selectAccount(account);
|
||||
selectButton.style.cssText = `
|
||||
padding: 4px 12px;
|
||||
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: var(--nl-font-family, 'Courier New', monospace);
|
||||
font-family: 'Courier New', monospace;
|
||||
text-align: center;
|
||||
`;
|
||||
selectButton.onmouseover = () => {
|
||||
selectButton.style.borderColor = 'var(--nl-accent-color)';
|
||||
@@ -1944,7 +2048,6 @@ class Modal {
|
||||
actionCell.appendChild(selectButton);
|
||||
|
||||
row.appendChild(indexCell);
|
||||
row.appendChild(pubkeyCell);
|
||||
row.appendChild(actionCell);
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
237
lite/ui/modal.js
237
lite/ui/modal.js
@@ -104,7 +104,7 @@ class Modal {
|
||||
closeButton.style.cssText = `
|
||||
background: var(--nl-secondary-color);
|
||||
border: var(--nl-border-width) solid var(--nl-primary-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
border-radius: 4px;
|
||||
font-size: 28px;
|
||||
color: var(--nl-primary-color);
|
||||
cursor: pointer;
|
||||
@@ -288,21 +288,13 @@ class Modal {
|
||||
};
|
||||
|
||||
const iconDiv = document.createElement('div');
|
||||
// Replace emoji icons with text-based ones
|
||||
const iconMap = {
|
||||
'🔌': '[EXT]',
|
||||
'🔑': '[KEY]',
|
||||
'🌱': '[SEED]',
|
||||
'🌐': '[NET]',
|
||||
'👁️': '[VIEW]',
|
||||
'📱': '[SMS]'
|
||||
};
|
||||
iconDiv.textContent = iconMap[option.icon] || option.icon;
|
||||
// Remove the icon entirely - no emojis or text-based icons
|
||||
iconDiv.textContent = '';
|
||||
iconDiv.style.cssText = `
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-right: 16px;
|
||||
width: 50px;
|
||||
width: 0px;
|
||||
text-align: center;
|
||||
color: var(--nl-primary-color);
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
@@ -1197,10 +1189,6 @@ class Modal {
|
||||
_showConnectScreen() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Connect to NIP-46 Remote Signer';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = 'Connect to a remote signer (bunker) server to use its keys for signing.';
|
||||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||||
@@ -1225,12 +1213,67 @@ class Modal {
|
||||
box-sizing: border-box;
|
||||
`;
|
||||
|
||||
// Users will enter the complete bunker connection string with relay info
|
||||
// Add real-time bunker key validation
|
||||
const formatHint = document.createElement('div');
|
||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||||
|
||||
const connectButton = document.createElement('button');
|
||||
connectButton.textContent = 'Connect to Bunker';
|
||||
connectButton.onclick = () => this._handleNip46Connect(pubkeyInput.value);
|
||||
connectButton.style.cssText = this._getButtonStyle();
|
||||
connectButton.disabled = true;
|
||||
connectButton.onclick = () => {
|
||||
if (!connectButton.disabled) {
|
||||
this._handleNip46Connect(pubkeyInput.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial disabled state
|
||||
connectButton.style.cssText = `
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-muted-color);
|
||||
margin-bottom: 12px;
|
||||
`;
|
||||
|
||||
pubkeyInput.oninput = () => {
|
||||
const value = pubkeyInput.value.trim();
|
||||
if (!value) {
|
||||
formatHint.textContent = '';
|
||||
// Disable button
|
||||
connectButton.disabled = true;
|
||||
connectButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
connectButton.style.color = 'var(--nl-muted-color)';
|
||||
connectButton.style.cursor = 'not-allowed';
|
||||
return;
|
||||
}
|
||||
|
||||
const isValid = this._validateBunkerKey(value);
|
||||
if (isValid) {
|
||||
formatHint.textContent = '✅ Valid bunker connection format detected';
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
connectButton.disabled = false;
|
||||
connectButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
connectButton.style.color = 'var(--nl-primary-color)';
|
||||
connectButton.style.cursor = 'pointer';
|
||||
} else {
|
||||
formatHint.textContent = '❌ Invalid format - must be bunker://, npub, or 64-char hex';
|
||||
formatHint.style.color = '#dc2626';
|
||||
// Disable button
|
||||
connectButton.disabled = true;
|
||||
connectButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
connectButton.style.color = 'var(--nl-muted-color)';
|
||||
connectButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
@@ -1239,14 +1282,49 @@ class Modal {
|
||||
|
||||
formGroup.appendChild(label);
|
||||
formGroup.appendChild(pubkeyInput);
|
||||
formGroup.appendChild(formatHint);
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
this.modalBody.appendChild(formGroup);
|
||||
this.modalBody.appendChild(connectButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
}
|
||||
|
||||
_validateBunkerKey(bunkerKey) {
|
||||
try {
|
||||
const trimmed = bunkerKey.trim();
|
||||
|
||||
// Check for bunker:// format
|
||||
if (trimmed.startsWith('bunker://')) {
|
||||
// Should have format: bunker://pubkey or bunker://pubkey?param=value
|
||||
const match = trimmed.match(/^bunker:\/\/([0-9a-fA-F]{64})(\?.*)?$/);
|
||||
return !!match;
|
||||
}
|
||||
|
||||
// Check for npub format
|
||||
if (trimmed.startsWith('npub1') && trimmed.length === 63) {
|
||||
try {
|
||||
if (window.NostrTools?.nip19) {
|
||||
const decoded = window.NostrTools.nip19.decode(trimmed);
|
||||
return decoded.type === 'npub';
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for hex format (64 characters, valid hex)
|
||||
if (trimmed.length === 64 && /^[a-fA-F0-9]{64}$/.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.log('Bunker key validation failed:', error.message);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
_handleNip46Connect(bunkerPubkey) {
|
||||
if (!bunkerPubkey || !bunkerPubkey.length) {
|
||||
this._showError('Bunker pubkey is required');
|
||||
@@ -1316,9 +1394,9 @@ class Modal {
|
||||
const localSecretKey = window.NostrTools.generateSecretKey();
|
||||
console.log('Generated local client keypair for NIP-46 session');
|
||||
|
||||
// Use nostr-tools BunkerSigner constructor
|
||||
// Use nostr-tools BunkerSigner factory method (not constructor - it's private)
|
||||
console.log('Creating nip46 BunkerSigner...');
|
||||
const signer = new window.NostrTools.nip46.BunkerSigner(localSecretKey, bunkerPointer, {
|
||||
const signer = window.NostrTools.nip46.BunkerSigner.fromBunker(localSecretKey, bunkerPointer, {
|
||||
onauth: (url) => {
|
||||
console.log('Received auth URL from bunker:', url);
|
||||
// Open auth URL in popup or redirect
|
||||
@@ -1398,12 +1476,8 @@ class Modal {
|
||||
_showSeedPhraseScreen() {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Import from Seed Phrase';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts:';
|
||||
description.innerHTML = 'Enter your 12 or 24-word mnemonic seed phrase to derive Nostr accounts, or <span id="generate-new" style="text-decoration: underline; cursor: pointer; color: var(--nl-primary-color);">generate new</span>.';
|
||||
description.style.cssText = 'margin-bottom: 12px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
const textarea = document.createElement('textarea');
|
||||
@@ -1426,10 +1500,40 @@ class Modal {
|
||||
const formatHint = document.createElement('div');
|
||||
formatHint.style.cssText = 'margin-bottom: 16px; font-size: 12px; color: #6b7280; min-height: 16px;';
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Accounts';
|
||||
importButton.disabled = true;
|
||||
importButton.onclick = () => {
|
||||
if (!importButton.disabled) {
|
||||
this._importFromSeedPhrase(textarea.value);
|
||||
}
|
||||
};
|
||||
|
||||
// Set initial disabled state
|
||||
importButton.style.cssText = `
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: var(--nl-border-width) solid var(--nl-muted-color);
|
||||
border-radius: var(--nl-border-radius);
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
cursor: not-allowed;
|
||||
transition: all 0.2s;
|
||||
font-family: var(--nl-font-family, 'Courier New', monospace);
|
||||
background: var(--nl-secondary-color);
|
||||
color: var(--nl-muted-color);
|
||||
`;
|
||||
|
||||
textarea.oninput = () => {
|
||||
const value = textarea.value.trim();
|
||||
if (!value) {
|
||||
formatHint.textContent = '';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1438,35 +1542,46 @@ class Modal {
|
||||
const wordCount = value.split(/\s+/).length;
|
||||
formatHint.textContent = `✅ Valid ${wordCount}-word mnemonic detected`;
|
||||
formatHint.style.color = '#059669';
|
||||
// Enable button
|
||||
importButton.disabled = false;
|
||||
importButton.style.borderColor = 'var(--nl-primary-color)';
|
||||
importButton.style.color = 'var(--nl-primary-color)';
|
||||
importButton.style.cursor = 'pointer';
|
||||
} else {
|
||||
formatHint.textContent = '❌ Invalid mnemonic - must be 12 or 24 valid BIP-39 words';
|
||||
formatHint.style.color = '#dc2626';
|
||||
// Disable button
|
||||
importButton.disabled = true;
|
||||
importButton.style.borderColor = 'var(--nl-muted-color)';
|
||||
importButton.style.color = 'var(--nl-muted-color)';
|
||||
importButton.style.cursor = 'not-allowed';
|
||||
}
|
||||
};
|
||||
|
||||
// Generate new seed phrase button
|
||||
const generateButton = document.createElement('button');
|
||||
generateButton.textContent = 'Generate New Seed Phrase';
|
||||
generateButton.onclick = () => this._generateNewSeedPhrase(textarea, formatHint);
|
||||
generateButton.style.cssText = this._getButtonStyle() + 'margin-bottom: 12px;';
|
||||
|
||||
const importButton = document.createElement('button');
|
||||
importButton.textContent = 'Import Accounts';
|
||||
importButton.onclick = () => this._importFromSeedPhrase(textarea.value);
|
||||
importButton.style.cssText = this._getButtonStyle();
|
||||
|
||||
const backButton = document.createElement('button');
|
||||
backButton.textContent = 'Back';
|
||||
backButton.onclick = () => this._renderLoginOptions();
|
||||
backButton.style.cssText = this._getButtonStyle('secondary') + 'margin-top: 12px;';
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
this.modalBody.appendChild(textarea);
|
||||
this.modalBody.appendChild(formatHint);
|
||||
this.modalBody.appendChild(generateButton);
|
||||
this.modalBody.appendChild(importButton);
|
||||
this.modalBody.appendChild(backButton);
|
||||
|
||||
// Add click handler for the "generate new" link
|
||||
const generateLink = document.getElementById('generate-new');
|
||||
if (generateLink) {
|
||||
generateLink.addEventListener('mouseenter', () => {
|
||||
generateLink.style.color = 'var(--nl-accent-color)';
|
||||
});
|
||||
generateLink.addEventListener('mouseleave', () => {
|
||||
generateLink.style.color = 'var(--nl-primary-color)';
|
||||
});
|
||||
generateLink.addEventListener('click', () => {
|
||||
this._generateNewSeedPhrase(textarea, formatHint);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_generateNewSeedPhrase(textarea, formatHint) {
|
||||
@@ -1482,12 +1597,12 @@ class Modal {
|
||||
// Set the generated mnemonic in the textarea
|
||||
textarea.value = mnemonic;
|
||||
|
||||
// Trigger validation to show it's valid
|
||||
const wordCount = mnemonic.split(/\s+/).length;
|
||||
formatHint.textContent = `✅ Generated valid ${wordCount}-word mnemonic`;
|
||||
formatHint.style.color = '#059669';
|
||||
// Trigger the oninput event to properly validate and enable the button
|
||||
if (textarea.oninput) {
|
||||
textarea.oninput();
|
||||
}
|
||||
|
||||
console.log('Generated new seed phrase:', wordCount, 'words');
|
||||
console.log('Generated new seed phrase:', mnemonic.split(/\s+/).length, 'words');
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to generate seed phrase:', error);
|
||||
@@ -1569,15 +1684,10 @@ class Modal {
|
||||
_showAccountSelection(accounts) {
|
||||
this.modalBody.innerHTML = '';
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = 'Select Account';
|
||||
title.style.cssText = 'margin: 0 0 16px 0; font-size: 18px; font-weight: 600;';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = `Select which account to use (${accounts.length} accounts derived from seed phrase):`;
|
||||
description.style.cssText = 'margin-bottom: 20px; color: #6b7280; font-size: 14px;';
|
||||
|
||||
this.modalBody.appendChild(title);
|
||||
this.modalBody.appendChild(description);
|
||||
|
||||
// Create table for account selection
|
||||
@@ -1595,8 +1705,7 @@ class Modal {
|
||||
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: left; border: 1px solid #d1d5db; font-weight: bold;">Public Key (npub)</th>
|
||||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Action</th>
|
||||
<th style="padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;">Use</th>
|
||||
</tr>
|
||||
`;
|
||||
table.appendChild(thead);
|
||||
@@ -1611,31 +1720,26 @@ class Modal {
|
||||
indexCell.textContent = account.index;
|
||||
indexCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db; font-weight: bold;';
|
||||
|
||||
const pubkeyCell = document.createElement('td');
|
||||
pubkeyCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db; font-family: monospace; word-break: break-all;';
|
||||
|
||||
// Show truncated npub for readability
|
||||
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
|
||||
pubkeyCell.innerHTML = `
|
||||
<code style="background: #f3f4f6; padding: 2px 4px; border-radius: 2px;">${truncatedNpub}</code><br>
|
||||
<small style="color: #6b7280;">Full: ${account.npub}</small>
|
||||
`;
|
||||
|
||||
const actionCell = document.createElement('td');
|
||||
actionCell.style.cssText = 'padding: 8px; text-align: center; border: 1px solid #d1d5db;';
|
||||
actionCell.style.cssText = 'padding: 8px; border: 1px solid #d1d5db;';
|
||||
|
||||
// Show truncated npub in the button
|
||||
const truncatedNpub = `${account.npub.slice(0, 12)}...${account.npub.slice(-8)}`;
|
||||
|
||||
const selectButton = document.createElement('button');
|
||||
selectButton.textContent = 'Use';
|
||||
selectButton.textContent = truncatedNpub;
|
||||
selectButton.onclick = () => this._selectAccount(account);
|
||||
selectButton.style.cssText = `
|
||||
padding: 4px 12px;
|
||||
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: var(--nl-font-family, 'Courier New', monospace);
|
||||
font-family: 'Courier New', monospace;
|
||||
text-align: center;
|
||||
`;
|
||||
selectButton.onmouseover = () => {
|
||||
selectButton.style.borderColor = 'var(--nl-accent-color)';
|
||||
@@ -1647,7 +1751,6 @@ class Modal {
|
||||
actionCell.appendChild(selectButton);
|
||||
|
||||
row.appendChild(indexCell);
|
||||
row.appendChild(pubkeyCell);
|
||||
row.appendChild(actionCell);
|
||||
tbody.appendChild(row);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user