crm-step.tsx10.0 KBView on GitHub 'use client';
import type { CrmProviderId } from '../../../../server/src/services/integrations/crm';
import { CrmIntegrationCard } from '@/modules/integrations/crm-integration-card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import type { Session } from '@/modules/auth/utils/auth-client';
import { useTRPC } from '@/providers/query-provider';
import { Button } from '@/components/ui/button';
import { useMemo, useState } from 'react';
import { Loader2 } from 'lucide-react';
import { toast } from 'sonner';
interface CrmStepProps {
integrationsData?: {
integrations: Array<{
id: string;
name: string;
connected: boolean;
type: string;
capabilities?: {
connectionType?: 'oauth' | 'api_key' | 'manual' | 'none';
};
orgConfig?: {
type: 'api_key' | 'oauth' | 'manual' | 'service_account';
userMustAuthenticate: boolean;
hasOrgCredentials: boolean;
};
isPersonalConnection?: boolean;
}>;
};
session?: Session | null;
selectedProviderId?: string;
}
export function CrmStep({ integrationsData, session, selectedProviderId }: CrmStepProps) {
const trpc = useTRPC();
const queryClient = useQueryClient();
const [isActivating, setIsActivating] = useState<Record<string, boolean>>({});
const { mutateAsync: activateOrgIntegration } = useMutation(
trpc.integrations.activateOrgIntegration.mutationOptions(),
);
const crmProviders = useMemo(() => {
const all = integrationsData?.integrations.filter((i) => i.type === 'crm') || [];
return selectedProviderId ? all.filter((i) => i.id === selectedProviderId) : all;
}, [integrationsData?.integrations, selectedProviderId]);
// Handle activation of org-provisioned integrations
const handleActivateOrgIntegration = async (providerId: string) => {
const providerName = providerId.charAt(0).toUpperCase() + providerId.slice(1);
try {
setIsActivating((prev) => ({ ...prev, [providerId]: true }));
const result = await activateOrgIntegration({ providerId });
if (result.alreadyConnected) {
toast.info(`${providerName} is already connected`);
}
queryClient.invalidateQueries({ queryKey=[redacted] });
} catch (error) {
console.error(`[CrmStep] Error activating ${providerId}:`, error);
toast.error(
`Failed to activate ${providerName}: ${error instanceof Error ? error.message : String(error)}`,
);
} finally {
setIsActivating((prev) => ({ ...prev, [providerId]: false }));
}
};
if (crmProviders.length === 0) {
return (
<div className="flex flex-col items-center space-y-4 text-center">
<h3 className="text-lg font-medium">No CRM providers available</h3>
<p className="text-muted-foreground text-sm">
Please contact support if you believe this is an error.
</p>
</div>
);
}
// Use the first provider as the default tab
const defaultTab = crmProviders[0]?.id || '';
if (selectedProviderId) {
const provider = crmProviders[0];
if (!provider) return null;
const isConnected = provider.connected;
const isOrgProvisioned =
provider.orgConfig?.userMustAuthenticate === false &&
provider.orgConfig?.hasOrgCredentials &&
provider.capabilities?.connectionType === 'api_key';
return (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium">Connect {provider.name}</h3>
<div className="flex items-center gap-2">
{provider.isPersonalConnection && isConnected && (
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">Personal</span>
)}
{isConnected && <span className="flex items-center gap-2 text-sm font-medium text-green-600">ā Connected</span>}
</div>
</div>
<p className="text-muted-foreground text-sm">Sync your contacts, deals, and pipeline data with {provider.name}.</p>
{provider.id === 'salesforce' && (
<p className="text-muted-foreground text-xs">If you face any difficulty logging in, open a new tab and login to Salesforce first, then try again.</p>
)}
</div>
{!isConnected && isOrgProvisioned && (
<div className="space-y-4">
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950/30">
<span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">Provided by your organization</span>
<p className="mt-2 text-sm text-blue-700 dark:text-blue-300">Your organization has configured {provider.name}. Click below to activate your account.</p>
</div>
<Button size="lg" className="w-full" onClick={() => handleActivateOrgIntegration(provider.id)} disabled={isActivating[provider.id]}>
{isActivating[provider.id] ? <><Loader2 className="mr-2 h-5 w-5 animate-spin" /><span>Activating...</span></> : `Activate ${provider.name}`}
</Button>
</div>
)}
{!isOrgProvisioned && <CrmIntegrationCard providerId={provider.id as CrmProviderId} userId={session?.user?.id} />}
{isConnected && isOrgProvisioned && <CrmIntegrationCard providerId={provider.id as CrmProviderId} userId={session?.user?.id} />}
</div>
);
}
return (
<div className="space-y-4">
<Tabs defaultValue={defaultTab} className="w-full">
<div className="overflow-x-auto">
<TabsList>
{crmProviders.map((provider) => (
<TabsTrigger key=[redacted] value={provider.id}>
{provider.name}
</TabsTrigger>
))}
</TabsList>
</div>
{crmProviders.map((provider) => {
const isConnected = provider.connected;
const isOrgProvisioned =
provider.orgConfig?.userMustAuthenticate === false &&
provider.orgConfig?.hasOrgCredentials &&
provider.capabilities?.connectionType === 'api_key';
return (
<TabsContent key=[redacted] value={provider.id} className="space-y-4 pt-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-lg font-medium">Connect {provider.name}</h3>
<div className="flex items-center gap-2">
{provider.isPersonalConnection && isConnected && (
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
Personal
</span>
)}
{isConnected && (
<span className="flex items-center gap-2 text-sm font-medium text-green-600">
ā Connected
</span>
)}
</div>
</div>
<p className="text-muted-foreground text-sm">
Sync your contacts, deals, and pipeline data with {provider.name}.
</p>
{/* Show Salesforce-specific note if needed */}
{provider.id === 'salesforce' && (
<p className="text-muted-foreground text-xs">
If you face any difficulty logging in by clicking the button below, open a new
tab and login to Salesforce first, then try clicking the button again.
</p>
)}
</div>
{/* Org-provisioned API key providers - show Activate button */}
{!isConnected && isOrgProvisioned && (
<div className="space-y-4">
<div className="rounded-lg border border-blue-200 bg-blue-50 p-4 dark:border-blue-800 dark:bg-blue-950/30">
<div className="flex items-center gap-2">
<span className="rounded-full bg-blue-100 px-2 py-1 text-xs font-medium text-blue-700 dark:bg-blue-900 dark:text-blue-300">
Provided by your organization
</span>
</div>
<p className="mt-2 text-sm text-blue-700 dark:text-blue-300">
Your organization has configured {provider.name}. Click below to activate your
account.
</p>
</div>
<Button
size="lg"
className="w-full"
onClick={() => handleActivateOrgIntegration(provider.id)}
disabled={isActivating[provider.id]}
>
{isActivating[provider.id] ? (
<>
<Loader2 className="mr-2 h-5 w-5 animate-spin" />
<span>Activating...</span>
</>
) : (
`Activate ${provider.name}`
)}
</Button>
</div>
)}
{/* Regular connection flow for non-org-provisioned providers or OAuth */}
{!isOrgProvisioned && (
<CrmIntegrationCard
providerId={provider.id as CrmProviderId}
userId={session?.user?.id}
/>
)}
{/* Show card after activation for org-provisioned providers */}
{isConnected && isOrgProvisioned && (
<CrmIntegrationCard
providerId={provider.id as CrmProviderId}
userId={session?.user?.id}
/>
)}
</TabsContent>
);
})}
</Tabs>
</div>
);
}