Merge pull request #1 from reyamir/nostr-connect
Implemented nostr connect redesign
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
*.build.js
|
||||
*.zip
|
||||
/extension/build/style.css
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
module.exports = api => {
|
||||
return {
|
||||
presets: [
|
||||
|
||||
5
build.js
@@ -14,11 +14,12 @@ esbuild
|
||||
'background.build': './extension/background.js',
|
||||
'content-script.build': './extension/content-script.js'
|
||||
},
|
||||
outdir: './extension',
|
||||
outdir: './extension/build',
|
||||
sourcemap: prod ? false : 'inline',
|
||||
define: {
|
||||
window: 'self',
|
||||
global: 'self'
|
||||
}
|
||||
},
|
||||
watch: !prod
|
||||
})
|
||||
.then(() => console.log('build success.'))
|
||||
|
||||
BIN
extension/.DS_Store
vendored
Normal file
@@ -130,8 +130,8 @@ async function handleContentScriptMessage({type, params, host}) {
|
||||
browser.windows.create({
|
||||
url: `${browser.runtime.getURL('prompt.html')}?${qs.toString()}`,
|
||||
type: 'popup',
|
||||
width: 340,
|
||||
height: 360
|
||||
width: 600,
|
||||
height: 600
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
87
extension/icons.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react'
|
||||
|
||||
export function LogoIcon() {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="56"
|
||||
height="56"
|
||||
fill="none"
|
||||
viewBox="0 0 56 56"
|
||||
>
|
||||
<rect width="56" height="56" fill="#EEECFD" rx="16"></rect>
|
||||
<rect
|
||||
width="55"
|
||||
height="55"
|
||||
x="0.5"
|
||||
y="0.5"
|
||||
stroke="#5A41F4"
|
||||
strokeOpacity="0.25"
|
||||
rx="15.5"
|
||||
></rect>
|
||||
<rect
|
||||
width="39"
|
||||
height="39"
|
||||
x="8.5"
|
||||
y="8.5"
|
||||
fill="url(#paint0_linear_24_2379)"
|
||||
rx="19.5"
|
||||
></rect>
|
||||
<rect
|
||||
width="39"
|
||||
height="39"
|
||||
x="8.5"
|
||||
y="8.5"
|
||||
stroke="#6149F6"
|
||||
rx="19.5"
|
||||
></rect>
|
||||
<g fill="#fff" stroke="#6149F6" clipPath="url(#clip0_24_2379)">
|
||||
<path d="M23.78 20.634l.408-.235-.21-.422a4.432 4.432 0 01-.458-1.797l-.031-.78-.696.355A11.533 11.533 0 0016.5 27.998h0V28c.002.87.103 1.738.302 2.585a3.525 3.525 0 102.843-1.058A8.377 8.377 0 0119.5 28a8.523 8.523 0 014.28-7.366zM36.5 28.023v.468l.467.03c.621.042 1.227.212 1.778.5l.687.36.044-.774.005-.075c.01-.166.02-.349.02-.532v-.001a11.524 11.524 0 00-8.142-10.99 3.526 3.526 0 10-.501 2.989A8.524 8.524 0 0136.5 28s0 0 0 0v.022zM33.185 32.622a3.49 3.49 0 00.311 1.844 8.442 8.442 0 01-9.766.877l-.407-.239-.262.392c-.343.514-.79.95-1.311 1.282l-.652.414.645.425a11.39 11.39 0 0014.092-1.23c.264.069.536.107.81.113h.01a3.5 3.5 0 002.803-5.6h.556l-1.603-.932a3.49 3.49 0 00-5.226 2.654z"></path>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_24_2379"
|
||||
x1="28"
|
||||
x2="28"
|
||||
y1="8"
|
||||
y2="48"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#8E7CFF"></stop>
|
||||
<stop offset="1" stopColor="#5A41F4"></stop>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_24_2379">
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M0 0H24V24H0z"
|
||||
transform="translate(16 15)"
|
||||
></path>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function SettingsIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.213 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z"
|
||||
/>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
BIN
extension/icons/.DS_Store
vendored
Normal file
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
BIN
extension/icons/icon128.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
extension/icons/icon16.png
Normal file
|
After Width: | Height: | Size: 643 B |
BIN
extension/icons/icon32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
extension/icons/icon48.png
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"name": "nos2x",
|
||||
"name": "Nostr Connect",
|
||||
"description": "Nostr Signer Extension",
|
||||
"version": "2.2.0",
|
||||
"homepage_url": "https://github.com/fiatjaf/nos2x",
|
||||
"version": "0.1.0",
|
||||
"homepage_url": "https://github.com/reyamir/nostr-connect",
|
||||
"manifest_version": 3,
|
||||
"icons": {
|
||||
"16": "icons/16x16.png",
|
||||
"32": "icons/32x32.png",
|
||||
"48": "icons/48x48.png",
|
||||
"128": "icons/128x128.png"
|
||||
"16": "icons/icon16.png",
|
||||
"32": "icons/icon32.png",
|
||||
"48": "icons/icon48.png",
|
||||
"128": "icons/icon128.png"
|
||||
},
|
||||
"options_page": "options.html",
|
||||
"background": {
|
||||
"service_worker": "background.build.js"
|
||||
"service_worker": "/build/background.build.js"
|
||||
},
|
||||
"action": {
|
||||
"default_title": "nos2x",
|
||||
"default_title": "Nostr Connect",
|
||||
"default_popup": "popup.html"
|
||||
},
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": ["<all_urls>"],
|
||||
"js": ["content-script.build.js"],
|
||||
"js": ["/build/content-script.build.js"],
|
||||
"all_frames": true
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<title>nos2x</title>
|
||||
<style>
|
||||
* {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="main" />
|
||||
|
||||
<script src="options.build.js"></script>
|
||||
|
||||
<style>
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th,
|
||||
td {
|
||||
border: 1px solid;
|
||||
padding: 1px 2px;
|
||||
}
|
||||
</style>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Nostr Connect</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="/build/style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-background text-foreground text-sm font-sans antialiased">
|
||||
<div id="main" />
|
||||
<script src="/build/options.build.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -3,8 +3,10 @@ import React, {useState, useCallback, useEffect} from 'react'
|
||||
import {render} from 'react-dom'
|
||||
import {generatePrivateKey, nip19} 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() {
|
||||
let [privKey, setPrivKey] = useState('')
|
||||
@@ -79,252 +81,420 @@ function Options() {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 style={{fontSize: '25px', marginBlockEnd: '0px'}}>nos2x</h1>
|
||||
<p style={{marginBlockStart: '0px'}}>nostr signer extension</p>
|
||||
<h2 style={{marginBlockStart: '20px', marginBlockEnd: '5px'}}>options</h2>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '10px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px',
|
||||
width: 'fit-content'
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div>private key: </div>
|
||||
<div
|
||||
style={{
|
||||
marginLeft: '10px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '10px'
|
||||
}}
|
||||
>
|
||||
<div style={{display: 'flex', gap: '10px'}}>
|
||||
<input
|
||||
type={hidingPrivateKey ? 'password' : 'text'}
|
||||
style={{width: '600px'}}
|
||||
value={privKey}
|
||||
onChange={handleKeyChange}
|
||||
/>
|
||||
{privKey === '' && <button onClick={generate}>generate</button>}
|
||||
{privKey && hidingPrivateKey && (
|
||||
<button onClick={() => hidePrivateKey(false)}>show key</button>
|
||||
)}
|
||||
{privKey && !hidingPrivateKey && (
|
||||
<button onClick={() => hidePrivateKey(true)}>hide key</button>
|
||||
)}
|
||||
</div>
|
||||
{privKey && !isKeyValid() && (
|
||||
<div style={{color: 'red'}}>private key is invalid!</div>
|
||||
)}
|
||||
{!hidingPrivateKey && isKeyValid() && (
|
||||
<div
|
||||
style={{
|
||||
height: 'auto',
|
||||
maxWidth: 256,
|
||||
width: '100%',
|
||||
marginTop: '5px'
|
||||
}}
|
||||
>
|
||||
<QRCode
|
||||
size={256}
|
||||
style={{height: 'auto', maxWidth: '100%', width: '100%'}}
|
||||
value={privKey.toUpperCase()}
|
||||
viewBox={`0 0 256 256`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="w-screen h-screen flex flex-col items-center justify-center">
|
||||
<div className="p-8 shadow-primary border border-primary rounded-2xl max-w-xl mx-auto flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<LogoIcon />
|
||||
<div>
|
||||
<h1 className="text-lg font-semibold">Nostr Connect</h1>
|
||||
<p className="text-sm text-muted font-medium">Nostr signer</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div>preferred relays:</div>
|
||||
<div
|
||||
style={{
|
||||
marginLeft: '10px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1px'
|
||||
}}
|
||||
>
|
||||
{relays.map(({url, policy}, i) => (
|
||||
<div
|
||||
key={i}
|
||||
style={{display: 'flex', alignItems: 'center', gap: '15px'}}
|
||||
>
|
||||
<input
|
||||
style={{width: '400px'}}
|
||||
value={url}
|
||||
onChange={changeRelayURL.bind(null, i)}
|
||||
/>
|
||||
<div style={{display: 'flex', gap: '5px'}}>
|
||||
<label style={{display: 'flex', alignItems: 'center'}}>
|
||||
read
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={policy.read}
|
||||
onChange={toggleRelayPolicy.bind(null, i, 'read')}
|
||||
/>
|
||||
</label>
|
||||
<label style={{display: 'flex', alignItems: 'center'}}>
|
||||
write
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={policy.write}
|
||||
onChange={toggleRelayPolicy.bind(null, i, 'write')}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
<button onClick={removeRelay.bind(null, i)}>remove</button>
|
||||
</div>
|
||||
))}
|
||||
<div style={{display: 'flex', gap: '10px', marginTop: '5px'}}>
|
||||
<input
|
||||
style={{width: '400px'}}
|
||||
value={newRelayURL}
|
||||
onChange={e => setNewRelayURL(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') addNewRelay()
|
||||
}}
|
||||
/>
|
||||
<button disabled={!newRelayURL} onClick={addNewRelay}>
|
||||
add relay
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label style={{display: 'flex', alignItems: 'center'}}>
|
||||
<div className="flex flex-col">
|
||||
<div className="mb-4 flex flex-col gap-2">
|
||||
<div className="font-semibold text-base">Private key:</div>
|
||||
<div>
|
||||
handle{' '}
|
||||
<span style={{padding: '2px', background: 'silver'}}>nostr:</span>{' '}
|
||||
links:
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={handleNostrLinks}
|
||||
onChange={changeHandleNostrLinks}
|
||||
/>
|
||||
</label>
|
||||
<div style={{marginLeft: '10px'}}>
|
||||
{handleNostrLinks && (
|
||||
<div>
|
||||
<div style={{display: 'flex'}}>
|
||||
<input
|
||||
placeholder="url template"
|
||||
value={protocolHandler}
|
||||
onChange={handleChangeProtocolHandler}
|
||||
style={{width: '680px', maxWidth: '90%'}}
|
||||
/>
|
||||
{!showProtocolHandlerHelp && (
|
||||
<button onClick={changeShowProtocolHandlerHelp}>?</button>
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
type={hidingPrivateKey ? 'password' : 'text'}
|
||||
value={privKey}
|
||||
onChange={handleKeyChange}
|
||||
className="flex-1 h-9 bg-transparent border border-primary px-3 py-1 rounded-lg"
|
||||
/>
|
||||
<div className="shrink-0">
|
||||
{!privKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={generate}
|
||||
className="px-3 h-9 font-semibold border w-24 border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
|
||||
>
|
||||
Generate
|
||||
</button>
|
||||
)}
|
||||
{privKey && hidingPrivateKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => hidePrivateKey(false)}
|
||||
className="px-3 h-9 font-bold border w-24 border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
|
||||
>
|
||||
Show key
|
||||
</button>
|
||||
)}
|
||||
{privKey && !hidingPrivateKey && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => hidePrivateKey(true)}
|
||||
className="px-3 h-9 font-bold border w-24 border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
|
||||
>
|
||||
Hide key
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showProtocolHandlerHelp && (
|
||||
<pre>{`
|
||||
{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}
|
||||
`}</pre>
|
||||
</div>
|
||||
<div className="mt-1 text-sm">
|
||||
{privKey && !isKeyValid() ? (
|
||||
<p className="text-red-500">Private key is invalid!</p>
|
||||
) : (
|
||||
<p className="text-gray-500">
|
||||
Your key is stored locally. The developer has no way of
|
||||
seeing your keys.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!hidingPrivateKey && isKeyValid() && (
|
||||
<div className="mt-5 flex flex-col items-center">
|
||||
<QRCode
|
||||
size={256}
|
||||
value={privKey.toUpperCase()}
|
||||
viewBox={`0 0 256 256`}
|
||||
className="w-full max-w-full"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Tabs.Root className="mb-4" defaultValue="relays">
|
||||
<Tabs.List className="mb-4 w-full border-b border-primary h-11 flex items-center gap-6">
|
||||
<Tabs.Trigger
|
||||
className="font-medium flex items-center text-muted gap-2 h-11 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
|
||||
value="relays"
|
||||
>
|
||||
Relays
|
||||
<span className="px-3 h-6 inline-flex items-center justify-center bg-muted data-[state=active]:text-primary rounded-full">
|
||||
{relays.length}
|
||||
</span>
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
className="font-medium flex items-center text-muted gap-2 h-11 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
|
||||
value="permissions"
|
||||
>
|
||||
Permissions
|
||||
<span className="px-3 h-6 inline-flex items-center justify-center bg-muted data-[state=active]:text-primary rounded-full">
|
||||
{policies.length}
|
||||
</span>
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="relays">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="font-semibold text-base">Preferred Relays:</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
{relays.map(({url, policy}, i) => (
|
||||
<div key={i} className="flex items-center gap-4">
|
||||
<input
|
||||
value={url}
|
||||
onChange={changeRelayURL.bind(null, i)}
|
||||
className="flex-1 h-9 bg-transparent border px-3 py-1 border-primary rounded-lg placeholder:text-muted"
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Checkbox.Root
|
||||
id={`read-${i}`}
|
||||
checked={policy.read}
|
||||
onCheckedChange={toggleRelayPolicy.bind(
|
||||
null,
|
||||
i,
|
||||
'read'
|
||||
)}
|
||||
className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
|
||||
>
|
||||
<Checkbox.Indicator className="text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
<label
|
||||
htmlFor={`read-${i}`}
|
||||
className="text-muted font-medium"
|
||||
>
|
||||
Read
|
||||
</label>
|
||||
</div>
|
||||
<div className="inline-flex items-center gap-2">
|
||||
<Checkbox.Root
|
||||
id={`write-${i}`}
|
||||
checked={policy.write}
|
||||
onCheckedChange={toggleRelayPolicy.bind(
|
||||
null,
|
||||
i,
|
||||
'write'
|
||||
)}
|
||||
className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
|
||||
>
|
||||
<Checkbox.Indicator className="text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
<label
|
||||
htmlFor={`write-${i}`}
|
||||
className="text-muted font-medium"
|
||||
>
|
||||
Write
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={removeRelay.bind(null, i)}
|
||||
className="shrink-0 px-3 w-24 h-9 font-semibold border border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
value={newRelayURL}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
<button
|
||||
disabled={!newRelayURL}
|
||||
onClick={addNewRelay}
|
||||
className="shrink-0 px-3 w-24 h-9 font-semibold border border-primary shadow-sm rounded-lg inline-flex items-center justify-center disabled:text-muted"
|
||||
>
|
||||
Add Relay
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="permissions">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="font-semibold text-base">Permissions:</div>
|
||||
{!policies.length ? (
|
||||
<div className="text-muted">
|
||||
You haven't granted any permissions to any apps yet
|
||||
</div>
|
||||
) : (
|
||||
<table className="table-auto">
|
||||
<thead>
|
||||
<tr className="mb-2">
|
||||
<th className="text-left border-b-8 border-transparent">
|
||||
Domain
|
||||
</th>
|
||||
<th className="text-left border-b-8 border-transparent">
|
||||
Permission
|
||||
</th>
|
||||
<th className="text-left border-b-8 border-transparent">
|
||||
Answer
|
||||
</th>
|
||||
<th className="text-left border-b-8 border-transparent">
|
||||
Conditions
|
||||
</th>
|
||||
<th className="text-left border-b-8 border-transparent">
|
||||
Since
|
||||
</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{policies.map(
|
||||
({host, type, accept, conditions, created_at}) => (
|
||||
<tr
|
||||
key={
|
||||
host + type + accept + JSON.stringify(conditions)
|
||||
}
|
||||
>
|
||||
<td className="font-semibold">{host}</td>
|
||||
<td className="text-muted">{type}</td>
|
||||
<td className="text-muted">
|
||||
{accept === 'true' ? 'allow' : 'deny'}
|
||||
</td>
|
||||
<td className="text-muted">
|
||||
{conditions.kinds
|
||||
? `kinds: ${Object.keys(conditions.kinds).join(
|
||||
', '
|
||||
)}`
|
||||
: 'always'}
|
||||
</td>
|
||||
<td className="text-muted">
|
||||
{new Date(created_at * 1000)
|
||||
.toISOString()
|
||||
.split('.')[0]
|
||||
.split('T')
|
||||
.join(' ')}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={handleRevoke}
|
||||
data-host={host}
|
||||
data-accept={accept}
|
||||
data-type={type}
|
||||
className="text-primary font-semibold"
|
||||
>
|
||||
Revoke
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
)}
|
||||
{!policies.length && (
|
||||
<tr>
|
||||
{Array(5)
|
||||
.fill('N/A')
|
||||
.map((v, i) => (
|
||||
<td key={i}>{v}</td>
|
||||
))}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
<div className="mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox.Root
|
||||
id="notification"
|
||||
className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
|
||||
checked={showNotifications}
|
||||
onCheckedChange={handleNotifications}
|
||||
>
|
||||
<Checkbox.Indicator className="text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
<label htmlFor="notification">
|
||||
Show desktop notifications when a permissions has been used
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<details>
|
||||
<summary className="flex items-center justify-between">
|
||||
<div className="font-semibold text-base">Advanced</div>
|
||||
<div>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-5 h-5"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M19.5 8.25l-7.5 7.5-7.5-7.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</summary>
|
||||
<div className="mt-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox.Root
|
||||
id="nostrlink"
|
||||
className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
|
||||
checked={handleNostrLinks}
|
||||
onCheckedChange={changeHandleNostrLinks}
|
||||
>
|
||||
<Checkbox.Indicator className="text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
<label htmlFor="nostrlink">Handle nostr links</label>
|
||||
</div>
|
||||
{handleNostrLinks && (
|
||||
<div className="mt-3">
|
||||
<div className="flex">
|
||||
<input
|
||||
placeholder="url template"
|
||||
value={protocolHandler}
|
||||
onChange={handleChangeProtocolHandler}
|
||||
/>
|
||||
{!showProtocolHandlerHelp && (
|
||||
<button onClick={changeShowProtocolHandlerHelp}>
|
||||
?
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{showProtocolHandlerHelp && (
|
||||
<pre className="bg-muted px-2 rounded-xl overflow-scroll">{`
|
||||
{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}
|
||||
`}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
<label style={{display: 'flex', alignItems: 'center'}}>
|
||||
show notifications when permissions are used:
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showNotifications}
|
||||
onChange={handleNotifications}
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
disabled={!unsavedChanges.length}
|
||||
onClick={saveChanges}
|
||||
style={{padding: '5px 20px'}}
|
||||
className="w-full h-10 bg-primary rounded-xl font-bold inline-flex items-center justify-center text-white disabled:cursor-not-allowed disabled:opacity-70 transform active:translate-y-1 transition-transform ease-in-out duration-75"
|
||||
>
|
||||
save
|
||||
Save
|
||||
</button>
|
||||
<div style={{fontSize: '120%'}}>
|
||||
{messages.map((message, i) => (
|
||||
<div key={i}>{message}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>permissions</h2>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>domain</th>
|
||||
<th>permission</th>
|
||||
<th>answer</th>
|
||||
<th>conditions</th>
|
||||
<th>since</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{policies.map(({host, type, accept, conditions, created_at}) => (
|
||||
<tr key={host + type + accept + JSON.stringify(conditions)}>
|
||||
<td>{host}</td>
|
||||
<td>{type}</td>
|
||||
<td>{accept === 'true' ? 'allow' : 'deny'}</td>
|
||||
<td>
|
||||
{conditions.kinds
|
||||
? `kinds: ${Object.keys(conditions.kinds).join(', ')}`
|
||||
: 'always'}
|
||||
</td>
|
||||
<td>
|
||||
{new Date(created_at * 1000)
|
||||
.toISOString()
|
||||
.split('.')[0]
|
||||
.split('T')
|
||||
.join(' ')}
|
||||
</td>
|
||||
<td>
|
||||
<button
|
||||
onClick={handleRevoke}
|
||||
data-host={host}
|
||||
data-accept={accept}
|
||||
data-type={type}
|
||||
>
|
||||
revoke
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{!policies.length && (
|
||||
<tr>
|
||||
{Array(5)
|
||||
.fill('N/A')
|
||||
.map((v, i) => (
|
||||
<td key={i}>{v}</td>
|
||||
))}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{!policies.length && (
|
||||
<div style={{marginTop: '5px'}}>
|
||||
no permissions have been granted yet
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
|
||||
async function handleKeyChange(e) {
|
||||
@@ -399,6 +569,7 @@ function Options() {
|
||||
|
||||
function addNewRelay() {
|
||||
if (newRelayURL.trim() === '') return
|
||||
if (!newRelayURL.startsWith('wss://')) return
|
||||
relays.push({
|
||||
url: newRelayURL,
|
||||
policy: {read: true, write: true}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<title>nos2x</title>
|
||||
|
||||
<div id="main" />
|
||||
|
||||
<script src="popup.build.js"></script>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Nostr Connect</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="/build/style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-background text-foreground text-sm font-sans antialiased">
|
||||
<div id="main" />
|
||||
<script src="/build/popup.build.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
import browser from 'webextension-polyfill'
|
||||
import {render} from 'react-dom'
|
||||
import {getPublicKey, nip19} from 'nostr-tools'
|
||||
import React, {useState, useRef, useEffect} from 'react'
|
||||
import React, {useState, useMemo, useEffect} from 'react'
|
||||
import QRCode from 'react-qr-code'
|
||||
import {SettingsIcon} from './icons'
|
||||
import {minidenticon} from 'minidenticons'
|
||||
import * as Tabs from '@radix-ui/react-tabs'
|
||||
|
||||
function Popup() {
|
||||
let [pubKey, setPubKey] = useState('')
|
||||
let keys = useRef([])
|
||||
let [keys, setKeys] = useState(null)
|
||||
let avatarURI = useMemo(
|
||||
() =>
|
||||
keys
|
||||
? 'data:image/svg+xml;utf8,' +
|
||||
encodeURIComponent(minidenticon(keys.npub, 90, 30))
|
||||
: null,
|
||||
[keys]
|
||||
)
|
||||
|
||||
const gotoSettings = () => {
|
||||
browser.tabs.create({
|
||||
url: browser.runtime.getURL('/options.html')
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
browser.storage.local.get(['private_key', 'relays']).then(results => {
|
||||
@@ -14,10 +30,7 @@ function Popup() {
|
||||
let hexKey = getPublicKey(results.private_key)
|
||||
let npubKey = nip19.npubEncode(hexKey)
|
||||
|
||||
setPubKey(npubKey)
|
||||
|
||||
keys.current.push(npubKey)
|
||||
keys.current.push(hexKey)
|
||||
setKeys({npub: npubKey, hex: hexKey})
|
||||
|
||||
if (results.relays) {
|
||||
let relaysList = []
|
||||
@@ -32,63 +45,138 @@ function Popup() {
|
||||
pubkey: hexKey,
|
||||
relays: relaysList
|
||||
})
|
||||
keys.current.push(nprofileKey)
|
||||
setKeys(prev => ({...prev, nprofile: nprofileKey}))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setPubKey(null)
|
||||
setKeys(null)
|
||||
}
|
||||
})
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2>nos2x</h2>
|
||||
{pubKey === null ? (
|
||||
<p style={{width: '150px'}}>
|
||||
you don't have a private key set. use the options page to set one.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<a onClick={toggleKeyType}>↩️</a> your public key:
|
||||
</p>
|
||||
<pre
|
||||
style={{
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
width: '200px'
|
||||
}}
|
||||
>
|
||||
<code>{pubKey}</code>
|
||||
</pre>
|
||||
|
||||
<div
|
||||
style={{
|
||||
height: 'auto',
|
||||
margin: '0 auto',
|
||||
maxWidth: 256,
|
||||
width: '100%'
|
||||
}}
|
||||
>
|
||||
<QRCode
|
||||
size={256}
|
||||
style={{height: 'auto', maxWidth: '100%', width: '100%'}}
|
||||
value={pubKey.startsWith('n') ? pubKey.toUpperCase() : pubKey}
|
||||
viewBox={`0 0 256 256`}
|
||||
/>
|
||||
<div className="w-[320px] p-6">
|
||||
{!keys ? (
|
||||
<div className="flex items-center justify-between gap-6">
|
||||
<div className="flex-1 flex items-center justify-between">
|
||||
<p className="text-sm font-medium">
|
||||
Click here to enter or create
|
||||
<br />
|
||||
your first identity
|
||||
</p>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-6 h-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => gotoSettings()}
|
||||
className="w-9 h-9 shrink-0 border border-primary shadow-sm rounded-xl inline-flex items-center justify-center"
|
||||
>
|
||||
<SettingsIcon className="w-5 h-5 text-muted" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
{avatarURI ? (
|
||||
<img
|
||||
src={avatarURI}
|
||||
className="w-9 h-9 rounded-full bg-muted"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-9 h-9 rounded-full bg-muted" />
|
||||
)}
|
||||
<p className="font-semibold">Account</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => gotoSettings()}
|
||||
className="w-9 h-9 shrink-0 border border-primary shadow-sm rounded-xl inline-flex items-center justify-center"
|
||||
>
|
||||
<SettingsIcon className="w-5 h-5 text-muted" />
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<Tabs.Root defaultValue="npub">
|
||||
<Tabs.List className="w-full border-b border-primary h-10 flex items-center">
|
||||
<Tabs.Trigger
|
||||
value="npub"
|
||||
className="font-medium flex-1 flex items-center justify-center text-muted h-10 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
|
||||
>
|
||||
npub
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger
|
||||
value="hex"
|
||||
className="font-medium flex-1 flex items-center justify-center text-muted h-10 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
|
||||
>
|
||||
hex
|
||||
</Tabs.Trigger>
|
||||
{keys.nprofile ? (
|
||||
<Tabs.Trigger
|
||||
value="nprofile"
|
||||
className="font-medium flex-1 flex items-center justify-center text-muted h-10 data-[state=active]:text-primary data-[state=active]:border-b data-[state=active]:border-secondary"
|
||||
>
|
||||
nprofile
|
||||
</Tabs.Trigger>
|
||||
) : null}
|
||||
</Tabs.List>
|
||||
<Tabs.Content value="npub">
|
||||
<div className="my-4">
|
||||
<textarea
|
||||
value={keys.npub}
|
||||
readOnly
|
||||
className="w-full h-20 resize-none p-3 bg-muted rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full rounded-xl border border-primary p-4 flex items-center justify-center">
|
||||
<QRCode size={128} value={keys.npub} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="hex">
|
||||
<div className="my-4">
|
||||
<textarea
|
||||
value={keys.hex}
|
||||
readOnly
|
||||
className="w-full h-20 resize-none p-3 bg-muted rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full rounded-xl border border-primary p-4 flex items-center justify-center">
|
||||
<QRCode size={128} value={keys.hex} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
{keys.nprofile ? (
|
||||
<Tabs.Content value="nprofile">
|
||||
<div className="my-4">
|
||||
<textarea
|
||||
value={keys.nprofile}
|
||||
readOnly
|
||||
className="w-full h-20 resize-none p-3 bg-muted rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full rounded-xl border border-primary p-4 flex items-center justify-center">
|
||||
<QRCode size={128} value={keys.nprofile} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
) : null}
|
||||
</Tabs.Root>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
|
||||
function toggleKeyType(e) {
|
||||
e.preventDefault()
|
||||
let nextKeyType =
|
||||
keys.current[(keys.current.indexOf(pubKey) + 1) % keys.current.length]
|
||||
setPubKey(nextKeyType)
|
||||
}
|
||||
}
|
||||
|
||||
render(<Popup />, document.getElementById('main'))
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<meta charset="utf-8" />
|
||||
<title>nos2x</title>
|
||||
|
||||
<div id="main" style="width: 300px; height: 200px; margin: auto" />
|
||||
|
||||
<script src="prompt.build.js"></script>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Nostr Connect</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link href="/build/style.css" rel="stylesheet" />
|
||||
</head>
|
||||
<body class="bg-background text-foreground text-sm font-sans antialiased">
|
||||
<div id="main" />
|
||||
<script src="build/prompt.build.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import browser from 'webextension-polyfill'
|
||||
import {render} from 'react-dom'
|
||||
import React from 'react'
|
||||
import React, {useState} from 'react'
|
||||
|
||||
import {PERMISSION_NAMES} from './common'
|
||||
import {LogoIcon} from './icons'
|
||||
import * as Checkbox from '@radix-ui/react-checkbox'
|
||||
|
||||
function Prompt() {
|
||||
const [isRemember, setIsRemember] = useState(false)
|
||||
|
||||
let qs = new URLSearchParams(location.search)
|
||||
let id = qs.get('id')
|
||||
let host = qs.get('host')
|
||||
let type = qs.get('type')
|
||||
let params, event
|
||||
|
||||
try {
|
||||
params = JSON.parse(qs.get('params'))
|
||||
if (Object.keys(params).length === 0) params = null
|
||||
@@ -18,83 +23,8 @@ function Prompt() {
|
||||
params = null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<b style={{display: 'block', textAlign: 'center', fontSize: '200%'}}>
|
||||
{host}
|
||||
</b>{' '}
|
||||
<p>
|
||||
is requesting your permission to <b>{PERMISSION_NAMES[type]}:</b>
|
||||
</p>
|
||||
</div>
|
||||
{params && (
|
||||
<>
|
||||
<p>now acting on</p>
|
||||
<pre style={{overflow: 'auto', maxHeight: '120px'}}>
|
||||
<code>{JSON.stringify(event || params, null, 2)}</code>
|
||||
</pre>
|
||||
</>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-around'
|
||||
}}
|
||||
>
|
||||
<button
|
||||
style={{marginTop: '5px'}}
|
||||
onClick={authorizeHandler(
|
||||
true,
|
||||
{} // store this and answer true forever
|
||||
)}
|
||||
>
|
||||
authorize forever
|
||||
</button>
|
||||
{event?.kind !== undefined && (
|
||||
<button
|
||||
style={{marginTop: '5px'}}
|
||||
onClick={authorizeHandler(
|
||||
true,
|
||||
{kinds: {[event.kind]: true}} // store and always answer true for all events that match this condition
|
||||
)}
|
||||
>
|
||||
authorize kind {event.kind} forever
|
||||
</button>
|
||||
)}
|
||||
<button style={{marginTop: '5px'}} onClick={authorizeHandler(true)}>
|
||||
authorize just this
|
||||
</button>
|
||||
{event?.kind !== undefined ? (
|
||||
<button
|
||||
style={{marginTop: '5px'}}
|
||||
onClick={authorizeHandler(
|
||||
false,
|
||||
{kinds: {[event.kind]: true}} // idem
|
||||
)}
|
||||
>
|
||||
reject kind {event.kind} forever
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
style={{marginTop: '5px'}}
|
||||
onClick={authorizeHandler(
|
||||
false,
|
||||
{} // idem
|
||||
)}
|
||||
>
|
||||
reject forever
|
||||
</button>
|
||||
)}
|
||||
<button style={{marginTop: '5px'}} onClick={authorizeHandler(false)}>
|
||||
reject
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
function authorizeHandler(accept, conditions) {
|
||||
function authorizeHandler(accept) {
|
||||
const conditions = isRemember ? {} : null
|
||||
return function (ev) {
|
||||
ev.preventDefault()
|
||||
browser.runtime.sendMessage({
|
||||
@@ -107,6 +37,129 @@ function Prompt() {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-screen h-screen flex flex-col items-center justify-center">
|
||||
<div className="p-8 shadow-primary border border-primary rounded-2xl max-w-xl mx-auto flex flex-col gap-5">
|
||||
<div className="flex flex-col items-center gap-5">
|
||||
<LogoIcon />
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h1 className="font-semibold text-lg">{host}</h1>
|
||||
<p>
|
||||
is requesting your permission to <b>{PERMISSION_NAMES[type]}</b>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{params && (
|
||||
<div className="flex flex-col gap-1">
|
||||
<p>Now acting on</p>
|
||||
<pre className="bg-muted px-2 rounded-xl overflow-scroll">
|
||||
<code>{JSON.stringify(event || params, null, 2)}</code>
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Checkbox.Root
|
||||
id="remember"
|
||||
className="flex h-6 w-6 appearance-none items-center justify-center rounded-lg bg-white outline-none border border-primary data-[state=checked]:bg-primary data-[state=checked]:border-secondary"
|
||||
onCheckedChange={setIsRemember}
|
||||
>
|
||||
<Checkbox.Indicator className="text-white">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M4.5 12.75l6 6 9-13.5"
|
||||
/>
|
||||
</svg>
|
||||
</Checkbox.Indicator>
|
||||
</Checkbox.Root>
|
||||
<label htmlFor="remember" className="text-muted">
|
||||
Remember my preference forever
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
onClick={authorizeHandler(false)}
|
||||
className="flex-1 h-10 rounded-lg shadow-sm border border-primary inline-flex items-center justify-center font-semibold"
|
||||
>
|
||||
Reject
|
||||
</button>
|
||||
<button
|
||||
onClick={authorizeHandler(true)}
|
||||
className="flex-1 h-10 rounded-lg shadow-sm border border-secondary bg-primary text-white inline-flex items-center justify-center font-semibold"
|
||||
>
|
||||
Authorize
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/*
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-around'
|
||||
}}
|
||||
>
|
||||
<button
|
||||
style={{marginTop: '5px'}}
|
||||
onClick={authorizeHandler(
|
||||
true,
|
||||
{} // store this and answer true forever
|
||||
)}
|
||||
>
|
||||
authorize forever
|
||||
</button>
|
||||
{event?.kind !== undefined && (
|
||||
<button
|
||||
style={{marginTop: '5px'}}
|
||||
onClick={authorizeHandler(
|
||||
true,
|
||||
{kinds: {[event.kind]: true}} // store and always answer true for all events that match this condition
|
||||
)}
|
||||
>
|
||||
authorize kind {event.kind} forever
|
||||
</button>
|
||||
)}
|
||||
<button style={{marginTop: '5px'}} onClick={authorizeHandler(true)}>
|
||||
authorize just this
|
||||
</button>
|
||||
{event?.kind !== undefined ? (
|
||||
<button
|
||||
style={{marginTop: '5px'}}
|
||||
onClick={authorizeHandler(
|
||||
false,
|
||||
{kinds: {[event.kind]: true}} // idem
|
||||
)}
|
||||
>
|
||||
reject kind {event.kind} forever
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
style={{marginTop: '5px'}}
|
||||
onClick={authorizeHandler(
|
||||
false,
|
||||
{} // idem
|
||||
)}
|
||||
>
|
||||
reject forever
|
||||
</button>
|
||||
)}
|
||||
<button style={{marginTop: '5px'}} onClick={authorizeHandler(false)}>
|
||||
reject
|
||||
</button>
|
||||
</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
render(<Prompt />, document.getElementById('main'))
|
||||
|
||||
7
extension/style.css
Normal file
@@ -0,0 +1,7 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
.data-\[state\=active\]\:text-primary[data-state=active] > .bg-muted {
|
||||
@apply bg-secondary
|
||||
}
|
||||
26
package.json
@@ -1,25 +1,31 @@
|
||||
{
|
||||
"license": "WTFPL",
|
||||
"dependencies": {
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"async-mutex": "^0.3.2",
|
||||
"esbuild": "^0.14.11",
|
||||
"eslint": "^8.6.0",
|
||||
"esbuild": "^0.14.54",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-plugin-babel": "^5.3.1",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"events": "^3.3.0",
|
||||
"nostr-tools": "^1.12.0",
|
||||
"prettier": "^2.5.1",
|
||||
"minidenticons": "^4.2.0",
|
||||
"nostr-tools": "^1.17.0",
|
||||
"prettier": "^2.8.8",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-native-svg": "^13.8.0",
|
||||
"react-qr-code": "^2.0.11",
|
||||
"react-native-svg": "^13.14.0",
|
||||
"react-qr-code": "^2.0.12",
|
||||
"use-boolean-state": "^1.0.2",
|
||||
"use-debounce": "^7.0.1",
|
||||
"webextension-polyfill": "^0.8.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "./build.js prod",
|
||||
"watch": "ag -l --js | entr ./build.js",
|
||||
"package": "./build.js prod; cd extension; zip -r archive *; cd ..; mv extension/archive.zip ./nos2x.zip"
|
||||
"dev": "./build.js; pnpm exec tailwindcss -i ./extension/style.css -o ./extension/build/style.css --watch",
|
||||
"build": "pnpm exec tailwindcss -i ./extension/style.css -o ./extension/build/style.css; ./build.js prod",
|
||||
"package": "./build.js prod; cd extension; zip -r archive *; cd ..; mv extension/archive.zip ./nostrconnect.zip"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tailwindcss": "^3.3.5"
|
||||
}
|
||||
}
|
||||
|
||||
6723
pnpm-lock.yaml
generated
Normal file
31
tailwind.config.js
Normal file
@@ -0,0 +1,31 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./extension/**/*.{html,js,jsx}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: '#FFF',
|
||||
foreground: '#36364A',
|
||||
muted: '#818498'
|
||||
},
|
||||
boxShadow: {
|
||||
primary: '0px 10px 36px 0px rgba(64, 47, 132, 0.04)',
|
||||
secondary:
|
||||
'0px 1px 2px 0px rgba(18, 43, 105, 0.1), 0px 2px 6px 0px rgba(18, 43, 105, 0.04), 0px 0px 0px 1px rgba(18, 43, 105, 0.08)'
|
||||
},
|
||||
borderColor: {
|
||||
primary: '#E1E3EA',
|
||||
secondary: 'rgba(90, 65, 244, 1)'
|
||||
},
|
||||
backgroundColor: {
|
||||
primary: 'rgba(90, 65, 244, 1)',
|
||||
secondary: 'rgba(90, 65, 244, 0.1)',
|
||||
muted: 'rgba(240, 240, 246, 1)'
|
||||
},
|
||||
textColor: {
|
||||
primary: 'rgba(90, 65, 244, 1)'
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: []
|
||||
}
|
||||