use-linkedin-connect.ts3.3 KBView on GitHub import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect, useRef, useState } from 'react';
import { toast } from 'sonner';
import { useTRPC } from '@/providers/query-provider';
/**
* Shared LinkedIn connect+poll flow, used by both the settings connections card
* and the inbox connect prompt. Opens Unipile hosted auth in a new tab, then polls
* `linkedin.accounts` until a newly connected seat appears (Unipile's account-status
* webhook upserts it server-side). Errors surface via toast.
*
* `enabled` gates the accounts query so callers that render the hook conditionally
* (e.g. only on the LinkedIn inbox filter) don't fetch on unrelated surfaces.
*/
export function useLinkedinConnect({ enabled = true }: { enabled?: boolean } = {}) {
const trpc = useTRPC();
const queryClient = useQueryClient();
const [isConnecting, setIsConnecting] = useState(false);
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const accountsQuery = useQuery({ ...trpc.linkedin.accounts.queryOptions(), enabled });
const accounts = accountsQuery.data;
const connectedCount = accounts?.filter((a) => a.status === 'connected').length ?? 0;
const { mutateAsync: startHostedAuth } = useMutation(
trpc.linkedin.startHostedAuth.mutationOptions(),
);
const stopPolling = () => {
if (pollRef.current) {
clearInterval(pollRef.current);
pollRef.current = null;
}
};
// Clear the poll timer if the consumer unmounts mid-connect.
useEffect(() => stopPolling, []);
const beginPolling = (startedFrom: number) => {
stopPolling();
let ticks = 0;
pollRef.current = setInterval(async () => {
ticks += 1;
const { data } = await accountsQuery.refetch();
const now = data?.filter((a) => a.status === 'connected').length ?? 0;
if (now > startedFrom) {
stopPolling();
setIsConnecting(false);
void queryClient.invalidateQueries({ queryKey=[redacted] });
} else if (ticks >= 40) {
// ~2 min — give up polling but leave the button ready to re-check. The seat only
// flips to connected once Unipile's account-status webhook lands, so surface that
// instead of silently dropping the spinner and leaving the user unsure what happened.
stopPolling();
setIsConnecting(false);
toast.error(
"LinkedIn is taking longer than expected to finish connecting. If you completed sign-in it should appear shortly — otherwise try Connect again.",
);
}
}, 3000);
};
const handleConnect = async () => {
try {
setIsConnecting(true);
const { authUrl } = await startHostedAuth({});
window.open(authUrl, '_blank', 'noopener,noreferrer');
beginPolling(connectedCount);
} catch (error) {
setIsConnecting(false);
const msg = error instanceof Error ? error.message : String(error);
toast.error(
msg.toLowerCase().includes('not configured')
? 'LinkedIn is not available for your workspace yet.'
: `Failed to start LinkedIn connection: ${msg}`,
);
}
};
return {
accounts,
refetchAccounts: accountsQuery.refetch,
connectedCount,
accountsLoaded: accountsQuery.isSuccess,
isConnecting,
handleConnect,
};
}