import React, { useState, useEffect, useMemo } from "react"; import { X, ChevronRight, Loader2, AlertCircle } from "lucide-react"; import { BlackskyIcon, NorthskyIcon, BlueskyIcon, TophhieIcon, MarginIcon, } from "../common/Icons"; import { startSignup } from "../../api/client"; interface Provider { id: string; name: string; service: string; Icon: React.ComponentType<{ size?: number }> | null; description: string; custom?: boolean; wide?: boolean; } const MARGIN_PROVIDER: Provider = { id: "margin", name: "Margin", service: "https://margin.cafe", Icon: MarginIcon, description: "Hosted by Margin, the easiest way to get started", }; const OTHER_PROVIDERS: Provider[] = [ { id: "bluesky", name: "Bluesky", service: "https://bsky.social", Icon: BlueskyIcon, description: "The most popular option on the AT Protocol", }, { id: "blacksky", name: "Blacksky", service: "https://blacksky.app", Icon: BlackskyIcon, description: "For the Culture. A safe space for users and allies", }, { id: "selfhosted.social", name: "selfhosted.social", service: "https://selfhosted.social", Icon: null, description: "For hackers, designers, and ATProto enthusiasts.", }, { id: "northsky", name: "Northsky", service: "https://northsky.social", Icon: NorthskyIcon, description: "A Canadian-based worker-owned cooperative", }, { id: "tophhie", name: "Tophhie", service: "https://tophhie.social", Icon: TophhieIcon, description: "A welcoming and friendly community", }, { id: "altq", name: "AltQ", service: "https://altq.net", Icon: null, description: "An independent, self-hosted PDS instance", }, { id: "custom", name: "Custom", service: "", custom: true, Icon: null, description: "Connect to your own or another custom PDS", }, ]; 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, ...OTHER_PROVIDERS] .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 [showCustomInput, setShowCustomInput] = 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); 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] : []), ]; }, [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 { const result = await startSignup(provider.service); if (result.authorizationUrl) { window.location.assign(result.authorizationUrl); } } catch (err) { console.error(err); setError("Could not connect to this provider. Please try again."); 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 { const result = await startSignup(serviceUrl); if (result.authorizationUrl) { window.location.href = result.authorizationUrl; } } catch (err) { console.error(err); setError("Could not connect to this PDS. Please check the URL."); setLoading(false); } }; return (
{loading ? (

Connecting to provider...

) : showCustomInput ? (

Custom Provider

setCustomService(e.target.value)} placeholder="pds.example.com" autoFocus />
{error && (
{error}
)}
) : (

Create your account

Margin adheres to the{" "} AT Protocol . Choose a provider to host your account.

{error && (
{error}
)}
{providers.map((p) => ( ))}
)}
); }