feat: Handle NIP5 LN Address
This commit is contained in:
parent
ac778bb216
commit
936473d438
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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, [
|
||||
|
@ -72,11 +72,6 @@
|
||||
line-height: 23px;
|
||||
}
|
||||
|
||||
.profile .nip05 {
|
||||
display: flex;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.profile-wrapper > .avatar-wrapper {
|
||||
z-index: 1;
|
||||
}
|
||||
|
@ -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,
|
||||
];
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
69
packages/app/src/Pages/settings/handle/LNAddress.tsx
Normal file
69
packages/app/src/Pages/settings/handle/LNAddress.tsx
Normal 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>
|
||||
);
|
||||
}
|
62
packages/app/src/Pages/settings/handle/ListHandles.tsx
Normal file
62
packages/app/src/Pages/settings/handle/ListHandles.tsx
Normal 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>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
21
packages/app/src/Pages/settings/handle/Manage.tsx
Normal file
21
packages/app/src/Pages/settings/handle/Manage.tsx
Normal 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} />
|
||||
</>
|
||||
);
|
||||
}
|
54
packages/app/src/Pages/settings/handle/TransferHandle.tsx
Normal file
54
packages/app/src/Pages/settings/handle/TransferHandle.tsx
Normal 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>
|
||||
);
|
||||
}
|
35
packages/app/src/Pages/settings/handle/index.tsx
Normal file
35
packages/app/src/Pages/settings/handle/index.tsx
Normal 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>;
|
@ -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"
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user