1- import { type FC , useCallback , useEffect , useState } from 'react' ;
1+ import {
2+ type FC ,
3+ type ReactNode ,
4+ useCallback ,
5+ useEffect ,
6+ useState ,
7+ } from 'react' ;
28import { useLocation , useNavigate } from 'react-router-dom' ;
39
410import { CopyIcon , SignInIcon , SyncIcon } from '@primer/octicons-react' ;
@@ -148,6 +154,140 @@ export const LoginWithDeviceFlowRoute: FC = () => {
148154 }
149155 } , [ session ?. userCode ] ) ;
150156
157+ // Render UI states as separate functions for clarity
158+ const renderSessionUI = ( ) => {
159+ if ( ! session ) {
160+ return null ;
161+ }
162+
163+ return (
164+ < Stack direction = "vertical" gap = "normal" >
165+ < Stack direction = "vertical" gap = "condensed" >
166+ < Text as = "p" >
167+ Go to{ ' ' }
168+ < PrimerLink
169+ data-testid = "device-verification-link"
170+ href = { session . verificationUri }
171+ >
172+ < code > { session . verificationUri } </ code >
173+ </ PrimerLink >
174+ </ Text >
175+ < Text as = "p" > and enter your device code when prompted:</ Text >
176+ </ Stack >
177+
178+ < Stack
179+ align = "center"
180+ direction = "horizontal"
181+ justify = "space-between"
182+ padding = "condensed"
183+ >
184+ < Text
185+ as = "div"
186+ data-testid = "device-user-code"
187+ style = { {
188+ fontSize : '32px' ,
189+ fontWeight : 'bold' ,
190+ fontFamily : 'monospace' ,
191+ } }
192+ >
193+ { session . userCode }
194+ </ Text >
195+ < IconButton
196+ aria-label = "Copy device code"
197+ data-testid = "copy-device-code"
198+ icon = { CopyIcon }
199+ onClick = { handleCopyUserCode }
200+ size = "small"
201+ variant = "default"
202+ />
203+ </ Stack >
204+
205+ < Text as = "p" size = "small" >
206+ We're waiting for authorization...
207+ </ Text >
208+ { isPolling && (
209+ < Stack align = "center" gap = "normal" >
210+ < IconButton
211+ aria-label = "Polling"
212+ className = "animate-spin"
213+ icon = { SyncIcon }
214+ size = "small"
215+ variant = "invisible"
216+ />
217+ < Text as = "em" size = "small" >
218+ Polling for authorization
219+ </ Text >
220+ </ Stack >
221+ ) }
222+ </ Stack >
223+ ) ;
224+ } ;
225+
226+ const renderScopeChoiceUI = ( ) => (
227+ < Stack direction = "vertical" gap = "normal" >
228+ < Text as = "p" > Receive notifications for:</ Text >
229+
230+ < Stack align = "center" direction = "vertical" >
231+ < Button
232+ block
233+ data-testid = "device-scope-full"
234+ labelWrap
235+ onClick = { ( ) => setScopeChoice ( 'full' ) }
236+ variant = "primary"
237+ >
238+ < Stack gap = "none" >
239+ < Text as = "strong" > Public and Private</ Text >
240+ < Text size = "small" >
241+ Best experience, but requires broader permissions.
242+ </ Text >
243+ < Text as = "em" size = "small" >
244+ Scopes: { getRecommendedScopeNames ( ) . join ( ', ' ) }
245+ </ Text >
246+ </ Stack >
247+ </ Button >
248+
249+ < Button
250+ block
251+ data-testid = "device-scope-public"
252+ labelWrap
253+ onClick = { ( ) => setScopeChoice ( 'public' ) }
254+ >
255+ < Stack gap = "none" >
256+ < Text > Public</ Text >
257+ < Text size = "small" >
258+ Limited experience with least privilege permissions.
259+ </ Text >
260+ < Text as = "em" size = "small" >
261+ Scopes: { getAlternateScopeNames ( ) . join ( ', ' ) }
262+ </ Text >
263+ </ Stack >
264+ </ Button >
265+ </ Stack >
266+ </ Stack >
267+ ) ;
268+
269+ const renderInitializingUI = ( ) => (
270+ < Stack align = "center" direction = "vertical" gap = "normal" >
271+ < IconButton
272+ aria-label = "Initializing"
273+ className = { 'animate-spin' }
274+ icon = { SyncIcon }
275+ size = "large"
276+ variant = "invisible"
277+ />
278+ < Text > Initializing authentication...</ Text >
279+ </ Stack >
280+ ) ;
281+
282+ let mainContent : ReactNode ;
283+ if ( session ) {
284+ mainContent = renderSessionUI ( ) ;
285+ } else if ( ! scopeChoice ) {
286+ mainContent = renderScopeChoiceUI ( ) ;
287+ } else {
288+ mainContent = renderInitializingUI ( ) ;
289+ }
290+
151291 return (
152292 < Page testId = "Login With Device Flow" >
153293 < Header icon = { SignInIcon } > Authorize with GitHub</ Header >
@@ -168,121 +308,7 @@ export const LoginWithDeviceFlowRoute: FC = () => {
168308 variant = "critical"
169309 />
170310 ) }
171-
172- { /** GitHub Device Code Flow session */ }
173- { session ? (
174- < Stack direction = "vertical" gap = "normal" >
175- < Stack direction = "vertical" gap = "condensed" >
176- < Text as = "p" >
177- Go to{ ' ' }
178- < PrimerLink
179- data-testid = "device-verification-link"
180- href = { session . verificationUri }
181- >
182- < code > { session . verificationUri } </ code >
183- </ PrimerLink >
184- </ Text >
185- < Text as = "p" > and enter your device code when prompted:</ Text >
186- </ Stack >
187-
188- < Stack
189- align = "center"
190- direction = "horizontal"
191- justify = "space-between"
192- padding = "condensed"
193- >
194- < Text
195- as = "div"
196- data-testid = "device-user-code"
197- style = { {
198- fontSize : '32px' ,
199- fontWeight : 'bold' ,
200- fontFamily : 'monospace' ,
201- } }
202- >
203- { session . userCode }
204- </ Text >
205- < IconButton
206- aria-label = "Copy device code"
207- data-testid = "copy-device-code"
208- icon = { CopyIcon }
209- onClick = { handleCopyUserCode }
210- size = "small"
211- variant = "default"
212- />
213- </ Stack >
214-
215- < Text as = "p" size = "small" >
216- We're waiting for authorization...
217- </ Text >
218- { isPolling && (
219- < Stack align = "center" gap = "normal" >
220- < IconButton
221- aria-label = "Polling"
222- className = "animate-spin"
223- icon = { SyncIcon }
224- size = "small"
225- variant = "invisible"
226- />
227- < Text as = "em" size = "small" >
228- Polling for authorization
229- </ Text >
230- </ Stack >
231- ) }
232- </ Stack >
233- ) : ! scopeChoice ? (
234- < Stack direction = "vertical" gap = "normal" >
235- < Text as = "p" > Receive notifications for:</ Text >
236-
237- < Stack align = "center" direction = "vertical" >
238- < Button
239- block
240- data-testid = "device-scope-full"
241- labelWrap
242- onClick = { ( ) => setScopeChoice ( 'full' ) }
243- variant = "primary"
244- >
245- < Stack gap = "none" >
246- < Text as = "strong" > Public and Private</ Text >
247- < Text size = "small" >
248- Best experience, but requires broader permissions.
249- </ Text >
250- < Text as = "em" size = "small" >
251- Scopes: { getRecommendedScopeNames ( ) . join ( ', ' ) }
252- </ Text >
253- </ Stack >
254- </ Button >
255-
256- < Button
257- block
258- data-testid = "device-scope-public"
259- labelWrap
260- onClick = { ( ) => setScopeChoice ( 'public' ) }
261- >
262- < Stack gap = "none" >
263- < Text > Public</ Text >
264- < Text size = "small" >
265- Limited experience with least privilege permissions.
266- </ Text >
267- < Text as = "em" size = "small" >
268- Scopes: { getAlternateScopeNames ( ) . join ( ', ' ) }
269- </ Text >
270- </ Stack >
271- </ Button >
272- </ Stack >
273- </ Stack >
274- ) : (
275- < Stack align = "center" direction = "vertical" gap = "normal" >
276- < IconButton
277- aria-label = "Initializing"
278- className = { 'animate-spin' }
279- icon = { SyncIcon }
280- size = "large"
281- variant = "invisible"
282- />
283- < Text > Initializing authentication...</ Text >
284- </ Stack >
285- ) }
311+ { mainContent }
286312 </ Contents >
287313
288314 < Footer justify = "space-between" >
0 commit comments