snort/src/Element/Nip5Service.tsx

281 lines
8.5 KiB
TypeScript
Raw Normal View History

2023-01-12 15:35:42 +00:00
import { useEffect, useMemo, useState } from "react";
2023-02-08 21:10:26 +00:00
import { useIntl, FormattedMessage } from "react-intl";
import { useSelector } from "react-redux";
2023-01-12 22:16:53 +00:00
import { useNavigate } from "react-router-dom";
2023-01-12 21:36:31 +00:00
import {
ServiceProvider,
ServiceConfig,
ServiceError,
HandleAvailability,
ServiceErrorCode,
HandleRegisterResponse,
CheckRegisterResponse,
2023-01-20 11:11:50 +00:00
} from "Nip05/ServiceProvider";
import AsyncButton from "Element/AsyncButton";
2023-02-07 13:32:32 +00:00
import SendSats from "Element/SendSats";
2023-01-20 11:11:50 +00:00
import Copy from "Element/Copy";
import { useUserProfile } from "Feed/ProfileFeed";
2023-01-20 11:11:50 +00:00
import useEventPublisher from "Feed/EventPublisher";
2023-01-24 14:09:56 +00:00
import { debounce, hexToBech32 } from "Util";
2023-01-20 11:11:50 +00:00
import { UserMetadata } from "Nostr";
2023-01-12 15:35:42 +00:00
2023-02-08 21:10:26 +00:00
import messages from "./messages";
2023-01-12 15:35:42 +00:00
type Nip05ServiceProps = {
name: string;
service: URL | string;
about: JSX.Element;
link: string;
supportLink: string;
2023-01-12 15:35:42 +00:00
};
type ReduxStore = any;
export default function Nip5Service(props: Nip05ServiceProps) {
const navigate = useNavigate();
2023-02-08 21:10:26 +00:00
const { formatMessage } = useIntl();
const pubkey = useSelector<ReduxStore, string>((s) => s.login.publicKey);
const user = useUserProfile(pubkey);
const publisher = useEventPublisher();
const svc = useMemo(
() => new ServiceProvider(props.service),
[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 [registerResponse, setRegisterResponse] =
useState<HandleRegisterResponse>();
const [showInvoice, setShowInvoice] = useState<boolean>(false);
const [registerStatus, setRegisterStatus] = useState<CheckRegisterResponse>();
2023-01-12 15:35:42 +00:00
const domainConfig = useMemo(
() => serviceConfig?.domains.find((a) => a.name === domain),
[domain, serviceConfig]
);
2023-01-12 15:35:42 +00:00
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, svc]);
2023-01-12 15:35:42 +00:00
useEffect(() => {
setError(undefined);
setAvailabilityResponse(undefined);
if (handle && domain) {
if (handle.length < (domainConfig?.length[0] ?? 2)) {
setAvailabilityResponse({ available: false, why: "TOO_SHORT" });
return;
}
if (handle.length > (domainConfig?.length[1] ?? 20)) {
setAvailabilityResponse({ available: false, why: "TOO_LONG" });
return;
}
let rx = new RegExp(
domainConfig?.regex[0] ?? "",
domainConfig?.regex[1] ?? ""
);
if (!rx.test(handle)) {
setAvailabilityResponse({ available: false, why: "REGEX" });
return;
}
return debounce(500, () => {
svc
.CheckAvailable(handle, domain)
.then((a) => {
if ("error" in a) {
setError(a as ServiceError);
} else {
setAvailabilityResponse(a as HandleAvailability);
2023-01-12 15:35:42 +00:00
}
})
.catch(console.error);
});
}
}, [handle, domain, domainConfig, svc]);
2023-01-12 15:35:42 +00:00
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);
}
2023-01-12 21:36:31 +00:00
}
}, 2_000);
return () => clearInterval(t);
2023-01-12 15:35:42 +00:00
}
}, [registerResponse, showInvoice, svc]);
2023-01-12 21:36:31 +00:00
function mapError(e: ServiceErrorCode, t: string | null): string | undefined {
let whyMap = new Map([
2023-02-08 21:10:26 +00:00
["TOO_SHORT", formatMessage(messages.TooShort)],
["TOO_LONG", formatMessage(messages.TooLong)],
["REGEX", formatMessage(messages.Regex)],
["REGISTERED", formatMessage(messages.Registered)],
["DISALLOWED_null", formatMessage(messages.Disallowed)],
["DISALLOWED_later", formatMessage(messages.DisalledLater)],
]);
return whyMap.get(e === "DISALLOWED" ? `${e}_${t}` : e);
}
2023-01-12 21:36:31 +00:00
async function startBuy(handle: string, domain: string) {
if (registerResponse) {
setShowInvoice(true);
return;
2023-01-12 21:36:31 +00:00
}
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) {
if (user) {
let newProfile = {
...user,
nip05: `${handle}@${domain}`,
} as UserMetadata;
let ev = await publisher.metadata(newProfile);
publisher.broadcast(ev);
navigate("/settings");
2023-01-12 21:36:31 +00:00
}
}
2023-01-12 21:36:31 +00:00
return (
<>
<h3>{props.name}</h3>
{props.about}
<p>
2023-02-08 21:10:26 +00:00
<FormattedMessage
{...messages.FindMore}
values={{
service: props.name,
link: (
<a href={props.link} target="_blank" rel="noreferrer">
{props.link}
</a>
),
}}
/>
</p>
{error && <b className="error">{error.error}</b>}
{!registerStatus && (
<div className="flex mb10">
<input
type="text"
placeholder="Handle"
value={handle}
onChange={(e) => setHandle(e.target.value.toLowerCase())}
/>
&nbsp;@&nbsp;
<select value={domain} onChange={(e) => setDomain(e.target.value)}>
{serviceConfig?.domains.map((a) => (
<option key={a.name}>{a.name}</option>
))}
</select>
</div>
)}
{availabilityResponse?.available && !registerStatus && (
<div className="flex">
<div className="mr10">
2023-02-08 21:10:26 +00:00
<FormattedMessage
{...messages.Sats}
values={{ n: availabilityResponse.quote?.price }}
/>
<br />
<small>{availabilityResponse.quote?.data.type}</small>
</div>
<input
type="text"
className="f-grow mr10"
placeholder="pubkey"
value={hexToBech32("npub", pubkey)}
disabled
/>
<AsyncButton onClick={() => startBuy(handle, domain)}>
2023-02-08 21:10:26 +00:00
<FormattedMessage {...messages.BuyNow} />
</AsyncButton>
</div>
)}
{availabilityResponse?.available === false && !registerStatus && (
<div className="flex">
<b className="error">
2023-02-08 21:10:26 +00:00
<FormattedMessage {...messages.NotAvailable} />{" "}
{mapError(
availabilityResponse.why!,
availabilityResponse.reasonTag || null
)}
</b>
</div>
)}
<SendSats
invoice={registerResponse?.invoice}
show={showInvoice}
onClose={() => setShowInvoice(false)}
2023-02-08 21:10:26 +00:00
title={formatMessage(messages.Buying, { item: `${handle}@${domain}` })}
/>
{registerStatus?.paid && (
<div className="flex f-col">
2023-02-08 21:10:26 +00:00
<h4>
<FormattedMessage {...messages.OrderPaid} />
</h4>
<p>
2023-02-08 21:10:26 +00:00
<FormattedMessage {...messages.NewNip} />{" "}
<code>
{handle}@{domain}
</code>
</p>
2023-02-08 21:10:26 +00:00
<h3>
<FormattedMessage {...messages.AccountSupport} />
</h3>
<p>
2023-02-08 21:10:26 +00:00
<FormattedMessage {...messages.SavePassword} />
</p>
<Copy text={registerStatus.password} />
<p>
2023-02-08 21:10:26 +00:00
<FormattedMessage {...messages.GoTo} />{" "}
<a href={props.supportLink} target="_blank" rel="noreferrer">
2023-02-08 21:10:26 +00:00
<FormattedMessage {...messages.AccountPage} />
</a>
</p>
2023-02-08 21:10:26 +00:00
<h4>
<FormattedMessage {...messages.ActivateNow} />
</h4>
<AsyncButton onClick={() => updateProfile(handle, domain)}>
2023-02-08 21:10:26 +00:00
<FormattedMessage {...messages.AddToProfile} />
</AsyncButton>
</div>
)}
</>
);
}