use-whatsapp-connect.ts3.4 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';
export type WhatsappLineMode = 'shared' | 'dedicated';
/**
* Shared WhatsApp connect+poll flow, used by both the settings connections card
* and the inbox connect prompt. Opens Unipile hosted auth in a new tab, then
* finalizes by polling `whatsapp.syncAccounts` (reconciles from Unipile without a
* public webhook) and re-reading `whatsapp.accounts`. Errors surface via toast.
*
* `enabled` gates the accounts query so callers that render the hook conditionally
* (e.g. only on the WhatsApp inbox filter) don't fetch on unrelated surfaces.
*/
export function useWhatsappConnect({ 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.outbound.whatsapp.accounts.queryOptions(), enabled });
const accounts = accountsQuery.data;
const connectedCount = accounts?.filter((a) => a.status === 'connected').length ?? 0;
const { mutateAsync: startHostedAuth } = useMutation(
trpc.outbound.whatsapp.startHostedAuth.mutationOptions(),
);
const { mutateAsync: syncAccounts } = useMutation(
trpc.outbound.whatsapp.syncAccounts.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, mode: WhatsappLineMode) => {
stopPolling();
let ticks = 0;
pollRef.current = setInterval(async () => {
ticks += 1;
// Reconcile straight from Unipile — works even without a public webhook.
try {
await syncAccounts({ lineMode: mode });
} catch {
/* keep polling; the account may not be live yet */
}
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.
stopPolling();
setIsConnecting(false);
}
}, 3000);
};
const handleConnect = async (lineMode: WhatsappLineMode) => {
try {
setIsConnecting(true);
const { authUrl } = await startHostedAuth({ lineMode });
window.open(authUrl, '_blank', 'noopener,noreferrer');
beginPolling(connectedCount, lineMode);
} catch (error) {
setIsConnecting(false);
const msg = error instanceof Error ? error.message : String(error);
toast.error(
msg.toLowerCase().includes('not configured')
? 'WhatsApp is not available for your workspace yet.'
: `Failed to start WhatsApp connection: ${msg}`,
);
}
};
return {
accounts,
refetchAccounts: accountsQuery.refetch,
connectedCount,
accountsLoaded: accountsQuery.isSuccess,
isConnecting,
handleConnect,
};
}