Nip5 shop #50
98
src/element/Nip5Service.tsx
Normal file
98
src/element/Nip5Service.tsx
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
🔥 🔥
|
|||||||
|
import { useSelector } from "react-redux";
|
||||||
🔥 🔥
|
|||||||
|
import { ServiceProvider, ServiceConfig, ServiceError, HandleAvailability, ServiceErrorCode } from "../nip05/ServiceProvider";
|
||||||
🔥 🔥
|
|||||||
|
|
||||||
🔥 🔥
|
|||||||
|
type Nip05ServiceProps = {
|
||||||
🔥 🔥
|
|||||||
|
name: string,
|
||||||
🔥 🔥
|
|||||||
|
service: URL | string,
|
||||||
🔥 🔥
|
|||||||
|
about: JSX.Element,
|
||||||
🔥 🔥
|
|||||||
|
link: string
|
||||||
🔥 🔥
|
|||||||
|
};
|
||||||
🔥 🔥
|
|||||||
|
|
||||||
🔥 🔥
|
|||||||
|
type ReduxStore = any;
|
||||||
🔥 🔥
|
|||||||
|
|
||||||
🔥 🔥
|
|||||||
|
export default function Nip5Service(props: Nip05ServiceProps) {
|
||||||
🔥 🔥
We can revisit this once our components are in TS. We can revisit this once our components are in TS.
|
|||||||
|
const pubkey = useSelector<ReduxStore, string>(s => s.login.publicKey);
|
||||||
🔥 🔥
|
|||||||
|
const svc = new ServiceProvider(props.service);
|
||||||
🔥 🔥
|
|||||||
|
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
||||||
🔥 🔥
|
|||||||
|
const [error, setError] = useState<ServiceError>();
|
||||||
🔥 🔥
|
|||||||
|
const [handle, setHandle] = useState<string>("");
|
||||||
🔥 🔥
|
|||||||
|
const [domain, setDomain] = useState<string>("");
|
||||||
🔥 🔥
|
|||||||
|
const [availabilityResponse, setAvailabilityResponse] = useState<HandleAvailability>();
|
||||||
🔥 🔥
|
|||||||
|
|
||||||
🔥 🔥
|
|||||||
|
const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain]);
|
||||||
🔥 🔥
|
|||||||
|
|
||||||
🔥 🔥
|
|||||||
|
useEffect(() => {
|
||||||
🔥 🔥
|
|||||||
|
svc.GetConfig()
|
||||||
🔥 🔥
|
|||||||
|
.then(a => {
|
||||||
🔥 🔥
|
|||||||
|
if ('error' in a) {
|
||||||
🔥 🔥
|
|||||||
|
setError(a as ServiceError)
|
||||||
🔥 🔥
|
|||||||
|
} else {
|
||||||
🔥 🔥
|
|||||||
|
let svc = a as ServiceConfig;
|
||||||
🔥 🔥
|
|||||||
|
setServiceConfig(svc);
|
||||||
🔥 🔥
|
|||||||
|
let defaultDomain = svc.domains.find(a => a.default)?.name || svc.domains[0].name;
|
||||||
🔥 🔥
|
|||||||
|
setDomain(defaultDomain);
|
||||||
🔥 🔥
|
|||||||
|
}
|
||||||
🔥 🔥
|
|||||||
|
})
|
||||||
🔥 🔥
|
|||||||
|
.catch(console.error)
|
||||||
🔥 🔥
|
|||||||
|
}, [props]);
|
||||||
🔥 🔥
|
|||||||
|
|
||||||
🔥 🔥
|
|||||||
|
useEffect(() => {
|
||||||
🔥 🔥
|
|||||||
|
if(handle.length === 0) {
|
||||||
🔥 🔥
|
|||||||
|
setAvailabilityResponse(undefined);
|
||||||
🔥 🔥
|
|||||||
|
}
|
||||||
🔥 🔥
|
|||||||
|
if (handle && domain) {
|
||||||
🔥 🔥
|
|||||||
|
if (!domainConfig?.regex[0].match(handle)) {
|
||||||
🔥 🔥
|
|||||||
|
setAvailabilityResponse({ available: false, why: "REGEX" });
|
||||||
🔥 🔥
|
|||||||
|
return;
|
||||||
🔥 🔥
|
|||||||
|
}
|
||||||
🔥 🔥
|
|||||||
|
svc.CheckAvailable(handle, domain)
|
||||||
🔥 🔥
|
|||||||
|
.then(a => {
|
||||||
🔥 🔥
|
|||||||
|
if ('error' in a) {
|
||||||
🔥 🔥
|
|||||||
|
setError(a as ServiceError);
|
||||||
🔥 🔥
|
|||||||
|
} else {
|
||||||
🔥 🔥
|
|||||||
|
setAvailabilityResponse(a as HandleAvailability);
|
||||||
🔥 🔥
|
|||||||
|
}
|
||||||
🔥 🔥
|
|||||||
|
})
|
||||||
🔥 🔥
|
|||||||
|
.catch(console.error);
|
||||||
🔥 🔥
|
|||||||
|
}
|
||||||
🔥 🔥
|
|||||||
|
}, [handle, domain]);
|
||||||
🔥 🔥
|
|||||||
|
|
||||||
🔥 🔥
|
|||||||
|
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
|
||||||
🔥 🔥
|
|||||||
|
let whyMap = new Map([
|
||||||
🔥 🔥
|
|||||||
|
["TOO_SHORT", "name too short"],
|
||||||
🔥 🔥
|
|||||||
|
["TOO_LONG", "name too long"],
|
||||||
🔥 🔥
|
|||||||
|
["REGEX", "name has disallowed characters"],
|
||||||
🔥 🔥
|
|||||||
|
["REGISTERED", "name is registered"],
|
||||||
🔥 🔥
|
|||||||
|
["DISALLOWED_null", "name is blocked"],
|
||||||
🔥 🔥
|
|||||||
|
["DISALLOWED_later", "name will be available later"],
|
||||||
🔥 🔥
|
|||||||
|
]);
|
||||||
🔥 🔥
|
|||||||
|
return whyMap.get(e === "DISALLOWED" ? `${e}_${t}` : e);
|
||||||
🔥 🔥
|
|||||||
|
}
|
||||||
🔥 🔥
|
|||||||
|
return (
|
||||||
🔥 🔥
|
|||||||
|
<>
|
||||||
🔥 🔥
|
|||||||
|
<h3>{props.name}</h3>
|
||||||
🔥 🔥
|
|||||||
|
{props.about}
|
||||||
🔥 🔥
|
|||||||
|
<p>Find out more info about {props.name} at <a href={props.link} target="_blank" rel="noreferrer">{props.link}</a></p>
|
||||||
🔥 🔥
|
|||||||
|
{error && <b className="error">{error.error}</b>}
|
||||||
🔥 🔥
|
|||||||
|
<div className="flex mb10">
|
||||||
🔥 🔥
|
|||||||
|
<input type="text" placeholder="Handle" value={handle} onChange={(e) => setHandle(e.target.value)} />
|
||||||
🔥 🔥
|
|||||||
|
@
|
||||||
🔥 🔥
|
|||||||
|
<select value={domain} onChange={(e) => setDomain(e.target.value)}>
|
||||||
🔥 🔥
|
|||||||
|
{serviceConfig?.domains.map(a => <option selected={a.default}>{a.name}</option>)}
|
||||||
🔥 🔥
|
|||||||
|
</select>
|
||||||
🔥 🔥
|
|||||||
|
</div>
|
||||||
🔥 🔥
|
|||||||
|
{availabilityResponse?.available && <div className="flex">
|
||||||
🔥 🔥
|
|||||||
|
<div>
|
||||||
🔥 🔥
|
|||||||
|
{availabilityResponse.quote?.price.toLocaleString()} sats
|
||||||
🔥 🔥
|
|||||||
|
|
||||||
🔥 🔥
|
|||||||
|
</div>
|
||||||
🔥 🔥
|
|||||||
|
<input type="text" className="f-grow mr10" placeholder="pubkey" value={pubkey} disabled={pubkey ? true : false} />
|
||||||
🔥 🔥
|
|||||||
|
<div className="btn">Buy Now</div>
|
||||||
🔥 🔥
|
|||||||
|
</div>}
|
||||||
🔥 🔥
|
|||||||
|
{availabilityResponse?.available === false && <div className="flex">
|
||||||
🔥 🔥
|
|||||||
|
<b className="error">Not available: {mapError(availabilityResponse.why!, availabilityResponse.reasonTag || null)}</b>
|
||||||
🔥 🔥
|
|||||||
|
</div>}
|
||||||
🔥 🔥
|
|||||||
|
</>
|
||||||
🔥 🔥
|
|||||||
|
)
|
||||||
🔥 🔥
|
|||||||
|
}
|
||||||
🔥 🔥
|
@ -107,7 +107,7 @@ textarea {
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
input[type="text"], input[type="password"], input[type="number"], textarea {
|
input[type="text"], input[type="password"], input[type="number"], textarea, select {
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -115,6 +115,11 @@ input[type="text"], input[type="password"], input[type="number"], textarea {
|
|||||||
color: var(--font-color);
|
color: var(--font-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
input:disabled {
|
||||||
|
color: var(--gray-medium);
|
||||||
|
cursor:not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
textarea:placeholder {
|
textarea:placeholder {
|
||||||
color: var(--gray-superlight);
|
color: var(--gray-superlight);
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,8 @@ import Store from "./state/Store";
|
|||||||
import NotificationsPage from './pages/Notifications';
|
import NotificationsPage from './pages/Notifications';
|
||||||
import NewUserPage from './pages/NewUserPage';
|
import NewUserPage from './pages/NewUserPage';
|
||||||
import SettingsPage from './pages/SettingsPage';
|
import SettingsPage from './pages/SettingsPage';
|
||||||
import ErrorPage from './pages/ErrorPage.tsx';
|
import ErrorPage from './pages/ErrorPage';
|
||||||
|
import VerificationPage from './pages/Verification';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nostr websocket managment system
|
* Nostr websocket managment system
|
||||||
@ -57,6 +58,10 @@ const router = createBrowserRouter([
|
|||||||
{
|
{
|
||||||
path: "/settings",
|
path: "/settings",
|
||||||
element: <SettingsPage />
|
element: <SettingsPage />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/verification",
|
||||||
|
element: <VerificationPage />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
86
src/nip05/ServiceProvider.ts
Normal file
86
src/nip05/ServiceProvider.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
export type ServiceErrorCode = "UNKNOWN_ERROR" | "INVALID_BODY" | "NO_SUCH_DOMAIN" | "TOO_SHORT" | "TOO_LONG" | "REGEX" | "DISALLOWED" | "REGISTERED" | "NOT_AVAILABLE" | "RATE_LIMITED" | "NO_TOKEN" | "INVALID_TOKEN" | "NO_SUCH_PAYMENT";
|
||||||
|
|
||||||
|
export interface ServiceError {
|
||||||
|
error: ServiceErrorCode
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ServiceConfig {
|
||||||
|
domains: DomainConfig[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DomainConfig = {
|
||||||
|
name: string,
|
||||||
|
default: boolean,
|
||||||
|
length: [number, number],
|
||||||
|
regex: [string, string],
|
||||||
|
regexChars: [string, string]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HandleAvailability = {
|
||||||
|
available: boolean,
|
||||||
|
why?: ServiceErrorCode,
|
||||||
|
reasonTag?: string | null,
|
||||||
|
quote?: HandleQuote
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HandleQuote = {
|
||||||
|
price: number,
|
||||||
|
data: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HandleRegisterResponse = {
|
||||||
|
quote: HandleQuote,
|
||||||
|
paymentHash: string,
|
||||||
|
invoice: string,
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ServiceProvider {
|
||||||
|
readonly url: URL | string
|
||||||
|
|
||||||
|
constructor(url: URL | string) {
|
||||||
|
this.url = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
async GetConfig(): Promise<ServiceConfig | ServiceError> {
|
||||||
|
return await this._GetJson("/config.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
async CheckAvailable(handle: string, domain: string): Promise<HandleAvailability | ServiceError> {
|
||||||
|
return await this._GetJson("/registration/availability", "POST", { name: handle, domain });
|
||||||
|
}
|
||||||
|
|
||||||
|
async RegisterHandle(handle: string, domain: string, pubkey: string): Promise<HandleRegisterResponse | ServiceError> {
|
||||||
|
return await this._GetJson("/registration/register", "PUT", { name: handle, domain, pk: pubkey });
|
||||||
|
}
|
||||||
|
|
||||||
|
async CheckRegistration(token: string): Promise<any> {
|
||||||
|
return await this._GetJson("/registration/register/check", "POST", undefined, {
|
||||||
|
authorization: token
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async _GetJson<T>(path: string, method?: "GET" | string, body?: any, headers?: any): Promise<T | ServiceError> {
|
||||||
|
try {
|
||||||
|
let rsp = await fetch(`${this.url}${path}`, {
|
||||||
|
method: method,
|
||||||
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
|
headers: {
|
||||||
|
accept: "application/json",
|
||||||
|
...(body ? { "content-type": "application/json" } : {}),
|
||||||
|
...headers
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (rsp.ok) {
|
||||||
|
let obj = await rsp.json();
|
||||||
|
if ('error' in obj) {
|
||||||
|
return <ServiceError>obj;
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
return { error: "UNKNOWN_ERROR" };
|
||||||
|
}
|
||||||
|
}
|
@ -1,15 +1,14 @@
|
|||||||
import React, { FC } from "react";
|
|
||||||
import { useRouteError } from "react-router-dom";
|
import { useRouteError } from "react-router-dom";
|
||||||
|
|
||||||
const ErrorPage: FC = () => {
|
const ErrorPage = () => {
|
||||||
const error = useRouteError();
|
const error = useRouteError();
|
||||||
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h4>{error?.message ?? "Uknown error"}</h4>
|
<h4>An error has occured!</h4>
|
||||||
<pre>
|
<pre>
|
||||||
{JSON.stringify(error)}
|
{JSON.stringify(error, undefined, ' ')}
|
||||||
</pre>
|
</pre>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
37
src/pages/Verification.tsx
Normal file
37
src/pages/Verification.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import Nip5Service from "../element/Nip5Service";
|
||||||
|
|
||||||
|
export default function VerificationPage() {
|
||||||
|
const services = [
|
||||||
|
/*{
|
||||||
|
name: "Snort",
|
||||||
|
service: "https://api.snort.social/api/v1/n5sp",
|
||||||
|
link: "https://snort.social/",
|
||||||
|
about: <>Our very own NIP-05 verification service, help support the development of this site and get a shiny special badge on our site!</>
|
||||||
|
},*/
|
||||||
|
{
|
||||||
|
name: "Nostr Plebs",
|
||||||
|
service: "https://nostrplebs.com/api/v1",
|
||||||
|
link: "https://nostrplebs.com/",
|
||||||
|
about: <>
|
||||||
|
<p>Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices</p>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h2>Get Verified</h2>
|
||||||
|
<p>
|
||||||
|
NIP-05 is a DNS based verification spec which helps to validate you as a real user.
|
||||||
|
</p>
|
||||||
|
<p>Getting NIP-05 verified can help:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Prevent fake accounts from immitating you</li>
|
||||||
|
<li>Make your profile easier to find and share</li>
|
||||||
|
<li>Fund developers and platforms providing NIP-05 verification services</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{services.map(a => <Nip5Service key={a.name} {...a} />)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
9
tsconfig.json
Normal file
9
tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user
🔥
🔥