Skip to content

Commit 9d7f4bc

Browse files
vveerrggclaude
andcommitted
feat: add LoginWithNostr embed script, onboarding page, and new NIP-07 APIs
- docs/login.js: drop-in LoginWithNostr button (<div id="nostr-login">) with dark/light themes, extension detection, install redirect + polling, and nostr:login CustomEvent on successful auth - docs/onboard.html: 3-step onboarding flow (install → create identity → connect relay) with platform detection and ?relay=&ref= params - window.nostr.addRelay(url): programmatically add a wss:// relay to the active profile (validates URL, deduplicates) - window.nostr.exportProfile(): export active profile data (name, npub, nsec, relays) for backup — requires unlocked extension - Updated nostr.js, content.js, background.js message passing for both new methods (bypass private key requirement) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a35b7d5 commit 9d7f4bc

5 files changed

Lines changed: 1050 additions & 1 deletion

File tree

docs/login.js

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
/**
2+
* LoginWithNostr — Drop-in "Login with Nostr" button for any website.
3+
*
4+
* Usage:
5+
* <script src="https://nostrkey.com/login.js"></script>
6+
* <div id="nostr-login"></div>
7+
*
8+
* Or with custom options:
9+
* <div id="nostr-login"
10+
* data-relay="wss://relay.example.com"
11+
* data-theme="dark"
12+
* data-size="large">
13+
* </div>
14+
*
15+
* Events:
16+
* document.addEventListener('nostr:login', function(e) {
17+
* console.log('Logged in:', e.detail.pubkey);
18+
* });
19+
*
20+
* document.addEventListener('nostr:logout', function(e) {
21+
* console.log('Logged out');
22+
* });
23+
*/
24+
(function () {
25+
'use strict';
26+
27+
var INSTALL_URL = 'https://nostrkey.com/onboard';
28+
var CHECK_INTERVAL = 2000;
29+
var CHECK_TIMEOUT = 120000;
30+
31+
// --- Styles ---
32+
var STYLES = {
33+
dark: {
34+
bg: '#3e3d32',
35+
bgHover: '#4a4940',
36+
text: '#f8f8f2',
37+
accent: '#a6e22e',
38+
border: 'none',
39+
},
40+
light: {
41+
bg: '#ffffff',
42+
bgHover: '#f5f5f5',
43+
text: '#1a1a1a',
44+
accent: '#5a8a10',
45+
border: '1px solid #e0e0e0',
46+
},
47+
};
48+
49+
function injectStyles() {
50+
if (document.getElementById('nostr-login-styles')) return;
51+
var style = document.createElement('style');
52+
style.id = 'nostr-login-styles';
53+
style.textContent = [
54+
'.nostr-login-btn {',
55+
' display: inline-flex;',
56+
' align-items: center;',
57+
' gap: 10px;',
58+
' padding: 12px 24px;',
59+
' border-radius: 10px;',
60+
' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
61+
' font-size: 15px;',
62+
' font-weight: 600;',
63+
' cursor: pointer;',
64+
' transition: transform 0.15s, box-shadow 0.15s, background 0.15s;',
65+
' text-decoration: none;',
66+
' line-height: 1;',
67+
'}',
68+
'.nostr-login-btn:hover {',
69+
' transform: translateY(-1px);',
70+
' box-shadow: 0 4px 12px rgba(0,0,0,0.15);',
71+
'}',
72+
'.nostr-login-btn:active {',
73+
' transform: translateY(0);',
74+
'}',
75+
'.nostr-login-btn.large {',
76+
' padding: 16px 32px;',
77+
' font-size: 17px;',
78+
'}',
79+
'.nostr-login-btn.small {',
80+
' padding: 8px 16px;',
81+
' font-size: 13px;',
82+
'}',
83+
'.nostr-login-icon {',
84+
' width: 20px;',
85+
' height: 20px;',
86+
' flex-shrink: 0;',
87+
'}',
88+
'.nostr-login-btn.large .nostr-login-icon {',
89+
' width: 24px;',
90+
' height: 24px;',
91+
'}',
92+
'.nostr-login-status {',
93+
' font-size: 13px;',
94+
' margin-top: 8px;',
95+
' opacity: 0.7;',
96+
'}',
97+
].join('\n');
98+
document.head.appendChild(style);
99+
}
100+
101+
var NOSTR_ICON = '<svg class="nostr-login-icon" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">' +
102+
'<circle cx="50" cy="50" r="45" stroke="currentColor" stroke-width="4" fill="none" opacity="0.3"/>' +
103+
'<path d="M35 50L45 60L65 40" stroke="currentColor" stroke-width="6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>' +
104+
'</svg>';
105+
106+
// --- Core ---
107+
function hasNostr() {
108+
return typeof window.nostr !== 'undefined' && typeof window.nostr.getPublicKey === 'function';
109+
}
110+
111+
function createButton(container) {
112+
var theme = container.getAttribute('data-theme') || 'dark';
113+
var size = container.getAttribute('data-size') || '';
114+
var relay = container.getAttribute('data-relay') || '';
115+
var colors = STYLES[theme] || STYLES.dark;
116+
117+
var btn = document.createElement('button');
118+
btn.className = 'nostr-login-btn' + (size ? ' ' + size : '');
119+
btn.style.background = colors.bg;
120+
btn.style.color = colors.text;
121+
btn.style.border = colors.border;
122+
btn.innerHTML = NOSTR_ICON + '<span>Login with Nostr</span>';
123+
124+
btn.addEventListener('mouseover', function () { btn.style.background = colors.bgHover; });
125+
btn.addEventListener('mouseout', function () { btn.style.background = colors.bg; });
126+
127+
btn.addEventListener('click', function () {
128+
if (!hasNostr()) {
129+
// Extension not installed — redirect to onboarding
130+
var onboardUrl = INSTALL_URL;
131+
if (relay) {
132+
onboardUrl += '?relay=' + encodeURIComponent(relay) +
133+
'&ref=' + encodeURIComponent(window.location.hostname);
134+
}
135+
window.open(onboardUrl, '_blank');
136+
137+
// Start polling for extension install
138+
var status = container.querySelector('.nostr-login-status');
139+
if (!status) {
140+
status = document.createElement('div');
141+
status.className = 'nostr-login-status';
142+
status.style.color = colors.text;
143+
container.appendChild(status);
144+
}
145+
status.textContent = 'Install NostrKey, then come back here...';
146+
147+
var start = Date.now();
148+
var poll = setInterval(function () {
149+
if (hasNostr()) {
150+
clearInterval(poll);
151+
status.textContent = '';
152+
doLogin(container, relay);
153+
} else if (Date.now() - start > CHECK_TIMEOUT) {
154+
clearInterval(poll);
155+
status.textContent = 'Timed out. Refresh and try again.';
156+
}
157+
}, CHECK_INTERVAL);
158+
return;
159+
}
160+
doLogin(container, relay);
161+
});
162+
163+
container.appendChild(btn);
164+
}
165+
166+
function doLogin(container, relay) {
167+
window.nostr.getPublicKey().then(function (pubkey) {
168+
// Add relay if specified
169+
if (relay && window.nostr.addRelay) {
170+
window.nostr.addRelay(relay).catch(function () {});
171+
}
172+
173+
// Dispatch login event
174+
var event = new CustomEvent('nostr:login', {
175+
detail: { pubkey: pubkey },
176+
bubbles: true,
177+
});
178+
document.dispatchEvent(event);
179+
container.dispatchEvent(event);
180+
181+
// Update button to show logged-in state
182+
var btn = container.querySelector('.nostr-login-btn');
183+
if (btn) {
184+
btn.innerHTML = NOSTR_ICON +
185+
'<span>' + pubkey.slice(0, 8) + '...' + pubkey.slice(-4) + '</span>';
186+
btn.style.opacity = '0.8';
187+
btn.style.cursor = 'default';
188+
}
189+
}).catch(function (err) {
190+
var status = container.querySelector('.nostr-login-status');
191+
if (!status) {
192+
status = document.createElement('div');
193+
status.className = 'nostr-login-status';
194+
container.appendChild(status);
195+
}
196+
status.textContent = 'Login cancelled or denied.';
197+
setTimeout(function () { status.textContent = ''; }, 3000);
198+
});
199+
}
200+
201+
// --- Init ---
202+
function init() {
203+
injectStyles();
204+
var containers = document.querySelectorAll('[id="nostr-login"], [data-nostr-login]');
205+
for (var i = 0; i < containers.length; i++) {
206+
createButton(containers[i]);
207+
}
208+
}
209+
210+
if (document.readyState === 'loading') {
211+
document.addEventListener('DOMContentLoaded', init);
212+
} else {
213+
init();
214+
}
215+
})();

0 commit comments

Comments
 (0)