diff --git a/packages/core/src/layout/SignInPage/SignInPage.tsx b/packages/core/src/layout/SignInPage/SignInPage.tsx index 2feb7be3bc..91bc251f90 100644 --- a/packages/core/src/layout/SignInPage/SignInPage.tsx +++ b/packages/core/src/layout/SignInPage/SignInPage.tsx @@ -14,79 +14,15 @@ * limitations under the License. */ -import React, { - FC, - useLayoutEffect, - useState, - ComponentType, - useMemo, -} from 'react'; +import React, { FC } from 'react'; import { Page } from '../Page'; import { Header } from '../Header'; import { Content } from '../Content/Content'; import { ContentHeader } from '../ContentHeader/ContentHeader'; -import { Grid, Typography, Button } from '@material-ui/core'; -import { InfoCard } from '../InfoCard/InfoCard'; -import { - SignInPageProps, - SignInResult, - useApi, - configApiRef, - useApiHolder, - ApiHolder, - errorApiRef, -} from '@backstage/core-api'; - -const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; - -type ProviderComponent = ComponentType; - -type ProviderLoader = (apis: ApiHolder) => Promise; - -type SignInProvider = { - component: ProviderComponent; - loader: ProviderLoader; -}; - -const GuestProvider: ProviderComponent = ({ onResult }) => ( - - onResult({ userId: 'guest' })} - > - Enter - - } - > - - Enter as a Guest User. -
- You will not have a verified identity, -
- meaning some features might be unavailable. -
-
-
-); - -const guestLoader: ProviderLoader = async () => { - return { userId: 'guest' }; -}; - -const guestProvider: SignInProvider = { - component: GuestProvider, - loader: guestLoader, -}; - -const signInProviders = { - guest: guestProvider, -}; - -export type SignInProviderId = keyof typeof signInProviders; +import { Grid } from '@material-ui/core'; +import { SignInPageProps, useApi, configApiRef } from '@backstage/core-api'; +import { useSignInProviders, SignInProviderId } from './providers'; +import Progress from '../../components/Progress'; export type Props = SignInPageProps & { providers: SignInProviderId[]; @@ -94,81 +30,12 @@ export type Props = SignInPageProps & { export const SignInPage: FC = ({ onResult, providers }) => { const configApi = useApi(configApiRef); - const errorApi = useApi(errorApiRef); - const apiHolder = useApiHolder(); - // We can't use storageApi here, as it might have a dependency on the IdentityApi - const selectedProvider = localStorage.getItem( - PROVIDER_STORAGE_KEY, - ) as SignInProviderId; + const [loading, providerElements] = useSignInProviders(providers, onResult); - const [attempting, setAttempting] = useState(Boolean(selectedProvider)); - - useLayoutEffect(() => { - if (!attempting || selectedProvider === null) { - return undefined; - } - - const provider = signInProviders[selectedProvider]; - if (!provider) { - setAttempting(false); - return undefined; - } - - let didCancel = false; - provider - .loader(apiHolder) - .then(result => { - if (didCancel) { - return; - } - setAttempting(false); - if (result) { - onResult({ - ...result, - logout: async () => { - localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.logout?.(); - }, - }); - } - }) - .catch(error => { - if (!didCancel) { - errorApi.post(error); - } - }); - - return () => { - didCancel = true; - }; - }, [attempting, errorApi, onResult, apiHolder, providers, selectedProvider]); - - const providerElements = useMemo( - () => - providers.map(providerId => { - const provider = signInProviders[providerId]; - if (!provider) { - throw new Error(`Unknown sign-in provider: ${providerId}`); - } - const { component: Component } = provider; - - const handleResult = (result: SignInResult) => { - localStorage.setItem(PROVIDER_STORAGE_KEY, providerId); - - onResult({ - ...result, - logout: async () => { - localStorage.removeItem(PROVIDER_STORAGE_KEY); - await result.logout?.(); - }, - }); - }; - - return ; - }), - [providers, onResult], - ); + if (loading) { + return ; + } return ( diff --git a/packages/core/src/layout/SignInPage/guestProvider.tsx b/packages/core/src/layout/SignInPage/guestProvider.tsx new file mode 100644 index 0000000000..78d0191ba9 --- /dev/null +++ b/packages/core/src/layout/SignInPage/guestProvider.tsx @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Grid, Typography, Button } from '@material-ui/core'; +import { InfoCard } from '../InfoCard/InfoCard'; +import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; + +const Component: ProviderComponent = ({ onResult }) => ( + + onResult({ userId: 'guest' })} + > + Enter + + } + > + + Enter as a Guest User. +
+ You will not have a verified identity, +
+ meaning some features might be unavailable. +
+
+
+); + +const loader: ProviderLoader = async () => { + return { userId: 'guest' }; +}; + +export const guestProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx new file mode 100644 index 0000000000..ae9c3cc010 --- /dev/null +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -0,0 +1,128 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; +import { guestProvider } from './guestProvider'; +import { + SignInPageProps, + SignInResult, + useApi, + useApiHolder, + errorApiRef, +} from '@backstage/core-api'; + +const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; + +const signInProviders = { + guest: guestProvider, +}; + +export type SignInProviderId = keyof typeof signInProviders; + +export const useSignInProviders = ( + providers: SignInProviderId[], + onResult: SignInPageProps['onResult'], +) => { + const errorApi = useApi(errorApiRef); + const apiHolder = useApiHolder(); + const [loading, setLoading] = useState(true); + + // This decorates the result with logout logic from this hook + const handleWrappedResult = useCallback( + (result: SignInResult) => { + onResult({ + ...result, + logout: async () => { + localStorage.removeItem(PROVIDER_STORAGE_KEY); + await result.logout?.(); + }, + }); + }, + [onResult], + ); + + // In this effect we check if the user has already selected an existing login + // provider, and in that case try to load an existing session for the provider. + useLayoutEffect(() => { + if (!loading) { + return undefined; + } + + // We can't use storageApi here, as it might have a dependency on the IdentityApi + const selectedProvider = localStorage.getItem( + PROVIDER_STORAGE_KEY, + ) as SignInProviderId; + + // No provider selected, let the user pick one + if (selectedProvider === null) { + setLoading(false); + return undefined; + } + + const provider = signInProviders[selectedProvider]; + if (!provider) { + setLoading(false); + return undefined; + } + + let didCancel = false; + provider + .loader(apiHolder) + .then(result => { + if (didCancel) { + return; + } + if (result) { + handleWrappedResult(result); + } + setLoading(false); + }) + .catch(error => { + if (didCancel) { + return; + } + errorApi.post(error); + setLoading(false); + }); + + return () => { + didCancel = true; + }; + }, [loading, errorApi, onResult, apiHolder, providers, handleWrappedResult]); + + // This renders all available sign-in providers + const elements = useMemo( + () => + providers.map(providerId => { + const provider = signInProviders[providerId]; + if (!provider) { + throw new Error(`Unknown sign-in provider: ${providerId}`); + } + const { Component } = provider; + + const handleResult = (result: SignInResult) => { + localStorage.setItem(PROVIDER_STORAGE_KEY, providerId); + + handleWrappedResult(result); + }; + + return ; + }), + [providers, handleWrappedResult], + ); + + return [loading, elements]; +}; diff --git a/packages/core/src/layout/SignInPage/types.ts b/packages/core/src/layout/SignInPage/types.ts new file mode 100644 index 0000000000..e13cda5ddd --- /dev/null +++ b/packages/core/src/layout/SignInPage/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentType } from 'react'; +import { SignInPageProps, SignInResult, ApiHolder } from '@backstage/core-api'; + +export type ProviderComponent = ComponentType; + +export type ProviderLoader = ( + apis: ApiHolder, +) => Promise; + +export type SignInProvider = { + Component: ProviderComponent; + loader: ProviderLoader; +};