ConnectStep.tsx7.6 KBView on GitHub import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { ArrowLeft, CheckCircle2, Presentation } from 'lucide-react';
import { GoogleSlidesManager } from '@/modules/integrations/google-slides-manager';
import { LinkedInIntegrationCard } from '@/modules/integrations/linkedin-integration-card';
import { SlackIntegrationCard } from '@/modules/integrations/slack-integration-card';
import { SystemsAndCredentialsSection } from '@/modules/integrations/systems/systems-and-credentials-section';
import { WhatsAppIntegrationCard } from '@/modules/integrations/whatsapp-integration-card';
import { MeetingRecorderOnboardingStep } from '@/modules/onboarding/components/meeting-recorder-step';
import { CrmStep } from '@/modules/onboarding/components/crm-step';
import { authClient } from '@/modules/auth/utils/auth-client';
import { useCopyToClipboard } from '@/hooks/use-copy-to-clipboard';
import { useTRPC } from '@/providers/query-provider';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { SetupPreviewFrame } from '../components/SetupPreviewFrame';
import { useSetupConnect } from '../hooks/use-setup-connect';
import type { SetupStepProps } from '../types';
interface ProviderPickerProps {
providers: { id: string; name: string; connected?: boolean }[];
onSelect: (id: string) => void;
accentClassName: string;
}
/** Provider cards for the step kinds that offer a choice (meeting recorder, CRM). */
function ProviderPicker({ providers, onSelect, accentClassName }: ProviderPickerProps) {
return (
<div className={cn('grid gap-3', providers.length === 1 ? 'grid-cols-1' : 'grid-cols-2')}>
{providers.map((provider) => (
<button
key=[redacted]
type="button"
onClick={() => onSelect(provider.id)}
className="hover:border-foreground/30 flex cursor-pointer flex-col items-center gap-3 rounded-xl border p-6 transition-colors"
>
<div
className={cn(
'flex h-12 w-12 items-center justify-center rounded-lg text-lg font-bold',
accentClassName,
)}
>
{provider.name.charAt(0).toUpperCase()}
</div>
<div className="text-center">
<p className="text-sm font-semibold">{provider.name}</p>
{provider.connected && (
<p className="mt-1 text-xs font-medium text-emerald-600">Connected</p>
)}
</div>
</button>
))}
</div>
);
}
/**
* One integration step.
*
* Provider selection is LOCAL to the step rather than hoisted to the flow, which
* is what the old page did. Hoisting it meant the footer's Back button had two
* jobs — leave the provider detail, or leave the step — and the same click did
* different things depending on invisible state.
*/
export function ConnectStepBody({ step }: SetupStepProps) {
const trpc = useTRPC();
const { session, integrationsData, meetingProviders, crmProviders } = useSetupConnect();
const { copiedValue, copyToClipboard } = useCopyToClipboard();
const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const { data: pickerConfig } = useQuery(
trpc.presentations.getPickerConfig.queryOptions(undefined, { retry: false }),
);
const back = (
<button
type="button"
onClick={() => setSelectedProvider(null)}
className="text-muted-foreground hover:text-foreground mb-4 flex cursor-pointer items-center gap-1 text-sm"
>
<ArrowLeft className="h-4 w-4" />
Back
</button>
);
switch (step.connect) {
case 'meeting':
return selectedProvider ? (
<div>
{back}
<MeetingRecorderOnboardingStep
integrationsData={integrationsData}
session={session}
copyToClipboard={copyToClipboard}
copiedValue={copiedValue}
selectedProviderId={selectedProvider}
/>
</div>
) : (
<ProviderPicker
providers={meetingProviders}
onSelect={setSelectedProvider}
accentClassName="bg-blue-50 text-blue-600 dark:bg-blue-900/20"
/>
);
case 'crm':
return selectedProvider ? (
<div>
{back}
<CrmStep
integrationsData={integrationsData}
session={session}
selectedProviderId={selectedProvider}
/>
</div>
) : (
<ProviderPicker
providers={crmProviders}
onSelect={setSelectedProvider}
accentClassName="bg-emerald-50 text-emerald-600 dark:bg-emerald-900/20"
/>
);
case 'slack':
return <SlackIntegrationCard userId={session?.user?.id} />;
case 'mcp':
return <SystemsAndCredentialsSection userId={session?.user?.id} />;
case 'linkedin':
return <LinkedInIntegrationCard />;
case 'whatsapp':
return <WhatsAppIntegrationCard />;
case 'drive':
return pickerConfig?.needsReauth === false ? (
<div className="space-y-6">
<div className="flex items-center gap-3">
<CheckCircle2 className="h-5 w-5 shrink-0 text-emerald-500" />
<p className="text-sm font-medium">Google Drive access granted</p>
</div>
<GoogleSlidesManager />
</div>
) : (
<div className="flex flex-col items-start gap-4">
<div className="rounded-full bg-blue-50 p-5 dark:bg-blue-900/20">
<Presentation className="h-8 w-8 text-blue-600 dark:text-blue-400" />
</div>
<Button
className="cursor-pointer"
onClick={() =>
authClient.linkSocial({
provider: 'google',
// Comes back into the flow on the Drive step. `drive` names the step
// rather than a position, so adding a step later cannot land the
// redirect somewhere else.
callbackURL: `${window.location.origin}/onboarding?step=connect-drive`,
})
}
>
Grant access
</Button>
</div>
);
default:
return null;
}
}
/** What is wired up so far — updates as each connection lands. */
export function ConnectStepPreview() {
const { integrations } = useSetupConnect();
const connected = integrations.filter((i) => i.connected);
return (
<SetupPreviewFrame label="Connected sources">
<div className="flex h-full flex-col gap-1 overflow-y-auto p-3">
{integrations.length === 0 ? (
<p className="text-muted-foreground p-6 text-center text-xs">
No integrations are available for your organisation.
</p>
) : (
<>
<p className="text-muted-foreground px-2 pb-2 text-xs">
{connected.length} of {integrations.length} connected
</p>
{integrations.map((integration) => (
<div
key=[redacted]
className="flex items-center gap-2 rounded-lg px-2 py-1.5 text-sm"
>
<span
className={cn(
'h-1.5 w-1.5 shrink-0 rounded-full',
integration.connected ? 'bg-emerald-500' : 'bg-muted-foreground/30',
)}
/>
<span className="min-w-0 flex-1 truncate">{integration.name}</span>
{integration.connected && (
<CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-emerald-500" />
)}
</div>
))}
</>
)}
</div>
</SetupPreviewFrame>
);
}