import { useState, useRef, useCallback, useEffect } from 'react' import { GithubLogo, CircleNotch, ArrowClockwise, Copy, Check } from '@phosphor-icons/react' import { invoke } from '@tauri-apps/api/core' import { isTauri, mockInvoke } from '../mock-tauri' import { openExternalUrl } from '../utils/url' import type { DeviceFlowStart, DeviceFlowPollResult, GitHubUser } from '../types' function tauriCall(cmd: string, args: Record = {}): Promise { return isTauri() ? invoke(cmd, args) : mockInvoke(cmd, args) } type OAuthStatus = 'idle' | 'waiting' | 'error' /** Process a device flow poll result. Returns 'done' if auth complete, 'continue' to keep polling. */ function processPollResult( result: DeviceFlowPollResult, callbacks: { onComplete: (token: string) => Promise onExpired: () => void onError: (msg: string) => void }, ): 'done' | 'continue' { if (result.status === 'complete' && result.access_token) { callbacks.onComplete(result.access_token) return 'done' } if (result.status === 'expired') { callbacks.onExpired() return 'done' } if (result.status === 'error') { callbacks.onError(result.error ?? 'Authorization failed.') return 'done' } return 'continue' } interface GitHubDeviceFlowProps { onConnected: (token: string, username: string) => void } export function GitHubDeviceFlow({ onConnected }: GitHubDeviceFlowProps) { const [oauthStatus, setOauthStatus] = useState('idle') const [userCode, setUserCode] = useState(null) const [verificationUri, setVerificationUri] = useState(null) const [errorMessage, setErrorMessage] = useState(null) const pollingRef = useRef(false) const deviceCodeRef = useRef(null) const stopPolling = useCallback(() => { pollingRef.current = false deviceCodeRef.current = null }, []) useEffect(() => { return () => { pollingRef.current = false } }, []) const handleLogin = useCallback(async () => { setOauthStatus('waiting') setErrorMessage(null) setUserCode(null) try { const flowStart = await tauriCall('github_device_flow_start') setUserCode(flowStart.user_code) setVerificationUri(flowStart.verification_uri) deviceCodeRef.current = flowStart.device_code openExternalUrl(flowStart.verification_uri).catch(() => {}) pollingRef.current = true const intervalMs = Math.max(flowStart.interval * 1000, 5000) const pollLoop = async () => { while (pollingRef.current && deviceCodeRef.current) { await new Promise(r => setTimeout(r, intervalMs)) if (!pollingRef.current) break const result = await tauriCall('github_device_flow_poll', { deviceCode: deviceCodeRef.current, }) const outcome = processPollResult(result, { onComplete: async (token) => { const user = await tauriCall('github_get_user', { token }) stopPolling() setOauthStatus('idle') setUserCode(null) onConnected(token, user.login) }, onExpired: () => { stopPolling() setOauthStatus('error') setErrorMessage('Authorization expired. Please try again.') }, onError: (msg) => { stopPolling() setOauthStatus('error') setErrorMessage(msg) }, }) if (outcome === 'done') return } } pollLoop().catch(err => { stopPolling() setOauthStatus('error') setErrorMessage(typeof err === 'string' ? err : err instanceof Error ? err.message : 'Polling failed.') }) } catch (err) { setOauthStatus('error') setErrorMessage(typeof err === 'string' ? err : err instanceof Error ? err.message : 'Failed to start login.') } }, [onConnected, stopPolling]) const resetOAuth = useCallback(() => { stopPolling() setOauthStatus('idle') setUserCode(null) setVerificationUri(null) setErrorMessage(null) }, [stopPolling]) if (oauthStatus === 'waiting' && userCode) { return } return { resetOAuth(); handleLogin() } : undefined} /> } function DeviceCodeView({ userCode, verificationUri, onCancel }: { userCode: string; verificationUri: string | null; onCancel: () => void }) { const [copied, setCopied] = useState(false) const handleCopyCode = useCallback(() => { navigator.clipboard.writeText(userCode).then(() => { setCopied(true) setTimeout(() => setCopied(false), 2000) }).catch(() => {}) }, [userCode]) const handleOpenUrl = useCallback(() => { if (verificationUri) openExternalUrl(verificationUri).catch(() => {}) }, [verificationUri]) return (
Enter this code on GitHub:
{userCode}
{verificationUri && ( )}
Waiting for authorization...
) } function LoginButton({ onLogin, disabled, errorMessage, onRetry }: { onLogin: () => void; disabled: boolean; errorMessage: string | null; onRetry?: () => void }) { return (
{errorMessage && (
{errorMessage} {onRetry && ( )}
)}
) }