import { useEffect, useState } from "react";
// The shape that api/company-profile.ts returns.
type CompanyProfile =
| { status: "ready"; id: string; name: string; accent: string | null }
| { status: "indexing"; retryAfter: number; expiresIn: number }
| { status: "not_found" }
| { status: "error" };
const FREE_EMAIL = new Set(["gmail.com", "outlook.com", "hotmail.com", "yahoo.com", "icloud.com", "proton.me"]);
function domainFromEmail(email: string) {
const domain = email.split("@")[1]?.trim().toLowerCase() ?? "";
if (!domain.includes(".") || FREE_EMAIL.has(domain)) {
return null;
}
return domain;
}
export function OnboardingAutofill({ token }: { token: string }) {
const [email, setEmail] = useState("");
// The email the user finished typing. A partial email such as you@acme.co
// would look up the wrong company and spend a lookup.
const [committedEmail, setCommittedEmail] = useState("");
const [name, setName] = useState("");
const [nameEdited, setNameEdited] = useState(false);
const [profile, setProfile] = useState<CompanyProfile | null>(null);
const domain = domainFromEmail(committedEmail);
// Pressing Enter submits without a blur, so submit what the field says now.
const currentDomain = domainFromEmail(email);
useEffect(() => {
setProfile(null);
if (!domain) {
return;
}
let cancelled = false;
let timer: ReturnType<typeof setTimeout> | undefined;
let deadline = 0;
const lookup = async () => {
let next: CompanyProfile;
try {
const res = await fetch(`/api/company-profile?domain=${encodeURIComponent(domain)}`);
next = (await res.json()) as CompanyProfile;
} catch {
next = { status: "error" };
}
if (cancelled) {
return;
}
setProfile(next);
// Retry a 202 until the brand is ready or the retry window ends.
if (next.status === "indexing") {
deadline ||= Date.now() + next.expiresIn * 1000;
if (Date.now() + next.retryAfter * 1000 < deadline) {
timer = setTimeout(lookup, next.retryAfter * 1000);
} else {
// The window ended. Stop showing the lookup and leave the field empty.
setProfile({ status: "error" });
}
}
};
lookup();
return () => {
cancelled = true;
clearTimeout(timer);
};
}, [domain]);
const ready = profile?.status === "ready" ? profile : null;
useEffect(() => {
// Only touch a name this form filled in. Without a ready profile for the
// current domain, clear it so the previous company's name never submits.
if (!nameEdited) {
setName(ready ? ready.name : "");
}
}, [ready, nameEdited]);
return (
<form className="grid max-w-md gap-4">
<label className="grid gap-2 text-sm font-medium">
Work email
<input
className="rounded-xl border border-zinc-950/10 px-4 py-3 text-base dark:border-white/20 dark:bg-zinc-900"
onBlur={() => setCommittedEmail(email)}
onChange={(e) => {
setEmail(e.target.value);
// A filled-in name belongs to the old domain once the email changes.
if (!nameEdited && domainFromEmail(e.target.value) !== domain) {
setName("");
}
}}
placeholder="you@company.com"
type="email"
value={email}
/>
</label>
<div className="flex items-center gap-3">
{domain ? (
<img
alt=""
className="h-10 w-10 rounded-lg object-contain"
height={40}
key={domain}
src={`https://img.logo.dev/${domain}?token=${token}&size=40&retina=true&format=webp`}
style={{ outline: ready?.accent ? `2px solid ${ready.accent}` : undefined }}
width={40}
/>
) : (
<div aria-hidden className="h-10 w-10 rounded-lg bg-zinc-200 dark:bg-zinc-700" />
)}
<label className="grid flex-1 gap-2 text-sm font-medium">
Company name
<input
className="rounded-xl border border-zinc-950/10 px-4 py-3 text-base dark:border-white/20 dark:bg-zinc-900"
onChange={(e) => {
setName(e.target.value);
setNameEdited(true);
}}
placeholder={profile?.status === "indexing" ? "Looking up your company…" : "Company name"}
type="text"
value={name}
/>
</label>
</div>
<input name="brand_id" type="hidden" value={ready && currentDomain === domain ? ready.id : ""} />
<input name="domain" type="hidden" value={currentDomain ?? ""} />
</form>
);
}