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,
  };
}