()
| 13 | type CallbackStatus = 'pending' | 'success' | 'error'; |
| 14 | |
| 15 | export function IntegrationCallback() { |
| 16 | const { provider } = useParams<{ provider: string }>(); |
| 17 | const [searchParams] = useSearchParams(); |
| 18 | const navigate = useNavigate(); |
| 19 | const userId = useMemo(() => getCurrentUserId(), []); |
| 20 | |
| 21 | const [status, setStatus] = useState<CallbackStatus>('pending'); |
| 22 | const [message, setMessage] = useState('Exchanging authorization code…'); |
| 23 | const exchangeStartedRef = useRef(false); |
| 24 | |
| 25 | useEffect(() => { |
| 26 | if (!provider) { |
| 27 | setStatus('error'); |
| 28 | setMessage('Missing provider information in callback URL.'); |
| 29 | return; |
| 30 | } |
| 31 | |
| 32 | const providerId = provider; |
| 33 | |
| 34 | const errorParam = searchParams.get('error'); |
| 35 | if (errorParam) { |
| 36 | setStatus('error'); |
| 37 | setMessage(`Provider returned an error: ${errorParam}`); |
| 38 | return; |
| 39 | } |
| 40 | |
| 41 | const code = searchParams.get('code'); |
| 42 | const state = searchParams.get('state'); |
| 43 | |
| 44 | if (!code || !state) { |
| 45 | setStatus('error'); |
| 46 | setMessage('Unable to complete OAuth without an authorization code and state.'); |
| 47 | return; |
| 48 | } |
| 49 | |
| 50 | if (exchangeStartedRef.current) { |
| 51 | return; |
| 52 | } |
| 53 | exchangeStartedRef.current = true; |
| 54 | |
| 55 | const authCode = code; |
| 56 | const authState = state; |
| 57 | |
| 58 | const redirectUri = `${window.location.origin}/integrations/callback/${providerId}`; |
| 59 | let cancelled = false; |
| 60 | |
| 61 | async function exchangeCode() { |
| 62 | try { |
| 63 | const connection = await api.integrations.completeOAuth(providerId, { |
| 64 | userId, |
| 65 | code: authCode, |
| 66 | state: authState, |
| 67 | redirectUri, |
| 68 | }); |
| 69 | |
| 70 | if (cancelled) { |
| 71 | return; |
| 72 | } |
nothing calls this directly
no test coverage detected