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-16 13:17:29 +00:00
|
|
|
import { useSelector } from "react-redux";
|
2023-01-12 22:16:53 +00:00
|
|
|
import { useNavigate } from "react-router-dom";
|
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 {
|
2023-02-07 20:04:50 +00:00
|
|
|
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-02-07 20:04:50 +00:00
|
|
|
import { useUserProfile } from "Feed/ProfileFeed";
|
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-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-02-09 12:26:54 +00:00
|
|
|
import { RootState } from "State/Store";
|
2023-02-08 21:10:26 +00:00
|
|
|
|
2023-01-12 15:35:42 +00:00
|
|
|
type Nip05ServiceProps = {
|
2023-02-07 20:04:50 +00:00
|
|
|
name: string;
|
|
|
|
service: URL | string;
|
|
|
|
about: JSX.Element;
|
|
|
|
link: string;
|
|
|
|
supportLink: string;
|
2023-02-09 11:24:15 +00:00
|
|
|
helpText?: boolean;
|
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) {
|
2023-02-07 20:04:50 +00:00
|
|
|
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-02-09 12:26:54 +00:00
|
|
|
const pubkey = useSelector((s: RootState) => s.login.publicKey);
|
2023-02-07 20:04:50 +00:00
|
|
|
const user = useUserProfile(pubkey);
|
|
|
|
const publisher = useEventPublisher();
|
2023-02-09 12:26:54 +00:00
|
|
|
const svc = useMemo(() => new ServiceProvider(props.service), [props.service]);
|
2023-02-07 20:04:50 +00:00
|
|
|
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>();
|
2023-02-07 20:04:50 +00:00
|
|
|
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
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
useEffect(() => {
|
|
|
|
svc
|
|
|
|
.GetConfig()
|
2023-02-09 12:26:54 +00:00
|
|
|
.then(a => {
|
2023-02-07 20:04:50 +00:00
|
|
|
if ("error" in a) {
|
|
|
|
setError(a as ServiceError);
|
|
|
|
} else {
|
2023-02-07 19:47:57 +00:00
|
|
|
const svc = a as ServiceConfig;
|
2023-02-07 20:04:50 +00:00
|
|
|
setServiceConfig(svc);
|
2023-02-09 12:26:54 +00:00
|
|
|
const defaultDomain = svc.domains.find(a => a.default)?.name || svc.domains[0].name;
|
2023-02-07 20:04:50 +00:00
|
|
|
setDomain(defaultDomain);
|
|
|
|
}
|
|
|
|
})
|
|
|
|
.catch(console.error);
|
|
|
|
}, [props, svc]);
|
2023-01-12 15:35:42 +00:00
|
|
|
|
2023-02-07 20:04:50 +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] ?? "");
|
2023-02-07 20:04:50 +00:00
|
|
|
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 => {
|
2023-02-07 20:04:50 +00:00
|
|
|
if ("error" in a) {
|
|
|
|
setError(a as ServiceError);
|
|
|
|
} else {
|
|
|
|
setAvailabilityResponse(a as HandleAvailability);
|
2023-01-12 15:35:42 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +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);
|
2023-02-07 20:04:50 +00:00
|
|
|
} 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
|
|
|
}
|
2023-02-07 20:04:50 +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)],
|
2023-02-07 20:04:50 +00:00
|
|
|
]);
|
|
|
|
return whyMap.get(e === "DISALLOWED" ? `${e}_${t}` : e);
|
|
|
|
}
|
2023-01-12 21:36:31 +00:00
|
|
|
|
2023-02-07 20:04:50 +00:00
|
|
|
async function startBuy(handle: string, domain: string) {
|
2023-02-09 12:26:54 +00:00
|
|
|
if (!pubkey) {
|
2023-02-07 20:04:50 +00:00
|
|
|
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);
|
2023-02-07 20:04:50 +00:00
|
|
|
if ("error" in rsp) {
|
|
|
|
setError(rsp);
|
|
|
|
} else {
|
|
|
|
setRegisterResponse(rsp);
|
|
|
|
setShowInvoice(true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
async function updateProfile(handle: string, domain: string) {
|
|
|
|
if (user) {
|
2023-02-09 22:22:16 +00:00
|
|
|
const nip05 = `${handle}@${domain}`;
|
2023-02-07 19:47:57 +00:00
|
|
|
const newProfile = {
|
2023-02-07 20:04:50 +00:00
|
|
|
...user,
|
2023-02-09 22:22:16 +00:00
|
|
|
nip05,
|
2023-02-07 20:04:50 +00:00
|
|
|
} as UserMetadata;
|
2023-02-07 19:47:57 +00:00
|
|
|
const ev = await publisher.metadata(newProfile);
|
2023-02-07 20:04:50 +00:00
|
|
|
publisher.broadcast(ev);
|
2023-02-09 22:22:16 +00:00
|
|
|
if (props.onSuccess) {
|
|
|
|
props.onSuccess(nip05);
|
|
|
|
}
|
2023-02-09 11:24:15 +00:00
|
|
|
if (helpText) {
|
|
|
|
navigate("/settings");
|
|
|
|
}
|
2023-01-12 21:36:31 +00:00
|
|
|
}
|
2023-02-07 20:04:50 +00:00
|
|
|
}
|
2023-01-12 21:36:31 +00:00
|
|
|
|
2023-02-07 20:04:50 +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>
|
|
|
|
)}
|
2023-02-07 20:04:50 +00:00
|
|
|
{error && <b className="error">{error.error}</b>}
|
|
|
|
{!registerStatus && (
|
|
|
|
<div className="flex mb10">
|
2023-02-09 22:22:16 +00:00
|
|
|
<input type="text" placeholder={formatMessage(messages.Handle)} value={handle} onChange={onHandleChange} />
|
2023-02-07 20:04:50 +00:00
|
|
|
@
|
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 => (
|
2023-02-07 20:04:50 +00:00
|
|
|
<option key={a.name}>{a.name}</option>
|
|
|
|
))}
|
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
{availabilityResponse?.available && !registerStatus && (
|
|
|
|
<div className="flex">
|
|
|
|
<div className="mr10">
|
2023-02-09 22:22:16 +00:00
|
|
|
<FormattedMessage
|
|
|
|
{...messages.Sats}
|
|
|
|
values={{ n: formatShort(unwrap(availabilityResponse.quote?.price)) }}
|
|
|
|
/>
|
2023-02-07 20:04:50 +00:00
|
|
|
<br />
|
|
|
|
<small>{availabilityResponse.quote?.data.type}</small>
|
|
|
|
</div>
|
|
|
|
<AsyncButton onClick={() => startBuy(handle, domain)}>
|
2023-02-08 21:10:26 +00:00
|
|
|
<FormattedMessage {...messages.BuyNow} />
|
2023-02-07 20:04:50 +00:00
|
|
|
</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)}
|
2023-02-07 20:04:50 +00:00
|
|
|
</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}` })}
|
2023-02-07 20:04:50 +00:00
|
|
|
/>
|
|
|
|
{registerStatus?.paid && (
|
|
|
|
<div className="flex f-col">
|
2023-02-08 21:10:26 +00:00
|
|
|
<h4>
|
|
|
|
<FormattedMessage {...messages.OrderPaid} />
|
|
|
|
</h4>
|
2023-02-07 20:04:50 +00:00
|
|
|
<p>
|
2023-02-08 21:10:26 +00:00
|
|
|
<FormattedMessage {...messages.NewNip} />{" "}
|
2023-02-07 20:04:50 +00:00
|
|
|
<code>
|
|
|
|
{handle}@{domain}
|
|
|
|
</code>
|
|
|
|
</p>
|
2023-02-08 21:10:26 +00:00
|
|
|
<h3>
|
|
|
|
<FormattedMessage {...messages.AccountSupport} />
|
|
|
|
</h3>
|
2023-02-07 20:04:50 +00:00
|
|
|
<p>
|
2023-02-08 21:10:26 +00:00
|
|
|
<FormattedMessage {...messages.SavePassword} />
|
2023-02-07 20:04:50 +00:00
|
|
|
</p>
|
|
|
|
<Copy text={registerStatus.password} />
|
|
|
|
<p>
|
2023-02-08 21:10:26 +00:00
|
|
|
<FormattedMessage {...messages.GoTo} />{" "}
|
2023-02-07 20:04:50 +00:00
|
|
|
<a href={props.supportLink} target="_blank" rel="noreferrer">
|
2023-02-08 21:10:26 +00:00
|
|
|
<FormattedMessage {...messages.AccountPage} />
|
2023-02-07 20:04:50 +00:00
|
|
|
</a>
|
|
|
|
</p>
|
2023-02-12 23:17:34 +00:00
|
|
|
<h4>
|
|
|
|
<FormattedMessage {...messages.ActivateNow} />
|
|
|
|
</h4>
|
|
|
|
<AsyncButton onClick={() => updateProfile(handle, domain)}>
|
|
|
|
<FormattedMessage {...messages.AddToProfile} />
|
|
|
|
</AsyncButton>
|
2023-02-07 20:04:50 +00:00
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</>
|
|
|
|
);
|
2023-01-27 21:38:41 +00:00
|
|
|
}
|