1
0
forked from Kieran/snort

feat: Handle NIP5 LN Address

This commit is contained in:
Kieran 2023-04-13 12:28:41 +01:00
parent ac778bb216
commit 936473d438
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
15 changed files with 283 additions and 153 deletions

View File

@ -1,7 +1,5 @@
.nip05 {
color: var(--font-secondary-color);
justify-content: flex-start;
align-items: center;
font-weight: normal;
}
@ -52,10 +50,4 @@
.zap .pfp .display-name {
align-items: center;
}
.nip05 .nick {
display: none;
}
.nip05 .domain {
display: none;
}
}

View File

@ -25,24 +25,6 @@
font-weight: 600;
}
.pfp .profile-name {
display: flex;
flex-direction: column;
}
.pfp .display-name {
display: flex;
flex-direction: column;
align-items: flex-start;
}
@media (max-width: 420px) {
.pfp .display-name {
flex-direction: row;
align-items: center;
}
}
.pfp .subheader .about {
max-width: calc(100vw - 140px);
}

View File

@ -96,7 +96,7 @@ export class ServiceProvider {
protected async getJson<T>(
path: string,
method?: "GET" | string,
body?: { [key: string]: string },
body?: unknown,
headers?: { [key: string]: string }
): Promise<T | ServiceError> {
try {

View File

@ -8,6 +8,18 @@ export interface ManageHandle {
domain: string;
pubkey: string;
created: Date;
lnAddress?: string;
}
export enum ForwardType {
Redirect = 0,
ProxyDirect = 1,
ProxyTrusted = 2,
}
export interface PatchHandle {
lnAddress?: string;
forwardType?: ForwardType;
}
export default class SnortServiceProvider extends ServiceProvider {
@ -23,13 +35,17 @@ export default class SnortServiceProvider extends ServiceProvider {
}
async transfer(id: string, to: string) {
return this.getJsonAuthd<object>(`/${id}?to=${to}`, "PATCH");
return this.getJsonAuthd<object>(`/${id}/transfer?to=${to}`, "PATCH");
}
async patch(id: string, obj: PatchHandle) {
return this.getJsonAuthd<object>(`/${id}`, "PATCH", obj);
}
async getJsonAuthd<T>(
path: string,
method?: "GET" | string,
body?: { [key: string]: string },
body?: unknown,
headers?: { [key: string]: string }
): Promise<T | ServiceError> {
const auth = await this.#publisher.generic("", EventKind.HttpAuthentication, [

View File

@ -72,11 +72,6 @@
line-height: 23px;
}
.profile .nip05 {
display: flex;
font-size: 16px;
}
.profile-wrapper > .avatar-wrapper {
z-index: 1;
}

View File

@ -6,7 +6,7 @@ import Relay from "Pages/settings/Relays";
import Preferences from "Pages/settings/Preferences";
import RelayInfo from "Pages/settings/RelayInfo";
import { WalletSettingsRoutes } from "Pages/settings/WalletSettings";
import Nip5ManagePage from "Pages/settings/ManageNip5";
import { ManageHandleRoutes } from "Pages/settings/handle";
import messages from "./messages";
@ -44,9 +44,6 @@ export const SettingsRoutes: RouteObject[] = [
path: "preferences",
element: <Preferences />,
},
{
path: "nip5",
element: <Nip5ManagePage />,
},
...ManageHandleRoutes,
...WalletSettingsRoutes,
];

View File

@ -47,7 +47,7 @@ const SettingsIndex = () => {
<FormattedMessage {...messages.Donate} />
<Icon name="arrowFront" />
</div>
<div className="settings-row" onClick={() => navigate("nip5")}>
<div className="settings-row" onClick={() => navigate("handle")}>
<Icon name="badge" />
<FormattedMessage defaultMessage="Manage Nostr Adddress (NIP-05)" />
<Icon name="arrowFront" />

View File

@ -1,113 +0,0 @@
import { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Link } from "react-router-dom";
import { ApiHost } from "Const";
import Modal from "Element/Modal";
import useEventPublisher from "Feed/EventPublisher";
import { ServiceError } from "Nip05/ServiceProvider";
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
export default function Nip5ManagePage() {
const publisher = useEventPublisher();
const { formatMessage } = useIntl();
const [handles, setHandles] = useState<Array<ManageHandle>>();
const [transfer, setTransfer] = useState("");
const [newKey, setNewKey] = useState("");
const [error, setError] = useState<Array<string>>([]);
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
useEffect(() => {
loadHandles().catch(console.error);
}, []);
async function loadHandles() {
const list = await sp.list();
setHandles(list as Array<ManageHandle>);
}
async function startTransfer() {
if (!transfer || !newKey) return;
setError([]);
const rsp = await sp.transfer(transfer, newKey);
if ("error" in rsp) {
setError((rsp as ServiceError).errors);
return;
}
await loadHandles();
setTransfer("");
setNewKey("");
}
function close() {
setTransfer("");
setNewKey("");
setError([]);
}
if (!handles) {
return null;
}
return (
<>
<h3>
<FormattedMessage defaultMessage="Nostr Address" />
</h3>
{handles.length === 0 && (
<FormattedMessage
defaultMessage="It looks like you dont have any, check {link} to buy one!"
values={{
link: (
<Link to="/verification">
<FormattedMessage defaultMessage="Verification" />
</Link>
),
}}
/>
)}
{handles.map(a => (
<>
<div className="card flex" key={a.id}>
<div className="f-grow">
<h4 className="nip05">
{a.handle}@
<span className="domain" data-domain={a.domain?.toLowerCase()}>
{a.domain}
</span>
</h4>
</div>
<div>
<button className="button" onClick={() => setTransfer(a.id)}>
<FormattedMessage defaultMessage="Transfer" />
</button>
</div>
</div>
</>
))}
{transfer && (
<Modal onClose={close}>
<h4>
<FormattedMessage defaultMessage="Transfer to Pubkey" />
</h4>
<div className="flex">
<div className="f-grow">
<input
type="text"
className="w-max mr10"
placeholder={formatMessage({
defaultMessage: "Public key (npub/nprofile)",
})}
value={newKey}
onChange={e => setNewKey(e.target.value)}
/>
</div>
<button className="button" onClick={() => startTransfer()}>
<FormattedMessage defaultMessage="Transfer" />
</button>
</div>
{error && <b className="error">{error}</b>}
</Modal>
)}
</>
);
}

View File

@ -0,0 +1,69 @@
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { ApiHost } from "Const";
import AsyncButton from "Element/AsyncButton";
import useEventPublisher from "Feed/EventPublisher";
import { LNURL } from "LNURL";
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
export default function LNForwardAddress({ handle }: { handle: ManageHandle }) {
const { formatMessage } = useIntl();
const publisher = useEventPublisher();
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
const [newAddress, setNewAddress] = useState(handle.lnAddress ?? "");
const [error, setError] = useState("");
async function startUpdate() {
const req = {
lnAddress: newAddress,
};
setError("");
try {
const svc = new LNURL(newAddress);
await svc.load();
} catch {
setError(
formatMessage({
defaultMessage: "Invalid LNURL",
})
);
return;
}
const rsp = await sp.patch(handle.id, req);
if ("error" in rsp) {
setError(rsp.error);
}
}
return (
<div className="card">
<h4>
<FormattedMessage defaultMessage="Update Lightning Address" />
</h4>
<p>
<FormattedMessage defaultMessage="Your handle will act like a lightning address and will redirect to your chosen LNURL or Lightning address" />
</p>
<div className="flex">
<div className="f-grow">
<input
type="text"
className="w-max mr10"
placeholder={formatMessage({
defaultMessage: "LNURL or Lightning Address",
})}
value={newAddress}
onChange={e => setNewAddress(e.target.value)}
/>
</div>
<AsyncButton onClick={() => startUpdate()}>
<FormattedMessage defaultMessage="Update" />
</AsyncButton>
</div>
{error && <b className="error">{error}</b>}
</div>
);
}

View File

@ -0,0 +1,62 @@
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { Link, useNavigate } from "react-router-dom";
import { ApiHost } from "Const";
import useEventPublisher from "Feed/EventPublisher";
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
export default function ListHandles() {
const navigate = useNavigate();
const publisher = useEventPublisher();
const [handles, setHandles] = useState<Array<ManageHandle>>([]);
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
useEffect(() => {
loadHandles().catch(console.error);
}, []);
async function loadHandles() {
const list = await sp.list();
setHandles(list as Array<ManageHandle>);
}
return (
<>
{handles.length === 0 && (
<FormattedMessage
defaultMessage="It looks like you dont have any, check {link} to buy one!"
values={{
link: (
<Link to="/verification">
<FormattedMessage defaultMessage="Verification" />
</Link>
),
}}
/>
)}
{handles.map(a => (
<div className="card flex" key={a.id}>
<div className="f-grow">
<h4 className="nip05">
{a.handle}@
<span className="domain" data-domain={a.domain?.toLowerCase()}>
{a.domain}
</span>
</h4>
</div>
<div>
<button
onClick={() =>
navigate("manage", {
state: a,
})
}>
<FormattedMessage defaultMessage="Manage" />
</button>
</div>
</div>
))}
</>
);
}

View File

@ -0,0 +1,21 @@
import { ManageHandle } from "Nip05/SnortServiceProvider";
import { useLocation } from "react-router-dom";
import LNForwardAddress from "./LNAddress";
import TransferHandle from "./TransferHandle";
export default function ManageHandleIndex() {
const location = useLocation();
const handle = location.state as ManageHandle;
return (
<>
<h3 className="nip05">
{handle.handle}@
<span className="domain" data-domain={handle.domain?.toLowerCase()}>
{handle.domain}
</span>
</h3>
<LNForwardAddress handle={handle} />
<TransferHandle handle={handle} />
</>
);
}

View File

@ -0,0 +1,54 @@
import { ApiHost } from "Const";
import AsyncButton from "Element/AsyncButton";
import useEventPublisher from "Feed/EventPublisher";
import { ServiceError } from "Nip05/ServiceProvider";
import SnortServiceProvider, { ManageHandle } from "Nip05/SnortServiceProvider";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
export default function TransferHandle({ handle }: { handle: ManageHandle }) {
const publisher = useEventPublisher();
const navigate = useNavigate();
const { formatMessage } = useIntl();
const sp = new SnortServiceProvider(publisher, `${ApiHost}/api/v1/n5sp`);
const [newKey, setNewKey] = useState("");
const [error, setError] = useState<Array<string>>([]);
async function startTransfer() {
if (!newKey) return;
setError([]);
const rsp = await sp.transfer(handle.id, newKey);
if ("error" in rsp) {
setError((rsp as ServiceError).errors);
return;
}
navigate(-1);
}
return (
<div className="card">
<h4>
<FormattedMessage defaultMessage="Transfer to Pubkey" />
</h4>
<div className="flex">
<div className="f-grow">
<input
type="text"
className="w-max mr10"
placeholder={formatMessage({
defaultMessage: "Public key (npub/nprofile)",
})}
value={newKey}
onChange={e => setNewKey(e.target.value)}
/>
</div>
<AsyncButton onClick={() => startTransfer()}>
<FormattedMessage defaultMessage="Transfer" />
</AsyncButton>
</div>
{error && <b className="error">{error}</b>}
</div>
);
}

View File

@ -0,0 +1,35 @@
import { FormattedMessage } from "react-intl";
import { Outlet, RouteObject, useNavigate } from "react-router-dom";
import ListHandles from "./ListHandles";
import ManageHandleIndex from "./Manage";
export default function ManageHandlePage() {
const navigate = useNavigate();
return (
<>
<h3 onClick={() => navigate("/settings/handle")} className="pointer">
<FormattedMessage defaultMessage="Nostr Address" />
</h3>
<Outlet />
</>
);
}
export const ManageHandleRoutes = [
{
path: "/settings/handle",
element: <ManageHandlePage />,
children: [
{
path: "",
element: <ListHandles />,
},
{
path: "manage",
element: <ManageHandleIndex />,
},
],
},
] as Array<RouteObject>;

View File

@ -30,6 +30,9 @@
"/n5KSF": {
"defaultMessage": "{n} ms"
},
"0Azlrb": {
"defaultMessage": "Manage"
},
"0BUTMv": {
"defaultMessage": "Search..."
},
@ -224,6 +227,9 @@
"BOr9z/": {
"defaultMessage": "Snort is an open source project built by passionate people in their free time"
},
"BWpuKl": {
"defaultMessage": "Update"
},
"BcGMo+": {
"defaultMessage": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages."
},
@ -517,6 +523,9 @@
"Rs4kCE": {
"defaultMessage": "Bookmark"
},
"SOqbe9": {
"defaultMessage": "Update Lightning Address"
},
"Sjo1P4": {
"defaultMessage": "Custom"
},
@ -612,6 +621,9 @@
"aWpBzj": {
"defaultMessage": "Show more"
},
"b5vAk0": {
"defaultMessage": "Your handle will act like a lightning address and will redirect to your chosen LNURL or Lightning address"
},
"bQdA2k": {
"defaultMessage": "Sensitive Content"
},
@ -1015,6 +1027,9 @@
"y1Z3or": {
"defaultMessage": "Language"
},
"yCLnBC": {
"defaultMessage": "LNURL or Lightning Address"
},
"yCmnnm": {
"defaultMessage": "Read global from",
"description": "Label for reading global feed from specific relays"

View File

@ -9,6 +9,7 @@
"/RD0e2": "Nostr uses digital signature technology to provide tamper proof notes which can safely be replicated to many relays to provide redundant storage of your content.",
"/d6vEc": "Make your profile easier to find and share",
"/n5KSF": "{n} ms",
"0Azlrb": "Manage",
"0BUTMv": "Search...",
"0jOEtS": "Invalid LNURL",
"0mch2Y": "name has disallowed characters",
@ -72,6 +73,7 @@
"B6+XJy": "zapped",
"BOUMjw": "No nostr users found for {twitterUsername}",
"BOr9z/": "Snort is an open source project built by passionate people in their free time",
"BWpuKl": "Update",
"BcGMo+": "Notes hold text content, the most popular usage of these notes is to store \"tweet like\" messages.",
"C81/uG": "Logout",
"CHTbO3": "Failed to load invoice",
@ -168,6 +170,7 @@
"RhDAoS": "Are you sure you want to delete {id}",
"RoOyAh": "Relays",
"Rs4kCE": "Bookmark",
"SOqbe9": "Update Lightning Address",
"Sjo1P4": "Custom",
"TpgeGw": "Hex Salt..",
"UDYlxu": "Pending Subscriptions",
@ -199,6 +202,7 @@
"a5UPxh": "Fund developers and platforms providing NIP-05 verification services",
"aJEO/4": "Can't create vote, maybe you're not logged in?",
"aWpBzj": "Show more",
"b5vAk0": "Your handle will act like a lightning address and will redirect to your chosen LNURL or Lightning address",
"bQdA2k": "Sensitive Content",
"brAXSu": "Pick a username",
"bxv59V": "Just now",
@ -330,6 +334,7 @@
"xbVgIm": "Automatically load media",
"xmcVZ0": "Search",
"y1Z3or": "Language",
"yCLnBC": "LNURL or Lightning Address",
"yCmnnm": "Read global from",
"zFegDD": "Contact",
"zINlao": "Owner",