snort/packages/app/src/Element/Nip5Service.tsx

344 lines
10 KiB
TypeScript
Raw Normal View History

2023-02-09 22:22:16 +00:00
import { useEffect, useMemo, useState, ChangeEvent } from "react";
2023-02-08 21:10:26 +00:00
import { useIntl, FormattedMessage } from "react-intl";
2023-01-12 22:16:53 +00:00
import { useNavigate } from "react-router-dom";
2023-04-14 11:33:19 +00:00
import { UserMetadata } from "@snort/nostr";
2023-02-09 22:22:16 +00:00
import { unwrap } from "Util";
import { formatShort } from "Number";
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";
2023-03-03 14:30:31 +00:00
import { useUserProfile } from "Hooks/useUserProfile";
2023-01-20 11:11:50 +00:00
import useEventPublisher from "Feed/EventPublisher";
2023-02-12 23:17:34 +00:00
import { debounce } from "Util";
2023-04-14 11:33:19 +00:00
import useLogin from "Hooks/useLogin";
2023-04-17 21:22:00 +00:00
import SnortServiceProvider from "Nip05/SnortServiceProvider";
2023-04-18 12:17:10 +00:00
import { mapEventToProfile, UserCache } from "Cache";
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-02-09 11:24:15 +00:00
helpText?: boolean;
2023-04-17 21:22:00 +00:00
forSubscription?: string;
2023-02-09 22:22:16 +00:00
onChange?(h: string): void;
onSuccess?(h: string): void;
2023-01-12 15:35:42 +00:00
};
export default function Nip5Service(props: Nip05ServiceProps) {
const navigate = useNavigate();
2023-02-12 23:17:34 +00:00
const { helpText = true } = props;
2023-02-08 21:10:26 +00:00
const { formatMessage } = useIntl();
2023-04-14 11:33:19 +00:00
const pubkey = useLogin().publicKey;
const user = useUserProfile(pubkey);
const publisher = useEventPublisher();
2023-02-09 12:26:54 +00:00
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>("");
2023-02-12 23:17:34 +00:00
const [checking, setChecking] = useState(false);
2023-02-09 12:26:54 +00:00
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
2023-02-09 22:22:16 +00:00
const onHandleChange = (e: ChangeEvent<HTMLInputElement>) => {
const h = e.target.value.toLowerCase();
setHandle(h);
if (props.onChange) {
props.onChange(`${h}@${domain}`);
}
};
const onDomainChange = (e: ChangeEvent<HTMLSelectElement>) => {
const d = e.target.value;
setDomain(d);
if (props.onChange) {
props.onChange(`${handle}@${d}`);
}
};
2023-02-09 12:26:54 +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()
2023-02-09 12:26:54 +00:00
.then(a => {
if ("error" in a) {
setError(a as ServiceError);
} else {
2023-02-07 19:47:57 +00:00
const svc = a as ServiceConfig;
setServiceConfig(svc);
2023-02-09 12:26:54 +00:00
const 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;
}
2023-02-09 12:26:54 +00:00
const 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)
2023-02-09 12:26:54 +00:00
.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
2023-02-12 23:17:34 +00:00
async function checkRegistration(rsp: HandleRegisterResponse) {
const status = await svc.CheckRegistration(rsp.token);
if ("error" in status) {
setError(status);
setRegisterResponse(undefined);
setShowInvoice(false);
} else {
const result: CheckRegisterResponse = status;
if (result.paid) {
if (!result.available) {
setError({
error: "REGISTERED",
} as ServiceError);
} else {
2023-02-12 23:17:34 +00:00
setError(undefined);
}
setShowInvoice(false);
setRegisterStatus(status);
setRegisterResponse(undefined);
}
}
}
useEffect(() => {
if (registerResponse && showInvoice && !checking) {
const t = setInterval(() => {
if (!checking) {
setChecking(true);
checkRegistration(registerResponse)
.then(() => setChecking(false))
.catch(e => {
console.error(e);
setChecking(false);
});
2023-01-12 21:36:31 +00:00
}
}, 2_000);
return () => clearInterval(t);
2023-01-12 15:35:42 +00:00
}
2023-02-12 23:17:34 +00:00
}, [registerResponse, showInvoice, svc, checking]);
2023-01-12 21:36:31 +00:00
2023-02-09 12:26:54 +00:00
function mapError(e: ServiceErrorCode | undefined, t: string | null): string | undefined {
2023-02-07 19:47:57 +00:00
if (e === undefined) {
return undefined;
}
const 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) {
2023-02-09 12:26:54 +00:00
if (!pubkey) {
return;
2023-01-12 21:36:31 +00:00
}
2023-02-07 19:47:57 +00:00
const rsp = await svc.RegisterHandle(handle, domain, pubkey);
if ("error" in rsp) {
setError(rsp);
} else {
setRegisterResponse(rsp);
setShowInvoice(true);
}
}
2023-04-17 21:22:00 +00:00
async function claimForSubscription(handle: string, domain: string, sub: string) {
if (!pubkey || !publisher) {
return;
}
const svcEx = new SnortServiceProvider(publisher, props.service);
const rsp = await svcEx.registerForSubscription(handle, domain, sub);
if ("error" in rsp) {
setError(rsp);
} else {
if (props.onSuccess) {
const nip05 = `${handle}@${domain}`;
props.onSuccess(nip05);
}
}
}
async function updateProfile(handle: string, domain: string) {
2023-04-14 15:02:15 +00:00
if (user && publisher) {
2023-02-09 22:22:16 +00:00
const nip05 = `${handle}@${domain}`;
2023-02-07 19:47:57 +00:00
const newProfile = {
...user,
2023-02-09 22:22:16 +00:00
nip05,
} as UserMetadata;
2023-02-07 19:47:57 +00:00
const ev = await publisher.metadata(newProfile);
publisher.broadcast(ev);
2023-02-09 22:22:16 +00:00
if (props.onSuccess) {
props.onSuccess(nip05);
}
2023-04-18 12:17:10 +00:00
const newMeta = mapEventToProfile(ev);
if (newMeta) {
UserCache.set(newMeta);
}
2023-02-09 11:24:15 +00:00
if (helpText) {
navigate("/settings");
}
2023-01-12 21:36:31 +00:00
}
}
2023-01-12 21:36:31 +00:00
return (
<>
2023-02-09 11:24:15 +00:00
{helpText && <h3>{props.name}</h3>}
{helpText && props.about}
{helpText && (
<p>
<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">
2023-02-12 12:31:48 +00:00
<input
type="text"
className="nip-handle"
placeholder={formatMessage(messages.Handle)}
value={handle}
onChange={onHandleChange}
/>
&nbsp;@&nbsp;
2023-02-09 22:22:16 +00:00
<select value={domain} onChange={onDomainChange}>
2023-02-09 12:26:54 +00:00
{serviceConfig?.domains.map(a => (
<option key={a.name}>{a.name}</option>
))}
</select>
</div>
)}
{availabilityResponse?.available && !registerStatus && (
<div className="flex">
2023-04-17 21:22:00 +00:00
{!props.forSubscription && (
<div className="mr10">
<FormattedMessage
{...messages.Sats}
values={{ n: formatShort(unwrap(availabilityResponse.quote?.price)) }}
/>
<br />
<small>{availabilityResponse.quote?.data.type}</small>
</div>
)}
<AsyncButton
onClick={() =>
props.forSubscription
? claimForSubscription(handle, domain, props.forSubscription)
: startBuy(handle, domain)
}>
{props.forSubscription ? (
<FormattedMessage defaultMessage="Claim Now" />
) : (
<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} />{" "}
2023-02-09 12:26:54 +00:00
{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-12 23:17:34 +00:00
<h4>
<FormattedMessage {...messages.ActivateNow} />
</h4>
<AsyncButton onClick={() => updateProfile(handle, domain)}>
<FormattedMessage {...messages.AddToProfile} />
</AsyncButton>
</div>
)}
</>
);
}