diff --git a/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx b/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx index 1fd32545c..8b42a6158 100644 --- a/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx +++ b/src/renderer/routes/github/LoginWithDeviceFlow.test.tsx @@ -61,7 +61,7 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { ]); await screen.findByTestId('device-user-code'); - expect(screen.getByTestId('device-verification-link')).toBeInTheDocument(); + expect(screen.getByTestId('open-browser-button')).toBeInTheDocument(); // Verify auto-copy and auto-open were called expect(copyToClipboardSpy).toHaveBeenCalledWith('USER-1234'); @@ -94,6 +94,58 @@ describe('renderer/routes/github/LoginWithDeviceFlow.tsx', () => { await screen.findByTestId('device-user-code'); }); + it('should show status indicators after device flow starts', async () => { + const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce({ + hostname: 'github.com', + clientId: 'test-id', + deviceCode: 'device-code', + userCode: 'USER-1234', + verificationUri: 'https://github.com/login/device', + intervalSeconds: 5, + expiresAt: Date.now() + 900000, + }); + + renderWithProviders(, { + loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, + }); + + await userEvent.click(screen.getByTestId('device-scope-public')); + await screen.findByTestId('device-user-code'); + + expect(screen.getByText('Code copied to clipboard')).toBeInTheDocument(); + expect(screen.getByText('Opened GitHub.com')).toBeInTheDocument(); + expect(screen.getByText('Waiting for authorization...')).toBeInTheDocument(); + }); + + it('should open browser and copy link via footer buttons', async () => { + const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce({ + hostname: 'github.com', + clientId: 'test-id', + deviceCode: 'device-code', + userCode: 'USER-1234', + verificationUri: 'https://github.com/login/device', + intervalSeconds: 5, + expiresAt: Date.now() + 900000, + }); + + renderWithProviders(, { + loginWithDeviceFlowStart: loginWithDeviceFlowStartMock, + }); + + await userEvent.click(screen.getByTestId('device-scope-public')); + await screen.findByTestId('device-user-code'); + + // Reset spies to isolate button interactions + openExternalLinkSpy.mockClear(); + copyToClipboardSpy.mockClear(); + + await userEvent.click(screen.getByTestId('open-browser-button')); + expect(openExternalLinkSpy).toHaveBeenCalledWith('https://github.com/login/device'); + + await userEvent.click(screen.getByTestId('copy-link-button')); + expect(copyToClipboardSpy).toHaveBeenCalledWith('https://github.com/login/device'); + }); + it('should copy user code to clipboard when clicking copy button', async () => { const loginWithDeviceFlowStartMock = vi.fn().mockResolvedValueOnce({ hostname: 'github.com', diff --git a/src/renderer/routes/github/LoginWithDeviceFlow.tsx b/src/renderer/routes/github/LoginWithDeviceFlow.tsx index 9d25dfaf9..fdad338e4 100644 --- a/src/renderer/routes/github/LoginWithDeviceFlow.tsx +++ b/src/renderer/routes/github/LoginWithDeviceFlow.tsx @@ -1,8 +1,15 @@ import { type FC, type ReactNode, useCallback, useEffect, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; -import { CopyIcon, SignInIcon, SyncIcon } from '@primer/octicons-react'; -import { Banner, Button, IconButton, Link as PrimerLink, Stack, Text } from '@primer/react'; +import { + CheckCircleFillIcon, + CopyIcon, + GlobeIcon, + LinkIcon, + SignInIcon, + SyncIcon, +} from '@primer/octicons-react'; +import { Banner, Button, Heading, IconButton, Stack, Text } from '@primer/react'; import { Constants } from '../../constants'; @@ -128,6 +135,18 @@ export const GitHubLoginWithDeviceFlowRoute: FC = () => { } }, [session?.userCode]); + const handleOpenBrowser = useCallback(() => { + if (session?.verificationUri) { + openExternalLink(session.verificationUri as Link); + } + }, [session?.verificationUri]); + + const handleCopyVerificationLink = useCallback(async () => { + if (session?.verificationUri) { + await copyToClipboard(session.verificationUri); + } + }, [session?.verificationUri]); + // Render UI states as separate functions for clarity const renderSessionUI = () => { if (!session) { @@ -136,17 +155,11 @@ export const GitHubLoginWithDeviceFlowRoute: FC = () => { return ( - - - Go to{' '} - - {session.verificationUri} - - - and enter your device code when prompted: - + + Authorize the app using this code. + - + { fontSize: '32px', fontWeight: 'bold', fontFamily: 'monospace', + letterSpacing: '0.05em', }} > {session.userCode} @@ -164,27 +178,29 @@ export const GitHubLoginWithDeviceFlowRoute: FC = () => { icon={CopyIcon} onClick={handleCopyUserCode} size="small" - variant="default" + variant="invisible" /> - - We're waiting for authorization... - - {isPolling && ( - - - - Polling for authorization + + + + Code copied to clipboard + + + + + + Opened{' '} + {session.hostname === Constants.GITHUB_HOSTNAME ? 'GitHub.com' : session.hostname} - )} + + + + Waiting for authorization... + + ); }; @@ -300,11 +316,37 @@ export const GitHubLoginWithDeviceFlowRoute: FC = () => { {mainContent} -
- -
+ {session ? ( +
+ + + + + +
+ ) : ( + + )} ); };