diff --git a/examples/demo/App.tsx b/examples/demo/App.tsx index c5593b511..f090f493c 100644 --- a/examples/demo/App.tsx +++ b/examples/demo/App.tsx @@ -5,7 +5,7 @@ * @format */ -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useCallback, useRef, useState} from 'react'; import { Alert, StatusBar, @@ -31,7 +31,7 @@ const configs: Record = { clientId: 'interactive.public', redirectUrl: 'io.identityserver.demo:/oauthredirect', additionalParameters: {}, - scopes: ['openid', 'profile', 'email', 'offline_access'] as const, + scopes: ['openid', 'profile', 'email', 'offline_access'], // serviceConfiguration: { // authorizationEndpoint: 'https://demo.duendesoftware.com/connect/authorize', @@ -44,7 +44,7 @@ const configs: Record = { clientId: 'VtXdAoGFcYzZ3IJaNy4UIS5RNHhdbKbU', redirectUrl: 'rnaa-demo://oauthredirect', additionalParameters: {}, - scopes: ['openid', 'profile', 'email', 'offline_access'] as const, + scopes: ['openid', 'profile', 'email', 'offline_access'], // serviceConfiguration: { // authorizationEndpoint: 'https://samples.auth0.com/authorize', @@ -74,14 +74,18 @@ interface ButtonProps { title: string; onPress: () => void; color?: string; + disabled?: boolean; } -const Button: React.FC = ({title, onPress, color = '#007AFF'}) => ( +const Button: React.FC = ({title, onPress, color = '#007AFF', disabled = false}) => ( [ styles.button, - {opacity: pressed ? 0.5 : 1, backgroundColor: color}, + {opacity: pressed || disabled ? 0.5 : 1, backgroundColor: color}, ]}> {title} @@ -118,82 +122,86 @@ const Row: React.FC = ({children}) => ( function App(): React.JSX.Element { const isDarkMode = useColorScheme() === 'dark'; const [authState, setAuthState] = useState(defaultAuthState); + const [isBusy, setIsBusy] = useState(false); + const pending = useRef(false); + const mounted = useRef(false); React.useEffect(() => { + mounted.current = true; prefetchConfiguration({ warmAndPrefetchChrome: true, connectionTimeoutSeconds: 5, ...configs.auth0, + }).catch(() => { + // Prefetch is optional; authorize will retry discovery when needed. }); + return () => { + mounted.current = false; + }; }, []); - const handleAuthorize = useCallback( - async (provider: keyof typeof configs) => { + const runAuthOperation = useCallback( + async (errorTitle: string, operation: () => Promise) => { + if (pending.current || !mounted.current) { + return; + } + pending.current = true; + setIsBusy(true); try { - const config = configs[provider]; - const newAuthState = await authorize({ - ...config, - connectionTimeoutSeconds: 5, - iosPrefersEphemeralSession: true, - }); - - setAuthState({ - hasLoggedInOnce: true, - provider: provider, - ...newAuthState, - }); + const nextState = await operation(); + if (mounted.current) { + setAuthState(nextState); + } } catch (error: any) { - Alert.alert('Failed to log in', error.message); + if (mounted.current) { + Alert.alert(errorTitle, error.message); + } + } finally { + pending.current = false; + if (mounted.current) { + setIsBusy(false); + } } }, [], ); - const handleRefresh = useCallback(async () => { - try { - const config = configs[authState.provider]; - const newAuthState = await refresh(config, { - refreshToken: authState.refreshToken, + const handleAuthorize = (provider: keyof typeof configs) => + runAuthOperation('Failed to log in', async () => { + const result = await authorize({ + ...configs[provider], + connectionTimeoutSeconds: 5, + iosPrefersEphemeralSession: true, }); + return {...result, hasLoggedInOnce: true, provider}; + }); - setAuthState(current => ({ - ...current, - ...newAuthState, - refreshToken: newAuthState.refreshToken || current.refreshToken, - })); - } catch (error: any) { - Alert.alert('Failed to refresh token', error.message); - } - }, [authState]); + const handleRefresh = () => + runAuthOperation('Failed to refresh token', async () => { + const result = await refresh(configs[authState.provider], { + refreshToken: authState.refreshToken, + }); + return { + ...authState, + ...result, + refreshToken: result.refreshToken || authState.refreshToken, + }; + }); - const handleRevoke = useCallback(async () => { - try { - const config = configs[authState.provider]; - await revoke(config, { + const handleRevoke = () => + runAuthOperation('Failed to revoke token', async () => { + await revoke(configs[authState.provider], { tokenToRevoke: authState.accessToken, sendClientId: true, }); + return defaultAuthState; + }); - setAuthState({ - hasLoggedInOnce: false, - provider: '', - accessToken: '', - accessTokenExpirationDate: '', - refreshToken: '', - }); - } catch (error: any) { - Alert.alert('Failed to revoke token', error.message); - } - }, [authState]); - - const showRevoke = useMemo(() => { - if (authState.accessToken) { - const config = configs[authState.provider]; - if (config.issuer || config?.serviceConfiguration?.revocationEndpoint) { - return true; - } - } - return false; - }, [authState]); + const providerConfig = configs[authState.provider]; + const showRevoke = Boolean( + authState.accessToken && + (providerConfig?.issuer || + providerConfig?.serviceConfiguration?.revocationEndpoint), + ); return ( @@ -201,6 +209,11 @@ function App(): React.JSX.Element {
+ {isBusy ? ( + + Authentication request in progress… + + ) : null}