import React, { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { ProviderIcon } from '../utils/provider'; import { normalizeLinkedInInput } from './linkedin'; import type { ConnectAccountsProps } from './types'; import { CheckCircle, Link2, LinkIcon, Unlink, ArrowLeft, ExternalLink, Eye, Lock, InfoIcon, EyeClosed, EyeOffIcon } from 'lucide-react'; import { AnimatePresence, motion, useReducedMotion } from 'framer-motion'; import { Button, Input, Spinner, Card, CardHeader, CardContent, CardTitle, ConnectProgress } from '../ui'; import Link from 'next/link'; import { Tooltip, TooltipTrigger, TooltipProvider, TooltipContent } from '../ui/'; function byPriority(a: string, b: string) { const pr = (id: string) => (id === 'github' ? 0 : id === 'linkedin' ? 1 : 2); const da = pr(a.toLowerCase()); const db = pr(b.toLowerCase()); return da - db; } export function ConnectAccounts(props: ConnectAccountsProps) { const { providers, connected, idToken, apiGatewayUrl, companyId, needsReconnectIds, sort = true, buildOAuthState, redirectUriOverrides, onConnected, onDisconnected, onError, className, oauthClientIds, providerPickCallback, handleBackButton, headerTitle, headerDescription, requiredProviders, companyName, initialProviderId, // = process.env.NEXT_PUBLIC_STAGE === 'dev' ? 'githubapp' : undefined, githubAppSlugId, userId, inviteId, shouldShowContinueButton, shouldDisableContinueButton, onContinue, } = props; const router = useRouter(); const [selectedProviderId, setSelectedProviderId] = useState(null); const [linkUrl, setLinkUrl] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const [isDisconnecting, setIsDisconnecting] = useState(null); const [showDataHandling, setShowDataHandling] = useState(false); const [previewProviderId, setPreviewProviderId] = useState(null); const [previewAction, setPreviewAction] = useState<'connect' | 'disconnect' | null>(null); const apiBase = apiGatewayUrl || (typeof process !== 'undefined' ? (process.env.NEXT_PUBLIC_API_GATEWAY_URL as string) : ''); const connectedIds = useMemo(() => { const set = new Set((connected || []).map(c => (c.id ? c.id.toLowerCase() : c.name.toLowerCase()))); // Include virtual provider 'githubapp' when the GitHub App is installed try { const gh = (connected || []).find(c => (c?.name || '').toLowerCase() === 'github'); const setupAction = gh?.app_installation_info?.setupAction; if (setupAction === 'install' || setupAction === 'created') { set.add('githubapp'); } } catch {} return set; }, [connected]); const reconnectIds = useMemo(() => new Set((needsReconnectIds || []).map(id => id.toLowerCase())), [needsReconnectIds]); // React to upstream changes to initialProviderId (e.g., after oauth success) useEffect(() => { if (initialProviderId && initialProviderId !== selectedProviderId) { setSelectedProviderIdAndCallback(initialProviderId); } // Do not clear selection if initialProviderId becomes falsy later // eslint-disable-next-line react-hooks/exhaustive-deps }, [initialProviderId]); const list = useMemo(() => { const arr = [...providers]; if (!sort) return arr; return arr.sort((a, b) => { const pr = byPriority(a.id, b.id); if (pr !== 0) return pr; return a.name.localeCompare(b.name); }); }, [providers, sort]); const getRedirectUri = (providerId: string) => { if (redirectUriOverrides && redirectUriOverrides[providerId as keyof typeof redirectUriOverrides]) { return redirectUriOverrides[providerId as keyof typeof redirectUriOverrides] as string; } if (typeof window !== 'undefined') { const origin = window.location.origin; return `${origin}/api/auth/${providerId}/callback`; } return ''; }; const buildState = () => { const baseStateObj: Record = {}; if (companyId) baseStateObj.companyId = companyId; const extra = typeof buildOAuthState === 'function' ? buildOAuthState() : undefined; if (typeof extra === 'string') return extra; const obj = { ...baseStateObj, ...(extra || {}) } as Record; // default to JSON string for safety across providers try { return JSON.stringify(obj); } catch { return ''; } }; const onDisconnect = async (providerId: string) => { setIsDisconnecting(providerId); try { const res = await fetch(`${apiBase}/user/disconnect`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${idToken}`, }, body: JSON.stringify({ provider: providerId, companyId }) }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data?.error || `Failed to disconnect ${providerId}.`); if (onDisconnected) onDisconnected(providerId); } catch (e) { const err = e as Error; if (onError) onError(err.message || 'Failed to disconnect'); } finally { setIsDisconnecting(null); } }; const setSelectedProviderIdAndCallback = (providerId: string | null) => { setSelectedProviderId(providerId); providerPickCallback(providerId); }; const onSubmitLink = async (providerId: string) => { const provider = providers.find(p => p.id.toLowerCase() === providerId.toLowerCase()); if (!provider || !provider.endpoint) return; if (!linkUrl) return onError && onError(`Please enter your ${provider.name} profile URL.`); try { setIsSubmitting(true); const urlToSend = provider.id.toLowerCase() === 'linkedin' ? normalizeLinkedInInput(linkUrl) : linkUrl; const res = await fetch(`${apiBase}${provider.endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${idToken}`, }, body: JSON.stringify(companyId ? { profileUrl: urlToSend, companyId } : { profileUrl: urlToSend }) }); const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data?.error || `Failed to sync ${provider.name} profile.`); setSelectedProviderIdAndCallback(null); setLinkUrl(''); if (onConnected) onConnected(providerId); } catch (e) { const err = e as Error; if (onError) onError(err.message || 'Failed to connect'); } finally { setIsSubmitting(false); } }; const onOAuth = (providerId: string) => { const id = providerId.toLowerCase(); const redirectUri = getRedirectUri(id); const state = buildState(); // Lazy import builder to avoid circular deps // eslint-disable-next-line @typescript-eslint/no-var-requires const { buildOAuthUrl } = require('./oauth'); const url = buildOAuthUrl({ providerId: id, redirectUri, state, clientIds: oauthClientIds }); if (url) router.push(url); }; const onGithubAppInstall = () => { const opaqueState = window.crypto?.randomUUID?.() || Math.random().toString(36).substring(2); const stateObj = { opaqueState, userId, companyId, inviteId }; const stateString = encodeURIComponent(JSON.stringify(stateObj)); const redirectUrl = `https://github.com/apps/${githubAppSlugId}/installations/new?state=${stateString}`; window.location.href = redirectUrl; }; const selectedProvider = useMemo(() => { if (!selectedProviderId) return null; if (selectedProviderId.toLowerCase() === 'githubapp') { // Special virtual provider for github app, not in providers list return { id: 'githubapp', name: 'GitHub App', connectionType: 'githubapp', iconColor: 'text-gray-900', placeholder: 'Install the GitHub App to connect your repositories.', }; } return providers.find( (p) => p.id.toLowerCase() === selectedProviderId.toLowerCase() ) || null; }, [selectedProviderId, providers]); const setPreview = (providerId: string, action: 'connect' | 'disconnect') => { setPreviewProviderId(providerId.toLowerCase()); setPreviewAction(action); }; const clearPreview = () => { setPreviewProviderId(null); setPreviewAction(null); }; const handleConnectBack = () => { setSelectedProviderIdAndCallback(null); setLinkUrl(''); setShowDataHandling(false); setPreviewProviderId(null); setPreviewAction(null); }; const cardVariants = { initial: { opacity: 0, y: 20 }, animate: { opacity: 1, y: 0 }, exit: { opacity: 0, y: -20 }, }; const shouldReduceMotion = useReducedMotion(); const ease = [0.22, 1, 0.36, 1] as const; const fadeOnly = { initial: { opacity: 0 }, animate: { opacity: 1 }, exit: { opacity: 0 }, }; const fadeIn = { hidden: { opacity: 0, y: shouldReduceMotion ? 0 : 8 }, visible: { opacity: 1, y: 0, transition: { duration: 0.3, ease } }, exit: { opacity: 0, y: shouldReduceMotion ? 0 : -4, transition: { duration: 0.2, ease } } }; // GitHub status helpers const githubConnectedAccount = useMemo(() => { return (connected || []).find(c => (c?.name || '').toLowerCase() === 'github'); }, [connected]); const isGithubConnected = !!githubConnectedAccount; const isGithubAppInstalled = useMemo(() => { const setupAction = githubConnectedAccount?.app_installation_info?.setupAction; return setupAction === 'install' || setupAction === 'created'; }, [githubConnectedAccount]); // Progress UI is outsourced to ConnectProgress return (
{/* Mobile: show progress above accounts, left-aligned */}
{showDataHandling ? (

How your data is handled

We understand that giving access to your private repositories can be a bit scary. So here's the deal: We install the KYD GitHub App in your account. The app has Contents read access - only to the repositories you select. Then, once you request a badge assessment, we read the repositories and analyze the code, then its deleted, forever. Your code is not accessible to anyone, not even us.

For other details, see our{' '} Privacy Policy .

) : selectedProvider && selectedProvider.id !== 'githubapp' ? (

{selectedProvider.connectionType === 'url' || (selectedProvider.connectionType || 'url') === 'link' ? `Use Public ${selectedProvider.name} Profile` : `Connect ${selectedProvider.name}`}

{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? (selectedProvider.placeholder || 'Enter your public profile URL.') : `Authorize with ${selectedProvider.name} to connect your account.`}

{(selectedProvider.connectionType === 'url' || selectedProvider.connectionType === 'link') ? ( { e.preventDefault(); onSubmitLink(selectedProvider.id); }} className="mt-6 space-y-4" initial="initial" animate="animate" exit="exit" variants={cardVariants} > {selectedProvider.id === 'linkedin' && ( <>

LinkedIn . This opens your public profile settings (you’ll see your shareable URL if you’re signed in).

LinkedIn data is not used to contribute to your score.

)}
setLinkUrl(e.target.value)} placeholder={selectedProvider.placeholder || 'https://example.com/your-profile'} required className="w-full border bg-transparent p-2 pl-9" style={{ color: 'var(--text-main)', borderColor: 'var(--icon-button-secondary)'}} onPaste={selectedProvider.id === 'linkedin' ? (e) => { const text = e.clipboardData.getData('text'); setLinkUrl(normalizeLinkedInInput(text)); e.preventDefault(); } : undefined} onBlur={selectedProvider.id === 'linkedin' ? (() => setLinkUrl(normalizeLinkedInInput(linkUrl))) : undefined} />
) : (
)}
) : selectedProvider && selectedProvider.id === 'githubapp' ? (
Connect Private GitHub Repositories

You've successfully linked your GitHub account!
To complete your profile, you can optionally allow access to your private repositories. This is useful if you'd like to highlight private work or share additional contributions for verification.

Would you like to connect your private repositories?

Connect your private repositories

) : ( {handleBackButton && ( )} {headerTitle}

{headerDescription}

{(() => { const oauthList = list.filter(p => p.connectionType === 'oauth'); const urlList = list.filter(p => (p.connectionType || 'url') === 'url' || p.connectionType === 'link'); const Row = (provider: typeof list[number]) => { const providerId = provider.id; const isRequired = requiredProviders?.includes(providerId); const isConnected = connectedIds.has(providerId.toLowerCase()); const needsReconnect = reconnectIds.has(providerId.toLowerCase()); const isOauth = provider.connectionType === 'oauth' const connectedUrl = connected.find(c => c.name.toLowerCase() === providerId.toLowerCase())?.url; const betaFlag = () => ( Beta ); const requiredFlag = () => (
!

Required by {companyName || 'your recruiter'}

); return (
{isConnected && connectedUrl ? ( {provider.name} {isRequired && !isConnected ? (requiredFlag()) : null} ) : (
{provider.name} {isRequired && !isConnected ? (requiredFlag()) : null}
)} {provider.beta ? betaFlag() : null}
{providerId.toLowerCase() === 'github' ? (
{/* Public (OAuth) */} {isGithubConnected ? (
Connected
) : (

Connect your public repositories

)} {/* Private (GitHub App) */} {isGithubAppInstalled ? (
Connected
) : (

Connect your private repositories

)}
) : isConnected && isDisconnecting !== providerId ? (
Connected
) : isDisconnecting === providerId ? (
) : (
<> {needsReconnect && !isRequired && ( )}
)}
); }; return ( <> {oauthList.length ? ( <>
OAuth
{oauthList.map(p => Row(p))} ) : null} {urlList.length ? ( <>
Public Urls
{urlList.map(p => Row(p))} ) : null} ); })()} )} {shouldShowContinueButton && ( )}
{/* Desktop: progress card to the right, sticky while scrolling */}
); } export default ConnectAccounts;