import browser from 'webextension-polyfill' import { useState, useCallback, useEffect } from 'react' import { render } from 'react-dom' import { generateSecretKey, nip19, utils } from 'nostr-tools' import QRCode from 'react-qr-code' import * as Tabs from '@radix-ui/react-tabs' import { LogoIcon } from './icons' import { removePermissions } from './common' import * as Checkbox from '@radix-ui/react-checkbox' 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) => { messages.push(msg) setMessages(messages) setTimeout(() => setMessages([]), 3000) }) const loadPermissions = useCallback(async () => { const { policies = {} } = await browser.storage.local.get('policies') const list = [] Object.entries(policies).forEach(([host, accepts]) => { Object.entries(accepts).forEach(([accept, types]) => { Object.entries(types).forEach(([type, { conditions, created_at }]) => { list.push({ host, type, accept, conditions, created_at }) }) }) }) setPermissions(list) }, []) 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() }, [loadPermissions]) 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 && ( )}
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(' ')}
N/A N/A N/A N/A N/A
)}
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() { const sk = generateSecretKey() setPrivKey(nip19.nsecEncode(utils.bytesToHex(sk))) 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([]) } } render(, document.getElementById('main'))