forked from Kieran/snort
Complete buy flow
This commit is contained in:
parent
bcdf035063
commit
192877c5d2
27
src/element/AsyncButton.tsx
Normal file
27
src/element/AsyncButton.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import { useState } from "react"
|
||||||
|
|
||||||
|
export default function AsyncButton(props: any) {
|
||||||
|
const [loading, setLoading] = useState<boolean>(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 (
|
||||||
|
<div {...props} className={`btn ${props.className}${loading ? "disabled" : ""}`} onClick={(e) => handle(e)}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -19,7 +19,7 @@ export default function LNURLTip(props) {
|
|||||||
const [success, setSuccess] = useState(null);
|
const [success, setSuccess] = useState(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (show && invoice === null) {
|
if (show && !props.invoice) {
|
||||||
loadService()
|
loadService()
|
||||||
.then(a => setPayService(a))
|
.then(a => setPayService(a))
|
||||||
.catch(() => setError("Failed to load LNURL service"));
|
.catch(() => setError("Failed to load LNURL service"));
|
||||||
@ -204,7 +204,7 @@ export default function LNURLTip(props) {
|
|||||||
return (
|
return (
|
||||||
<Modal onClose={() => onClose()}>
|
<Modal onClose={() => onClose()}>
|
||||||
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
|
<div className="lnurl-tip" onClick={(e) => e.stopPropagation()}>
|
||||||
<h2>⚡️ Send sats</h2>
|
<h2>{props.title || "⚡️ Send sats"}</h2>
|
||||||
{invoiceForm()}
|
{invoiceForm()}
|
||||||
{error ? <p className="error">{error}</p> : null}
|
{error ? <p className="error">{error}</p> : null}
|
||||||
{payInvoice()}
|
{payInvoice()}
|
||||||
|
@ -1,24 +1,52 @@
|
|||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
import { ServiceProvider, ServiceConfig, ServiceError, HandleAvailability, ServiceErrorCode } from "../nip05/ServiceProvider";
|
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 = {
|
type Nip05ServiceProps = {
|
||||||
name: string,
|
name: string,
|
||||||
service: URL | string,
|
service: URL | string,
|
||||||
about: JSX.Element,
|
about: JSX.Element,
|
||||||
link: string
|
link: string,
|
||||||
|
supportLink: string
|
||||||
};
|
};
|
||||||
|
|
||||||
type ReduxStore = any;
|
type ReduxStore = any;
|
||||||
|
|
||||||
export default function Nip5Service(props: Nip05ServiceProps) {
|
export default function Nip5Service(props: Nip05ServiceProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const navigate = useNavigate();
|
||||||
const pubkey = useSelector<ReduxStore, string>(s => s.login.publicKey);
|
const pubkey = useSelector<ReduxStore, string>(s => s.login.publicKey);
|
||||||
|
const user: any = useProfile(pubkey);
|
||||||
|
const publisher = useEventPublisher();
|
||||||
const svc = new ServiceProvider(props.service);
|
const svc = new ServiceProvider(props.service);
|
||||||
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
const [serviceConfig, setServiceConfig] = useState<ServiceConfig>();
|
||||||
const [error, setError] = useState<ServiceError>();
|
const [error, setError] = useState<ServiceError>();
|
||||||
const [handle, setHandle] = useState<string>("");
|
const [handle, setHandle] = useState<string>("");
|
||||||
const [domain, setDomain] = useState<string>("");
|
const [domain, setDomain] = useState<string>("");
|
||||||
const [availabilityResponse, setAvailabilityResponse] = useState<HandleAvailability>();
|
const [availabilityResponse, setAvailabilityResponse] = useState<HandleAvailability>();
|
||||||
|
const [registerResponse, setRegisterResponse] = useState<HandleRegisterResponse>();
|
||||||
|
const [showInvoice, setShowInvoice] = useState<boolean>(false);
|
||||||
|
const [registerStatus, setRegisterStatus] = useState<CheckRegisterResponse>();
|
||||||
|
|
||||||
const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain]);
|
const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain]);
|
||||||
|
|
||||||
@ -38,11 +66,12 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
}, [props]);
|
}, [props]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(handle.length === 0) {
|
if (handle.length === 0) {
|
||||||
setAvailabilityResponse(undefined);
|
setAvailabilityResponse(undefined);
|
||||||
}
|
}
|
||||||
if (handle && domain) {
|
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" });
|
setAvailabilityResponse({ available: false, why: "REGEX" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -58,6 +87,28 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
}
|
}
|
||||||
}, [handle, domain]);
|
}, [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 {
|
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
|
||||||
let whyMap = new Map([
|
let whyMap = new Map([
|
||||||
["TOO_SHORT", "name too short"],
|
["TOO_SHORT", "name too short"],
|
||||||
@ -69,30 +120,72 @@ export default function Nip5Service(props: Nip05ServiceProps) {
|
|||||||
]);
|
]);
|
||||||
return whyMap.get(e === "DISALLOWED" ? `${e}_${t}` : e);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<h3>{props.name}</h3>
|
<h3>{props.name}</h3>
|
||||||
{props.about}
|
{props.about}
|
||||||
<p>Find out more info about {props.name} at <a href={props.link} target="_blank" rel="noreferrer">{props.link}</a></p>
|
<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>}
|
{error && <b className="error">{error.error}</b>}
|
||||||
<div className="flex mb10">
|
{!registerStatus && <div className="flex mb10">
|
||||||
<input type="text" placeholder="Handle" value={handle} onChange={(e) => setHandle(e.target.value)} />
|
<input type="text" placeholder="Handle" value={handle} onChange={(e) => setHandle(e.target.value)} />
|
||||||
@
|
@
|
||||||
<select value={domain} onChange={(e) => setDomain(e.target.value)}>
|
<select value={domain} onChange={(e) => setDomain(e.target.value)}>
|
||||||
{serviceConfig?.domains.map(a => <option selected={a.default}>{a.name}</option>)}
|
{serviceConfig?.domains.map(a => <option key={a.name}>{a.name}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>}
|
||||||
{availabilityResponse?.available && <div className="flex">
|
{availabilityResponse?.available && !registerStatus && <div className="flex">
|
||||||
<div>
|
<div className="mr10">
|
||||||
{availabilityResponse.quote?.price.toLocaleString()} sats
|
{availabilityResponse.quote?.price.toLocaleString()} sats<br />
|
||||||
|
<small>{availabilityResponse.quote?.data.type}</small>
|
||||||
</div>
|
</div>
|
||||||
<input type="text" className="f-grow mr10" placeholder="pubkey" value={pubkey} disabled={pubkey ? true : false} />
|
<input type="text" className="f-grow mr10" placeholder="pubkey" value={pubkey} disabled={pubkey ? true : false} />
|
||||||
<div className="btn">Buy Now</div>
|
<AsyncButton onClick={() => startBuy(handle, domain)}>Buy Now</AsyncButton>
|
||||||
</div>}
|
</div>}
|
||||||
{availabilityResponse?.available === false && <div className="flex">
|
{availabilityResponse?.available === false && !registerStatus && <div className="flex">
|
||||||
<b className="error">Not available: {mapError(availabilityResponse.why!, availabilityResponse.reasonTag || null)}</b>
|
<b className="error">Not available: {mapError(availabilityResponse.why!, availabilityResponse.reasonTag || null)}</b>
|
||||||
</div>}
|
</div>}
|
||||||
|
<LNURLTip invoice={registerResponse?.invoice} show={showInvoice} onClose={() => setShowInvoice(false)} title={`Buying ${handle}@${domain}`} />
|
||||||
|
{registerStatus?.paid && <div className="flex f-col">
|
||||||
|
<h4>Order Paid!</h4>
|
||||||
|
Your new NIP-05 handle is: <code>{handle}@{domain}</code>
|
||||||
|
<h3>Account Support</h3>
|
||||||
|
Please make sure to save the following password in order to manage your handle in the future
|
||||||
|
<Copy text={registerStatus.password} />
|
||||||
|
<p>Go to <a href={props.supportLink} target="_blank" rel="noreferrer">account page</a></p>
|
||||||
|
<h4>Activate Now</h4>
|
||||||
|
<AsyncButton onClick={() => updateProfile(handle, domain)}>Add to Profile</AsyncButton>
|
||||||
|
</div>}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
@ -91,6 +91,10 @@ code {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.disabled {
|
||||||
|
color: var(--gray-light);
|
||||||
|
}
|
||||||
|
|
||||||
.btn:hover {
|
.btn:hover {
|
||||||
background-color: var(--gray);
|
background-color: var(--gray);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,11 @@ export type HandleAvailability = {
|
|||||||
|
|
||||||
export type HandleQuote = {
|
export type HandleQuote = {
|
||||||
price: number,
|
price: number,
|
||||||
data: any
|
data: HandleData
|
||||||
|
}
|
||||||
|
|
||||||
|
export type HandleData = {
|
||||||
|
type: string | "premium" | "short"
|
||||||
}
|
}
|
||||||
|
|
||||||
export type HandleRegisterResponse = {
|
export type HandleRegisterResponse = {
|
||||||
@ -35,6 +39,12 @@ export type HandleRegisterResponse = {
|
|||||||
token: string
|
token: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CheckRegisterResponse = {
|
||||||
|
available: boolean,
|
||||||
|
paid: boolean,
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
export class ServiceProvider {
|
export class ServiceProvider {
|
||||||
readonly url: URL | string
|
readonly url: URL | string
|
||||||
|
|
||||||
@ -54,7 +64,7 @@ export class ServiceProvider {
|
|||||||
return await this._GetJson("/registration/register", "PUT", { name: handle, domain, pk: pubkey });
|
return await this._GetJson("/registration/register", "PUT", { name: handle, domain, pk: pubkey });
|
||||||
}
|
}
|
||||||
|
|
||||||
async CheckRegistration(token: string): Promise<any> {
|
async CheckRegistration(token: string): Promise<CheckRegisterResponse | ServiceError> {
|
||||||
return await this._GetJson("/registration/register/check", "POST", undefined, {
|
return await this._GetJson("/registration/register/check", "POST", undefined, {
|
||||||
authorization: token
|
authorization: token
|
||||||
});
|
});
|
||||||
|
@ -70,7 +70,11 @@ export default class Connection {
|
|||||||
for (let p of this.Pending) {
|
for (let p of this.Pending) {
|
||||||
this._SendJson(p);
|
this._SendJson(p);
|
||||||
}
|
}
|
||||||
|
this.Pending = [];
|
||||||
|
|
||||||
|
for (let s of Object.values(this.Subscriptions)) {
|
||||||
|
this._SendSubscription(s, s.ToObject());
|
||||||
|
}
|
||||||
this._UpdateState();
|
this._UpdateState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,15 +163,7 @@ export default class Connection {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let req = ["REQ", sub.Id, subObj];
|
this._SendSubscription(sub, 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.Subscriptions[sub.Id] = sub;
|
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) {
|
_SendJson(obj) {
|
||||||
if (this.Socket?.readyState !== WebSocket.OPEN) {
|
if (this.Socket?.readyState !== WebSocket.OPEN) {
|
||||||
this.Pending.push(obj);
|
this.Pending.push(obj);
|
||||||
|
@ -12,6 +12,7 @@ export default function VerificationPage() {
|
|||||||
name: "Nostr Plebs",
|
name: "Nostr Plebs",
|
||||||
service: "https://nostrplebs.com/api/v1",
|
service: "https://nostrplebs.com/api/v1",
|
||||||
link: "https://nostrplebs.com/",
|
link: "https://nostrplebs.com/",
|
||||||
|
supportLink: "https://nostrplebs.com/manage",
|
||||||
about: <>
|
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>
|
<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>
|
||||||
</>
|
</>
|
||||||
|
Loading…
Reference in New Issue
Block a user