import * as Checkbox from "@radix-ui/react-checkbox"; import * as Tabs from "@radix-ui/react-tabs"; import { nip19, generateSecretKey } from "nostr-tools"; import { useState, useCallback, useEffect } from "react"; import QRCode from "react-qr-code"; import browser from "webextension-polyfill"; import { removePermissions } from "./common"; import { LogoIcon } from "./icons"; function Options() { const [privKey, setPrivKey] = useState(""); const [relays, setRelays] = useState([]); const [newRelayURL, setNewRelayURL] = useState(""); const [policies, setPermissions] = useState([]); const [protocolHandler, setProtocolHandler] = useState( "https://njump.me/{raw}", ); const [hidingPrivateKey, hidePrivateKey] = useState(true); const [showNotifications, setNotifications] = useState(false); const [messages, setMessages] = useState([]); const [handleNostrLinks, setHandleNostrLinks] = useState(false); const [showProtocolHandlerHelp, setShowProtocolHandlerHelp] = useState(false); const [unsavedChanges, setUnsavedChanges] = useState([]); const showMessage = useCallback((msg: string) => { messages.push(msg); setMessages(messages); setTimeout(() => setMessages([]), 3000); }, []); useEffect(() => { browser.storage.local .get(["private_key", "relays", "protocol_handler", "notifications"]) .then((results) => { if (results.private_key) { setPrivKey(nip19.nsecEncode(results.private_key)); } if (results.relays) { const relaysList = []; for (const url in results.relays) { relaysList.push({ url, policy: results.relays[url], }); } setRelays(relaysList); } if (results.protocol_handler) { setProtocolHandler(results.protocol_handler); setHandleNostrLinks(true); setShowProtocolHandlerHelp(false); } if (results.notifications) { setNotifications(true); } }); }, []); useEffect(() => { loadPermissions(); }, []); async function loadPermissions() { const { policies = {} } = await browser.storage.local.get("policies"); const list = []; // biome-ignore lint/complexity/noForEach: TODO: fix this Object.entries(policies).forEach(([host, accepts]) => { // biome-ignore lint/complexity/noForEach: TODO: fix this Object.entries(accepts).forEach(([accept, types]) => { // biome-ignore lint/complexity/noForEach: TODO: fix this Object.entries(types).forEach(([type, { conditions, created_at }]) => { list.push({ host, type, accept, conditions, created_at, }); }); }); }); setPermissions(list); } return (

Nostr Connect

Nostr signer

Private key:
{!privKey && ( )} {privKey && hidingPrivateKey && ( )} {privKey && !hidingPrivateKey && ( )}
{privKey && !isKeyValid() ? (

Private key is invalid!

) : (

Your key is stored locally. The developer has no way of seeing your keys.

)}
{!hidingPrivateKey && isKeyValid() && (
)}
Relays {relays.length} Permissions {policies.length}
Preferred Relays:
{relays.map(({ url, policy }, i) => (
))}
setNewRelayURL(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") addNewRelay(); }} placeholder="wss://" className="flex-1 h-9 bg-transparent border px-3 py-1 border-primary rounded-lg placeholder:text-muted" />
Permissions:
{!policies.length ? (
You haven't granted any permissions to any apps yet
) : ( {policies.map( ({ host, type, accept, conditions, created_at }) => ( ), )} {!policies.length && ( {Array(5) .fill("N/A") .map((v) => ( ))} )}
Domain Permission Answer Conditions Since
{host} {type} {accept === "true" ? "allow" : "deny"} {conditions.kinds ? `kinds: ${Object.keys(conditions.kinds).join( ", ", )}` : "always"} {new Date(created_at * 1000) .toISOString() .split(".")[0] .split("T") .join(" ")}
{v}
)}
Advanced
{handleNostrLinks && (
{!showProtocolHandlerHelp && ( )}
{showProtocolHandlerHelp && (
{`
{raw} = anything after the colon, i.e. the full nip19 bech32 string
{hex} = hex pubkey for npub or nprofile, hex event id for note or nevent
{p_or_e} = "p" for npub or nprofile, "e" for note or nevent
{u_or_n} = "u" for npub or nprofile, "n" for note or nevent
{relay0} = first relay in a nprofile or nevent
{relay1} = second relay in a nprofile or nevent
{relay2} = third relay in a nprofile or nevent
{hrp} = human-readable prefix of the nip19 string

examples:
  - https://njump.me/{raw}
  - https://snort.social/{raw}
  - https://nostr.band/{raw}
                `}
)}
)}
); async function handleKeyChange(e) { const key = e.target.value.toLowerCase().trim(); setPrivKey(key); addUnsavedChanges("private_key"); } async function generate() { setPrivKey(nip19.nsecEncode(generateSecretKey())); addUnsavedChanges("private_key"); } async function saveKey() { if (!isKeyValid()) { showMessage("PRIVATE KEY IS INVALID! did not save private key."); return; } let hexOrEmptyKey = privKey; try { const { type, data } = nip19.decode(privKey); if (type === "nsec") hexOrEmptyKey = data; } catch (_) {} await browser.storage.local.set({ private_key: hexOrEmptyKey, }); if (hexOrEmptyKey !== "") { setPrivKey(nip19.nsecEncode(hexOrEmptyKey)); } showMessage("saved private key!"); } function isKeyValid() { if (privKey === "") return true; if (privKey.match(/^[a-f0-9]{64}$/)) return true; try { if (nip19.decode(privKey).type === "nsec") return true; } catch (_) {} return false; } function changeRelayURL(i, ev) { setRelays([ ...relays.slice(0, i), { url: ev.target.value, policy: relays[i].policy }, ...relays.slice(i + 1), ]); addUnsavedChanges("relays"); } function toggleRelayPolicy(i, cat) { setRelays([ ...relays.slice(0, i), { url: relays[i].url, policy: { ...relays[i].policy, [cat]: !relays[i].policy[cat] }, }, ...relays.slice(i + 1), ]); addUnsavedChanges("relays"); } function removeRelay(i) { setRelays([...relays.slice(0, i), ...relays.slice(i + 1)]); addUnsavedChanges("relays"); } function addNewRelay() { if (newRelayURL.trim() === "") return; if (!newRelayURL.startsWith("wss://")) return; relays.push({ url: newRelayURL, policy: { read: true, write: true }, }); setRelays(relays); addUnsavedChanges("relays"); setNewRelayURL(""); } async function handleRevoke(e) { const { host, accept, type } = e.target.dataset; if ( window.confirm( `revoke all ${ accept === "true" ? "accept" : "deny" } ${type} policies from ${host}?`, ) ) { await removePermissions(host, accept, type); showMessage("removed policies"); loadPermissions(); } } function handleNotifications() { setNotifications(!showNotifications); addUnsavedChanges("notifications"); if (!showNotifications) requestBrowserNotificationPermissions(); } async function requestBrowserNotificationPermissions() { const granted = await browser.permissions.request({ permissions: ["notifications"], }); if (!granted) setNotifications(false); } async function saveNotifications() { await browser.storage.local.set({ notifications: showNotifications }); showMessage("saved notifications!"); } async function saveRelays() { await browser.storage.local.set({ relays: Object.fromEntries( relays .filter(({ url }) => url.trim() !== "") .map(({ url, policy }) => [url.trim(), policy]), ), }); showMessage("saved relays!"); } function changeShowProtocolHandlerHelp() { setShowProtocolHandlerHelp(true); } function changeHandleNostrLinks() { if (handleNostrLinks) { setProtocolHandler(""); addUnsavedChanges("protocol_handler"); } else setShowProtocolHandlerHelp(true); setHandleNostrLinks(!handleNostrLinks); } function handleChangeProtocolHandler(e) { setProtocolHandler(e.target.value); addUnsavedChanges("protocol_handler"); } async function saveNostrProtocolHandlerSettings() { await browser.storage.local.set({ protocol_handler: protocolHandler }); showMessage("saved protocol handler!"); } function addUnsavedChanges(section) { if (!unsavedChanges.find((s) => s === section)) { unsavedChanges.push(section); setUnsavedChanges(unsavedChanges); } } async function saveChanges() { for (const section of unsavedChanges) { switch (section) { case "private_key": await saveKey(); break; case "relays": await saveRelays(); break; case "protocol_handler": await saveNostrProtocolHandlerSettings(); break; case "notifications": await saveNotifications(); break; } } setUnsavedChanges([]); } } const container = document.getElementById("main"); const root = createRoot(container); root.render();