From bcdf035063b032b8955dc8926af5ce0e5fa1b602 Mon Sep 17 00:00:00 2001 From: Kieran Date: Thu, 12 Jan 2023 15:35:42 +0000 Subject: [PATCH] Verfication page --- src/element/Nip5Service.tsx | 98 ++++++++++++++++++++++++++++++++++++ src/index.css | 7 ++- src/index.js | 7 ++- src/nip05/ServiceProvider.ts | 86 +++++++++++++++++++++++++++++++ src/pages/ErrorPage.tsx | 7 ++- src/pages/Verification.tsx | 37 ++++++++++++++ tsconfig.json | 9 ++++ 7 files changed, 245 insertions(+), 6 deletions(-) create mode 100644 src/element/Nip5Service.tsx create mode 100644 src/nip05/ServiceProvider.ts create mode 100644 src/pages/Verification.tsx create mode 100644 tsconfig.json diff --git a/src/element/Nip5Service.tsx b/src/element/Nip5Service.tsx new file mode 100644 index 000000000..272a7f304 --- /dev/null +++ b/src/element/Nip5Service.tsx @@ -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) { + const pubkey = useSelector(s => s.login.publicKey); + const svc = new ServiceProvider(props.service); + const [serviceConfig, setServiceConfig] = useState(); + const [error, setError] = useState(); + const [handle, setHandle] = useState(""); + const [domain, setDomain] = useState(""); + const [availabilityResponse, setAvailabilityResponse] = useState(); + + 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 ( + <> +

{props.name}

+ {props.about} +

Find out more info about {props.name} at {props.link}

+ {error && {error.error}} +
+ setHandle(e.target.value)} /> +  @  + +
+ {availabilityResponse?.available &&
+
+ {availabilityResponse.quote?.price.toLocaleString()} sats +   +
+ +
Buy Now
+
} + {availabilityResponse?.available === false &&
+ Not available: {mapError(availabilityResponse.why!, availabilityResponse.reasonTag || null)} +
} + + ) +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index bc2e998fa..0e6c88bfa 100644 --- a/src/index.css +++ b/src/index.css @@ -107,7 +107,7 @@ textarea { 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; border-radius: 5px; border: 0; @@ -115,6 +115,11 @@ input[type="text"], input[type="password"], input[type="number"], textarea { color: var(--font-color); } +input:disabled { + color: var(--gray-medium); + cursor:not-allowed; +} + textarea:placeholder { color: var(--gray-superlight); } diff --git a/src/index.js b/src/index.js index 0dcaeb9ec..10ec5f17c 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,8 @@ import Store from "./state/Store"; import NotificationsPage from './pages/Notifications'; import NewUserPage from './pages/NewUserPage'; 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 @@ -57,6 +58,10 @@ const router = createBrowserRouter([ { path: "/settings", element: + }, + { + path: "/verification", + element: } ] } diff --git a/src/nip05/ServiceProvider.ts b/src/nip05/ServiceProvider.ts new file mode 100644 index 000000000..d801bf655 --- /dev/null +++ b/src/nip05/ServiceProvider.ts @@ -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 { + return await this._GetJson("/config.json"); + } + + async CheckAvailable(handle: string, domain: string): Promise { + return await this._GetJson("/registration/availability", "POST", { name: handle, domain }); + } + + async RegisterHandle(handle: string, domain: string, pubkey: string): Promise { + return await this._GetJson("/registration/register", "PUT", { name: handle, domain, pk: pubkey }); + } + + async CheckRegistration(token: string): Promise { + return await this._GetJson("/registration/register/check", "POST", undefined, { + authorization: token + }); + } + async _GetJson(path: string, method?: "GET" | string, body?: any, headers?: any): Promise { + 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 obj; + } + return obj; + } + } catch (e) { + console.warn(e); + } + return { error: "UNKNOWN_ERROR" }; + } +} \ No newline at end of file diff --git a/src/pages/ErrorPage.tsx b/src/pages/ErrorPage.tsx index 6ccee610b..90c431b8e 100644 --- a/src/pages/ErrorPage.tsx +++ b/src/pages/ErrorPage.tsx @@ -1,15 +1,14 @@ -import React, { FC } from "react"; import { useRouteError } from "react-router-dom"; -const ErrorPage: FC = () => { +const ErrorPage = () => { const error = useRouteError(); console.error(error); return ( <> -

{error?.message ?? "Uknown error"}

+

An error has occured!

-                {JSON.stringify(error)}
+                {JSON.stringify(error, undefined, '  ')}
             
); diff --git a/src/pages/Verification.tsx b/src/pages/Verification.tsx new file mode 100644 index 000000000..bee782a77 --- /dev/null +++ b/src/pages/Verification.tsx @@ -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: <> +

Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices

+ + } + ]; + + return ( + <> +

Get Verified

+

+ NIP-05 is a DNS based verification spec which helps to validate you as a real user. +

+

Getting NIP-05 verified can help:

+
    +
  • Prevent fake accounts from immitating you
  • +
  • Make your profile easier to find and share
  • +
  • Fund developers and platforms providing NIP-05 verification services
  • +
+ + {services.map(a => )} + + ) +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 000000000..5ecf8630a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "jsx": "react-jsx", + "moduleResolution": "node", + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true + } +} \ No newline at end of file