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 ? (
+
+ ) : (
+
+ )}
);
};