import { useEffect, useMemo, useState, ChangeEvent } from "react"; import { useIntl, FormattedMessage } from "react-intl"; import { useNavigate } from "react-router-dom"; import { UserMetadata } from "@snort/nostr"; import { unwrap } from "Util"; import { formatShort } from "Number"; import { ServiceProvider, ServiceConfig, ServiceError, HandleAvailability, ServiceErrorCode, HandleRegisterResponse, CheckRegisterResponse, } from "Nip05/ServiceProvider"; import AsyncButton from "Element/AsyncButton"; import SendSats from "Element/SendSats"; import Copy from "Element/Copy"; import { useUserProfile } from "Hooks/useUserProfile"; import useEventPublisher from "Feed/EventPublisher"; import { debounce } from "Util"; import useLogin from "Hooks/useLogin"; import SnortServiceProvider from "Nip05/SnortServiceProvider"; import { mapEventToProfile, UserCache } from "Cache"; import messages from "./messages"; type Nip05ServiceProps = { name: string; service: URL | string; about: JSX.Element; link: string; supportLink: string; helpText?: boolean; forSubscription?: string; onChange?(h: string): void; onSuccess?(h: string): void; }; export default function Nip5Service(props: Nip05ServiceProps) { const navigate = useNavigate(); const { helpText = true } = props; const { formatMessage } = useIntl(); const pubkey = useLogin().publicKey; const user = useUserProfile(pubkey); const publisher = useEventPublisher(); const svc = useMemo(() => new ServiceProvider(props.service), [props.service]); const [serviceConfig, setServiceConfig] = useState(); const [error, setError] = useState(); const [handle, setHandle] = useState(""); const [domain, setDomain] = useState(""); const [checking, setChecking] = useState(false); const [availabilityResponse, setAvailabilityResponse] = useState(); const [registerResponse, setRegisterResponse] = useState(); const [showInvoice, setShowInvoice] = useState(false); const [registerStatus, setRegisterStatus] = useState(); const onHandleChange = (e: ChangeEvent) => { const h = e.target.value.toLowerCase(); setHandle(h); if (props.onChange) { props.onChange(`${h}@${domain}`); } }; const onDomainChange = (e: ChangeEvent) => { const d = e.target.value; setDomain(d); if (props.onChange) { props.onChange(`${handle}@${d}`); } }; const domainConfig = useMemo(() => serviceConfig?.domains.find(a => a.name === domain), [domain, serviceConfig]); useEffect(() => { svc .GetConfig() .then(a => { if ("error" in a) { setError(a as ServiceError); } else { const svc = a as ServiceConfig; setServiceConfig(svc); const defaultDomain = svc.domains.find(a => a.default)?.name || svc.domains[0].name; setDomain(defaultDomain); } }) .catch(console.error); }, [props, svc]); 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; } 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) .then(a => { if ("error" in a) { setError(a as ServiceError); } else { setAvailabilityResponse(a as HandleAvailability); } }) .catch(console.error); }); } }, [handle, domain, domainConfig, svc]); 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 { 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); }); } }, 2_000); return () => clearInterval(t); } }, [registerResponse, showInvoice, svc, checking]); function mapError(e: ServiceErrorCode | undefined, t: string | null): string | undefined { if (e === undefined) { return undefined; } const whyMap = new Map([ ["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); } async function startBuy(handle: string, domain: string) { if (!pubkey) { return; } const rsp = await svc.RegisterHandle(handle, domain, pubkey); if ("error" in rsp) { setError(rsp); } else { setRegisterResponse(rsp); setShowInvoice(true); } } 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) { if (user && publisher) { const nip05 = `${handle}@${domain}`; const newProfile = { ...user, nip05, } as UserMetadata; const ev = await publisher.metadata(newProfile); publisher.broadcast(ev); if (props.onSuccess) { props.onSuccess(nip05); } const newMeta = mapEventToProfile(ev); if (newMeta) { UserCache.set(newMeta); } if (helpText) { navigate("/settings"); } } } return ( <> {helpText &&

{props.name}

} {helpText && props.about} {helpText && (

{props.link} ), }} />

)} {error && {error.error}} {!registerStatus && (
 @ 
)} {availabilityResponse?.available && !registerStatus && (
{!props.forSubscription && (

{availabilityResponse.quote?.data.type}
)} props.forSubscription ? claimForSubscription(handle, domain, props.forSubscription) : startBuy(handle, domain) }> {props.forSubscription ? ( ) : ( )}
)} {availabilityResponse?.available === false && !registerStatus && (
{" "} {mapError(availabilityResponse.why, availabilityResponse.reasonTag || null)}
)} setShowInvoice(false)} title={formatMessage(messages.Buying, { item: `${handle}@${domain}` })} /> {registerStatus?.paid && (

{" "} {handle}@{domain}

{" "}

updateProfile(handle, domain)}>
)} ); }