import React, { useState, useEffect, useMemo } from "react"; import { X, ChevronRight, Loader2, AlertCircle } from "lucide-react"; import { useTranslation } from "react-i18next"; import { BlackskyIcon, NorthskyIcon, BlueskyIcon, TophhieIcon, MarginIcon, EuroskuIcon, } from "../common/Icons"; import { startSignup } from "../../api/client"; import { analytics } from "../../lib/analytics"; interface Provider { id: string; name: string; service: string; Icon: React.ComponentType<{ size?: number }> | null; description: string; custom?: boolean; wide?: boolean; } type ProviderBase = { id: string; service: string; Icon: React.ComponentType<{ size?: number }> | null; custom?: boolean; }; const MARGIN_PROVIDER_BASE: ProviderBase = { id: "margin", service: "https://margin.cafe", Icon: MarginIcon, }; const OTHER_PROVIDERS_BASE: ProviderBase[] = [ { id: "bluesky", service: "https://bsky.social", Icon: BlueskyIcon }, { id: "blacksky", service: "https://blacksky.app", Icon: BlackskyIcon }, { id: "eurosky", service: "https://eurosky.social", Icon: EuroskuIcon }, { id: "selfhosted.social", service: "https://selfhosted.social", Icon: null }, { id: "northsky", service: "https://northsky.social", Icon: NorthskyIcon }, { id: "tophhie", service: "https://tophhie.social", Icon: TophhieIcon }, { id: "custom", service: "", custom: true, Icon: null }, ]; function shuffleArray(arr: T[]): T[] { const shuffled = [...arr]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; } const inviteStatusPromise: Promise> = (async () => { const results: Record = {}; await Promise.allSettled( [MARGIN_PROVIDER_BASE, ...OTHER_PROVIDERS_BASE] .filter((p) => p.service && !p.custom) .map(async (p) => { try { const res = await fetch( `${p.service}/xrpc/com.atproto.server.describeServer`, ); if (res.ok) { const data = await res.json(); results[p.id] = !!data.inviteCodeRequired; } } catch { // ignore unreachable providers } }), ); return results; })(); interface SignUpModalProps { onClose: () => void; } export default function SignUpModal({ onClose }: SignUpModalProps) { const { t } = useTranslation(); const [showCustomInput, setShowCustomInput] = useState(false); const [showMore, setShowMore] = useState(false); const [customService, setCustomService] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [inviteStatus, setInviteStatus] = useState>({}); const [statusLoaded, setStatusLoaded] = useState(false); const MARGIN_PROVIDER: Provider = { ...MARGIN_PROVIDER_BASE, name: t("signUp.providers.margin.name"), description: t("signUp.providers.margin.description"), }; const providerI18nKey: Record = { bluesky: "bluesky", blacksky: "blacksky", eurosky: "eurosky", "selfhosted.social": "selfhostedSocial", northsky: "northsky", tophhie: "tophhie", custom: "customPds", }; const OTHER_PROVIDERS: Provider[] = OTHER_PROVIDERS_BASE.map((p) => { const k = providerI18nKey[p.id] || p.id; return { ...p, name: t(`signUp.providers.${k}.name`), description: t(`signUp.providers.${k}.description`), }; }); useEffect(() => { inviteStatusPromise.then((status) => { setInviteStatus(status); setStatusLoaded(true); }); }, []); const providers = useMemo(() => { const nonCustom = OTHER_PROVIDERS.filter((p) => !p.custom); const custom = OTHER_PROVIDERS.find((p) => p.custom); if (!statusLoaded) { return [ MARGIN_PROVIDER, ...shuffleArray(nonCustom), ...(custom ? [custom] : []), ]; } const open = nonCustom.filter((p) => !inviteStatus[p.id]); const inviteOnly = nonCustom.filter((p) => inviteStatus[p.id]); return [ MARGIN_PROVIDER, ...shuffleArray(open), ...shuffleArray(inviteOnly), ...(custom ? [custom] : []), ]; // eslint-disable-next-line react-hooks/exhaustive-deps }, [statusLoaded, inviteStatus]); useEffect(() => { document.body.style.overflow = "hidden"; return () => { document.body.style.overflow = "unset"; }; }, []); const handleProviderSelect = async (provider: Provider) => { if (provider.custom) { setShowCustomInput(true); return; } setLoading(true); setError(null); try { analytics.capture("signup_initiated", { provider: provider.id }); const result = await startSignup(provider.service); if (result.authorizationUrl) { window.location.assign(result.authorizationUrl); } } catch (err) { console.error(err); analytics.captureException(err); setError(t("signUp.providerError")); setLoading(false); } }; const handleCustomSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!customService.trim()) return; setLoading(true); setError(null); let serviceUrl = customService.trim(); if (!serviceUrl.startsWith("http")) { serviceUrl = `https://${serviceUrl}`; } try { analytics.capture("signup_initiated", { provider: "custom" }); const result = await startSignup(serviceUrl); if (result.authorizationUrl) { const url = new URL(result.authorizationUrl); if (url.protocol !== "https:") throw new Error("Invalid authorization URL"); window.location.href = result.authorizationUrl; } } catch (err) { console.error(err); analytics.captureException(err); setError(t("signUp.customPdsError")); setLoading(false); } }; return (

{loading ? t("signUp.connecting") : showCustomInput ? t("signUp.customPdsTitle") : t("signUp.title")}

{loading ? (
) : showCustomInput ? (

{t("signUp.customPdsTitle")}

{t("signUp.customPdsSubtitle")}

setCustomService(e.target.value)} placeholder={t("signUp.pdsAddressPlaceholder")} autoFocus />
{error && (
{error}
)}
) : (

{t("signUp.subtitle")}{" "} {t("signUp.atProtocol")} {t("signUp.subtitleSuffix")}

{error && (
{error}
)}
{(showMore ? providers : providers.slice(0, 1)).map((p) => ( ))}
{!showMore && (

{t("signUp.atmosphereNote")}

)}
)}
); }