"use client"; import { useEffect, useState } from "react"; import { useRouter, useSearchParams, usePathname } from "next/navigation"; import { SubscribeWithHandle, AtSubscribeSuccess } from "./HandleSubscribe"; import { EmailInput, EmailConfirm } from "./EmailSubscribe"; import { EmailSubscribeSuccess } from "./EmailSubscribeSuccess"; import { LinkIdentityModal } from "./LinkIdentityModal"; import { Modal } from "components/Modal"; import { ButtonPrimary } from "components/Buttons"; import { ManageSubscription } from "./ManageSubscribe"; import { useToaster } from "components/Toast"; import { useIdentityData } from "components/IdentityProvider"; import { AtmosphereAccount } from "components/Icons/AtmosphereAccount"; import { EmailTiny } from "components/Icons/EmailTiny"; import { Menu, RadioMenuGroup, RadioMenuItem } from "components/Menu"; import { requestPublicationEmailSubscription, confirmPublicationEmailSubscription, } from "actions/publications/subscribeEmail"; import { useViewerSubscription } from "./viewerSubscription"; import { Separator } from "components/Layout"; import { ArrowDownTiny } from "components/Icons/ArrowDownTiny"; type SubscribeMode = "email" | "atproto"; export type SubscribeProps = { autoFocus?: boolean; publicationUri: string; publicationUrl?: string; publicationName: string; publicationDescription?: string; newsletterMode: boolean; }; export const SubscribePanel = (props: SubscribeProps) => { return (

{props.publicationName}

{props.publicationDescription && (
{props.publicationDescription}
)}
); }; export const SubscribeInput = (props: SubscribeProps) => { let toaster = useToaster(); let router = useRouter(); let pathname = usePathname(); let searchParams = useSearchParams(); const user = useViewerSubscription(props.publicationUri); const { identity, mutate: mutateIdentity } = useIdentityData(); let [email, setEmail] = useState(user.email ?? ""); let [confirmState, setConfirmState] = useState<"confirm" | "success">( "confirm", ); let [confirmOpen, setConfirmOpen] = useState(false); let [requesting, setRequesting] = useState(false); let [confirming, setConfirming] = useState(false); let [locallySubscribed, setLocallySubscribed] = useState(false); let [linkModalOpen, setLinkModalOpen] = useState(false); let [subscribeMode, setSubscribeMode] = useState("email"); let [atSuccessOpen, setAtSuccessOpen] = useState(false); // Tracks that the user passed through LinkIdentityModal — when they enter // the confirmation code we attach the email to their current atp identity // (or merge from any existing email-only identity) instead of creating a // disconnected email-only account. let [linkToCurrent, setLinkToCurrent] = useState(false); const viewerHandle = identity?.bsky_profiles?.handle; const viewerAtpDid = identity?.atp_did; const viewerEmail = identity?.email; // The atp-only-but-subscribing-via-email case: signed in as a Bluesky // account with no email yet. The modal asks them to link the typed email // (or log out) before we send a confirmation code. const needsLinkConfirmation = !!viewerAtpDid && !viewerEmail && !!email; // Embedded subscribe forms (see /api/subscribe_email) redirect back here // with `subscribe_email=` after sending the confirmation code. Open // the confirm modal so the user can paste the code from their inbox without // re-entering their email. `subscribe_email_confirmed=1` means the user was // already verified server-side and the modal jumps straight to success. useEffect(() => { if (!props.newsletterMode) return; const incomingEmail = searchParams.get("subscribe_email"); const alreadyConfirmed = searchParams.get("subscribe_email_confirmed") === "1"; const errorCode = searchParams.get("subscribe_email_error"); if (!incomingEmail && !errorCode) return; if (errorCode) { const message = ERROR_MESSAGES[errorCode as SubscribeError] ?? "We couldn't process that subscription. Try again."; toaster({ type: "error", content: message }); } else if (incomingEmail) { setEmail(incomingEmail); setConfirmState(alreadyConfirmed ? "success" : "confirm"); setConfirmOpen(true); if (alreadyConfirmed) { setLocallySubscribed(true); router.refresh(); } } const next = new URLSearchParams(searchParams.toString()); next.delete("subscribe_email"); next.delete("subscribe_email_confirmed"); next.delete("subscribe_email_error"); const qs = next.toString(); router.replace(qs ? `${pathname}?${qs}` : pathname); // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.newsletterMode]); const sendRequest = async (link: boolean) => { setRequesting(true); setLinkToCurrent(link); let res = await requestPublicationEmailSubscription( props.publicationUri, email, ); setRequesting(false); if (!res.ok) { toaster({ type: "error", content: ERROR_MESSAGES[res.error] }); return; } if (res.value.confirmed) { setConfirmState("success"); router.refresh(); } setConfirmOpen(true); }; const showManage = props.newsletterMode ? user.emailSubscribed : user.atprotoSubscribed; const isSubscribed = showManage || locallySubscribed; const modeMenu = ( ); return ( <>
{isSubscribed ? ( <> {props.newsletterMode && user.atprotoSubscribed && !user.emailSubscribed ? (
Opt in to get updates via email!
{ if (requesting) return; if (needsLinkConfirmation) { setLinkModalOpen(true); return; } await sendRequest(false); }} > Get Emails } />
) : null} ) : props.newsletterMode ? (
{subscribeMode === "email" ? ( { if (!email || requesting) return; if (needsLinkConfirmation) { setLinkModalOpen(true); return; } await sendRequest(false); }} > Subscribe } /> ) : ( setAtSuccessOpen(true)} leading={modeMenu} /> )}
) : ( setLocallySubscribed(true)} /> )} {props.newsletterMode && needsLinkConfirmation && ( { setLinkModalOpen(false); await sendRequest(true); }} /> )} {props.newsletterMode && ( <> { setAtSuccessOpen(open); if (!open) { setLocallySubscribed(true); mutateIdentity(); router.refresh(); } }} > { setConfirmOpen(open); if (!open) { if (confirmState === "success") setLocallySubscribed(true); setConfirmState("confirm"); setLinkToCurrent(false); } }} > {confirmState === "success" ? ( ) : ( setConfirmOpen(false)} emailValue={email} onSubmit={async (code) => { if (confirming) return; setConfirming(true); let res = await confirmPublicationEmailSubscription( props.publicationUri, email, code, linkToCurrent, ); setConfirming(false); if (!res.ok) { toaster({ type: "error", content: ERROR_MESSAGES[res.error], }); return; } setConfirmState("success"); router.refresh(); }} /> )} )} ); }; const SubscribeModeMenu = (props: { mode: SubscribeMode; onChange: (mode: SubscribeMode) => void; }) => { return (
{props.mode === "email" ? : } } >
Subscribe via…
props.onChange(v as SubscribeMode)} > Email Atmosphere
); }; type SubscribeError = | "invalid_email" | "newsletter_disabled" | "email_send_failed" | "subscriber_not_found" | "invalid_code" | "database_error" | "suppressed_spam_complaint" | "suppression_delete_failed" | "link_invalid_state" | "email_belongs_to_other_account"; const ERROR_MESSAGES: Record = { invalid_email: "Please enter a valid email address.", newsletter_disabled: "This publication isn't accepting email subscriptions.", email_send_failed: "We couldn't send the confirmation email. Try again.", subscriber_not_found: "No pending subscription. Start over.", invalid_code: "That code didn't match. Try again.", database_error: "Something went wrong. Try again.", suppressed_spam_complaint: "This address was previously marked as spam and can't be resubscribed. Contact the publication to resolve.", suppression_delete_failed: "We couldn't clear a prior delivery issue on this address. Try again later.", link_invalid_state: "Couldn't link this email to your account. Try logging out and subscribing again.", email_belongs_to_other_account: "This email is already linked to a different Bluesky account. Log out to use that account instead.", };