diff --git a/src/element/AsyncButton.tsx b/src/element/AsyncButton.tsx new file mode 100644 index 00000000..f6ed67ac --- /dev/null +++ b/src/element/AsyncButton.tsx @@ -0,0 +1,27 @@ +import { useState } from "react" + +export default function AsyncButton(props: any) { + const [loading, setLoading] = useState(false); + + async function handle(e : any) { + if(loading) return; + setLoading(true); + try { + if (typeof props.onClick === "function") { + let f = props.onClick(e); + if (f instanceof Promise) { + await f; + } + } + } + finally { + setLoading(false); + } + } + + return ( +
handle(e)}> + {props.children} +
+ ) +} \ No newline at end of file diff --git a/src/element/LNURLTip.js b/src/element/LNURLTip.js index e5bf3ae5..08b10b8d 100644 --- a/src/element/LNURLTip.js +++ b/src/element/LNURLTip.js @@ -19,7 +19,7 @@ export default function LNURLTip(props) { const [success, setSuccess] = useState(null); useEffect(() => { - if (show && invoice === null) { + if (show && !props.invoice) { loadService() .then(a => setPayService(a)) .catch(() => setError("Failed to load LNURL service")); @@ -204,7 +204,7 @@ export default function LNURLTip(props) { return ( onClose()}>
e.stopPropagation()}> -

⚡️ Send sats

+

{props.title || "⚡️ Send sats"}

{invoiceForm()} {error ?

{error}

: null} {payInvoice()} diff --git a/src/element/Nip5Service.tsx b/src/element/Nip5Service.tsx index 272a7f30..2af6ec9d 100644 --- a/src/element/Nip5Service.tsx +++ b/src/element/Nip5Service.tsx @@ -1,24 +1,52 @@ import { useEffect, useMemo, useState } from "react"; -import { useSelector } from "react-redux"; -import { ServiceProvider, ServiceConfig, ServiceError, HandleAvailability, ServiceErrorCode } from "../nip05/ServiceProvider"; +import { useDispatch, useSelector } from "react-redux"; +import { + ServiceProvider, + ServiceConfig, + ServiceError, + HandleAvailability, + ServiceErrorCode, + HandleRegisterResponse, + CheckRegisterResponse +} from "../nip05/ServiceProvider"; +import AsyncButton from "./AsyncButton"; +// @ts-ignore +import LNURLTip from "./LNURLTip"; +// @ts-ignore +import Copy from "./Copy"; +// @ts-ignore +import useProfile from "../feed/ProfileFeed"; +// @ts-ignore +import useEventPublisher from "../feed/EventPublisher"; +// @ts-ignore +import { resetProfile } from "../state/Users"; +import { useNavigate } from "react-router-dom"; type Nip05ServiceProps = { name: string, service: URL | string, about: JSX.Element, - link: string + link: string, + supportLink: string }; type ReduxStore = any; export default function Nip5Service(props: Nip05ServiceProps) { + const dispatch = useDispatch(); + const navigate = useNavigate(); const pubkey = useSelector(s => s.login.publicKey); + const user: any = useProfile(pubkey); + const publisher = useEventPublisher(); 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 [registerResponse, setRegisterResponse] = useState(); + const [showInvoice, setShowInvoice] = useState(false); + const [registerStatus, setRegisterStatus] = useState(); const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain]); @@ -38,11 +66,12 @@ export default function Nip5Service(props: Nip05ServiceProps) { }, [props]); useEffect(() => { - if(handle.length === 0) { + if (handle.length === 0) { setAvailabilityResponse(undefined); } if (handle && domain) { - if (!domainConfig?.regex[0].match(handle)) { + let rx = new RegExp(domainConfig?.regex[0] ?? "", domainConfig?.regex[1] ?? ""); + if (!rx.test(handle)) { setAvailabilityResponse({ available: false, why: "REGEX" }); return; } @@ -58,6 +87,28 @@ export default function Nip5Service(props: Nip05ServiceProps) { } }, [handle, domain]); + useEffect(() => { + if (registerResponse && showInvoice) { + let t = setInterval(async () => { + let status = await svc.CheckRegistration(registerResponse.token); + if ('error' in status) { + setError(status); + setRegisterResponse(undefined); + setShowInvoice(false); + } else { + let result: CheckRegisterResponse = status; + if (result.available && result.paid) { + setShowInvoice(false); + setRegisterStatus(status); + setRegisterResponse(undefined); + setError(undefined); + } + } + }, 2_000); + return () => clearInterval(t); + } + }, [registerResponse, showInvoice]) + function mapError(e: ServiceErrorCode, t: string | null): string | undefined { let whyMap = new Map([ ["TOO_SHORT", "name too short"], @@ -69,30 +120,72 @@ export default function Nip5Service(props: Nip05ServiceProps) { ]); return whyMap.get(e === "DISALLOWED" ? `${e}_${t}` : e); } + + async function startBuy(handle: string, domain: string) { + if (registerResponse) { + setShowInvoice(true); + return; + } + + let rsp = await svc.RegisterHandle(handle, domain, pubkey); + if ('error' in rsp) { + setError(rsp); + } else { + setRegisterResponse(rsp); + setShowInvoice(true); + } + } + + async function updateProfile(handle: string, domain: string) { + let newProfile = { + ...user, + nip05: `${handle}@${domain}` + }; + debugger; + delete newProfile["loaded"]; + delete newProfile["fromEvent"]; + delete newProfile["pubkey"]; + let ev = await publisher.metadata(newProfile); + dispatch(resetProfile(pubkey)); + publisher.broadcast(ev); + navigate("/settings"); + } + return ( <>

{props.name}

{props.about}

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

{error && {error.error}} -
+ {!registerStatus &&
setHandle(e.target.value)} />  @  -
- {availabilityResponse?.available &&
-
- {availabilityResponse.quote?.price.toLocaleString()} sats -   +
} + {availabilityResponse?.available && !registerStatus &&
+
+ {availabilityResponse.quote?.price.toLocaleString()} sats
+ {availabilityResponse.quote?.data.type}
-
Buy Now
+ startBuy(handle, domain)}>Buy Now
} - {availabilityResponse?.available === false &&
+ {availabilityResponse?.available === false && !registerStatus &&
Not available: {mapError(availabilityResponse.why!, availabilityResponse.reasonTag || null)}
} + setShowInvoice(false)} title={`Buying ${handle}@${domain}`} /> + {registerStatus?.paid &&
+

Order Paid!

+ Your new NIP-05 handle is: {handle}@{domain} +

Account Support

+ Please make sure to save the following password in order to manage your handle in the future + +

Go to account page

+

Activate Now

+ updateProfile(handle, domain)}>Add to Profile +
} ) } \ No newline at end of file diff --git a/src/index.css b/src/index.css index 0e6c88bf..9161c163 100644 --- a/src/index.css +++ b/src/index.css @@ -91,6 +91,10 @@ code { font-weight: bold; } +.btn.disabled { + color: var(--gray-light); +} + .btn:hover { background-color: var(--gray); } diff --git a/src/nip05/ServiceProvider.ts b/src/nip05/ServiceProvider.ts index d801bf65..a5b14255 100644 --- a/src/nip05/ServiceProvider.ts +++ b/src/nip05/ServiceProvider.ts @@ -25,7 +25,11 @@ export type HandleAvailability = { export type HandleQuote = { price: number, - data: any + data: HandleData +} + +export type HandleData = { + type: string | "premium" | "short" } export type HandleRegisterResponse = { @@ -35,6 +39,12 @@ export type HandleRegisterResponse = { token: string } +export type CheckRegisterResponse = { + available: boolean, + paid: boolean, + password: string +} + export class ServiceProvider { readonly url: URL | string @@ -54,7 +64,7 @@ export class ServiceProvider { return await this._GetJson("/registration/register", "PUT", { name: handle, domain, pk: pubkey }); } - async CheckRegistration(token: string): Promise { + async CheckRegistration(token: string): Promise { return await this._GetJson("/registration/register/check", "POST", undefined, { authorization: token }); diff --git a/src/nostr/Connection.js b/src/nostr/Connection.js index a8931341..efd1a5c1 100644 --- a/src/nostr/Connection.js +++ b/src/nostr/Connection.js @@ -70,7 +70,11 @@ export default class Connection { for (let p of this.Pending) { this._SendJson(p); } + this.Pending = []; + for (let s of Object.values(this.Subscriptions)) { + this._SendSubscription(s, s.ToObject()); + } this._UpdateState(); } @@ -159,15 +163,7 @@ export default class Connection { return; } - let req = ["REQ", sub.Id, subObj]; - if (sub.OrSubs.length > 0) { - req = [ - ...req, - ...sub.OrSubs.map(o => o.ToObject()) - ]; - } - sub.Started[this.Address] = new Date().getTime(); - this._SendJson(req); + this._SendSubscription(sub, subObj); this.Subscriptions[sub.Id] = sub; } @@ -227,6 +223,18 @@ export default class Connection { } } + _SendSubscription(sub, subObj) { + let req = ["REQ", sub.Id, subObj]; + if (sub.OrSubs.length > 0) { + req = [ + ...req, + ...sub.OrSubs.map(o => o.ToObject()) + ]; + } + sub.Started[this.Address] = new Date().getTime(); + this._SendJson(req); + } + _SendJson(obj) { if (this.Socket?.readyState !== WebSocket.OPEN) { this.Pending.push(obj); diff --git a/src/pages/Verification.tsx b/src/pages/Verification.tsx index bee782a7..aa1cbf66 100644 --- a/src/pages/Verification.tsx +++ b/src/pages/Verification.tsx @@ -12,6 +12,7 @@ export default function VerificationPage() { name: "Nostr Plebs", service: "https://nostrplebs.com/api/v1", link: "https://nostrplebs.com/", + supportLink: "https://nostrplebs.com/manage", about: <>

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