import * as React from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { toast } from "sonner"; import { isHostedClientAuthMode } from "@/lib/auth-mode"; import { getStandardErrorMessage } from "@/client/lib/error-messages"; import { captureClientEvent } from "@/client/lib/posthog"; import { GoogleGlyph } from "@/client/features/gsc/GoogleGlyph"; import { SitePicker } from "@/client/features/gsc/SitePicker"; import { startGscLink } from "@/client/features/gsc/startGscLink"; import { disconnectGsc, getGscConnection, listGscSites, setGscSite, } from "@/serverFunctions/gsc"; const GRANT_STATUS_KEY = ["gscGrantStatus"]; export function SearchConsoleConnectionCard({ projectId, }: { projectId: string; }) { const hosted = isHostedClientAuthMode(); const queryClient = useQueryClient(); const [picking, setPicking] = React.useState(false); const [selectedSiteUrl, setSelectedSiteUrl] = React.useState(""); const connectionKey = ["gscConnection", projectId]; const connectionQuery = useQuery({ queryKey: connectionKey, queryFn: () => getGscConnection({ data: { projectId } }), enabled: hosted, }); const connection = connectionQuery.data; const connected = Boolean(connection?.connected); const showPicker = picking || (connection?.currentUserHasGrant && !connected); const sitesQuery = useQuery({ queryKey: ["gscSites", projectId], queryFn: () => listGscSites({ data: { projectId } }), enabled: Boolean(showPicker), }); const setSiteMutation = useMutation({ mutationFn: (siteUrl: string) => setGscSite({ data: { projectId, siteUrl } }), onSuccess: () => { captureClientEvent("gsc:property_select"); toast.success("Search Console connected"); setPicking(false); void queryClient.invalidateQueries({ queryKey: connectionKey }); void queryClient.invalidateQueries({ queryKey: GRANT_STATUS_KEY }); }, onError: (error) => toast.error(getStandardErrorMessage(error)), }); const disconnectMutation = useMutation({ mutationFn: () => disconnectGsc({ data: { projectId } }), onSuccess: () => { toast.success("Search Console disconnected"); setPicking(false); void queryClient.invalidateQueries({ queryKey: connectionKey }); // Disconnect can drop the account-level grant server-side; keep the // shared grant-status cache (onboarding step + re-engagement nudge) honest. void queryClient.invalidateQueries({ queryKey: GRANT_STATUS_KEY }); }, onError: (error) => toast.error(getStandardErrorMessage(error)), }); const handleConnect = () => void startGscLink(window.location.href); if (!hosted) { return (

Available on hosted OpenSEO. Self-hosted? Use a CSV export.

); } return ( {connectionQuery.isLoading ? (
Checking…
) : connected && !picking ? ( { setSelectedSiteUrl(connection?.siteUrl ?? ""); setPicking(true); }} onDisconnect={() => disconnectMutation.mutate()} disconnecting={disconnectMutation.isPending} /> ) : showPicker ? ( selectedSiteUrl && setSiteMutation.mutate(selectedSiteUrl) } saving={setSiteMutation.isPending} onReconnect={handleConnect} secondaryAction={ connected ? { label: "Cancel", onClick: () => setPicking(false) } : { label: "Disconnect", destructive: true, disabled: disconnectMutation.isPending, onClick: () => disconnectMutation.mutate(), } } /> ) : (

Real clicks, impressions, and rankings. Free.

)}
); } // --------------------------------------------------------------------------- // Card shell // --------------------------------------------------------------------------- function IntegrationCard({ status, children, }: { status?: "connected" | "disconnected"; children: React.ReactNode; }) { return (

Google Search Console

Your search data, straight from Google.

{status ? : null}
{children}
); } function StatusPill({ status }: { status: "connected" | "disconnected" }) { const connected = status === "connected"; return ( {connected ? "Connected" : "Not connected"} ); } // --------------------------------------------------------------------------- // Connected state // --------------------------------------------------------------------------- function ConnectedState({ siteUrl, connectedByEmail, onChange, onDisconnect, disconnecting, }: { siteUrl: string; connectedByEmail: string | null; onChange: () => void; onDisconnect: () => void; disconnecting: boolean; }) { return (

{siteUrl}

{connectedByEmail ? (

Connected by {connectedByEmail}

) : null}
); }