;
-
-export const LoginStory: Story = {
- args: {
- isGoogle: true,
- termsOfService: (
- I have read and agree to privacy notice and terms of use
- ),
- text: (
-
- Please sign in to your Data Explorer account{" "}
-
-
- ),
- title: "Sign in to your account",
- warning: (
-
-
WARNING NOTICE
-
This is a warning notice.
-
- ),
- },
-};
diff --git a/src/components/Login/login.tsx b/src/components/Login/login.tsx
index 20d041fe..8e8c9a5e 100644
--- a/src/components/Login/login.tsx
+++ b/src/components/Login/login.tsx
@@ -1,13 +1,13 @@
import { Checkbox, Typography } from "@mui/material";
-import React, { ChangeEvent, ReactNode, useState } from "react";
-import { useAuthentication } from "../../hooks/useAuthentication/useAuthentication";
-import { LoginButton } from "../common/Button/components/LoginButton/loginButton";
+import React, { ChangeEvent, useCallback, useState } from "react";
+import { useAuth } from "../../providers/authentication/auth/hook";
+import { ProviderId } from "../../providers/authentication/common/types";
import { CheckedIcon } from "../common/CustomIcon/components/CheckedIcon/checkedIcon";
-import { GoogleIcon } from "../common/CustomIcon/components/GoogleIcon/googleIcon";
import { UncheckedErrorIcon } from "../common/CustomIcon/components/UncheckedErrorIcon/uncheckedErrorIcon";
import { UncheckedIcon } from "../common/CustomIcon/components/UncheckedIcon/uncheckedIcon";
import { RoundedPaper } from "../common/Paper/paper.styles";
import { SectionContent } from "../common/Section/section.styles";
+import { Button } from "./components/Button/button";
import {
LoginAgreement,
LoginSection,
@@ -17,35 +17,31 @@ import {
LoginWrapper,
TermsOfService,
} from "./login.styles";
+import { Props } from "./types";
-export interface LoginProps {
- isGoogle?: boolean;
- termsOfService?: ReactNode;
- text?: ReactNode;
- title: string;
- warning?: ReactNode;
-}
-
-export const Login = ({
- isGoogle = false,
+export const Login = ({
+ providers = [],
termsOfService,
text,
title,
warning,
-}: LoginProps): JSX.Element => {
- const { authenticateUser } = useAuthentication();
+}: Props
): JSX.Element => {
+ const { service: { requestLogin } = {} } = useAuth();
const [isError, setIsError] = useState(false);
const [isInAgreement, setIsInAgreement] = useState(!termsOfService);
// Authenticates the user, if the user has agreed to the terms of service.
// If the terms of service are not accepted, set the terms of service error state to true.
- const onAuthenticateUser = (): void => {
- if (!isInAgreement) {
- setIsError(true);
- return;
- }
- authenticateUser();
- };
+ const onLogin = useCallback(
+ (providerId: ProviderId): void => {
+ if (!isInAgreement) {
+ setIsError(true);
+ return;
+ }
+ requestLogin?.(providerId);
+ },
+ [isInAgreement, requestLogin]
+ );
// Callback fired when the checkbox value is changed.
// Clears the terms of service error state and sets state isInAgreement with checkbox selected value.
@@ -75,11 +71,15 @@ export const Login = ({
{termsOfService}
)}
- {isGoogle && (
-
- Google
-
- )}
+ {providers?.map((provider) => (
+
+ ))}
diff --git a/src/components/Login/types.ts b/src/components/Login/types.ts
new file mode 100644
index 00000000..f2053034
--- /dev/null
+++ b/src/components/Login/types.ts
@@ -0,0 +1,11 @@
+import { ClientSafeProvider } from "next-auth/react";
+import { ReactNode } from "react";
+import { OAuthProvider } from "../../config/entities";
+
+export interface Props {
+ providers?: ClientSafeProvider[] | OAuthProvider
[];
+ termsOfService?: ReactNode;
+ text?: ReactNode;
+ title: string;
+ warning?: ReactNode;
+}
diff --git a/src/components/common/Banner/components/SessionTimeout/sessionTimeout.tsx b/src/components/common/Banner/components/SessionTimeout/sessionTimeout.tsx
index 3b3dee65..d56ffb05 100644
--- a/src/components/common/Banner/components/SessionTimeout/sessionTimeout.tsx
+++ b/src/components/common/Banner/components/SessionTimeout/sessionTimeout.tsx
@@ -1,6 +1,6 @@
import { AlertProps as MAlertProps } from "@mui/material";
import React, { Fragment, ReactNode } from "react";
-import { useSessionTimeout } from "../../../../../hooks/useSessionTimeout";
+import { useSessionTimeout } from "../../../../../hooks/authentication/session/useSessionTimeout";
import { Banner } from "./sessionTimeout.styles";
export interface SessionTimeoutProps extends Omit {
diff --git a/src/components/common/Button/components/LoginButton/loginButton.styles.ts b/src/components/common/Button/components/LoginButton/loginButton.styles.ts
deleted file mode 100644
index c25c0c95..00000000
--- a/src/components/common/Button/components/LoginButton/loginButton.styles.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import styled from "@emotion/styled";
-import { ButtonSecondary } from "../ButtonSecondary/buttonSecondary";
-
-export const LoginButton = styled(ButtonSecondary)`
- .MuiButton-endIcon {
- margin-right: -6px;
- }
-`;
-
-export const LoginButtonText = styled.span`
- flex: 1;
- text-align: left;
-`;
diff --git a/src/components/common/Button/components/LoginButton/loginButton.tsx b/src/components/common/Button/components/LoginButton/loginButton.tsx
deleted file mode 100644
index 107f46c6..00000000
--- a/src/components/common/Button/components/LoginButton/loginButton.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from "react";
-import { ButtonProps } from "../../button";
-import { LoginButton as Button, LoginButtonText } from "./loginButton.styles";
-
-export type LoginButtonProps = Exclude;
-
-export const LoginButton = ({
- children,
- ...props
-}: LoginButtonProps): JSX.Element => {
- return (
-
- );
-};
diff --git a/src/components/common/CustomIcon/common/entities.ts b/src/components/common/CustomIcon/common/entities.ts
index 6d7f66a8..9df8b75c 100644
--- a/src/components/common/CustomIcon/common/entities.ts
+++ b/src/components/common/CustomIcon/common/entities.ts
@@ -1,6 +1,3 @@
import { SvgIconProps } from "@mui/material";
-export interface CustomSVGIconProps extends SvgIconProps {
- fontSize?: SvgIconProps["fontSize"];
- viewBox?: string;
-}
+export type CustomSVGIconProps = SvgIconProps;
diff --git a/src/components/common/CustomIcon/components/GoogleIcon/googleIcon.tsx b/src/components/common/CustomIcon/components/GoogleIcon/googleIcon.tsx
index 966515c2..c635398c 100644
--- a/src/components/common/CustomIcon/components/GoogleIcon/googleIcon.tsx
+++ b/src/components/common/CustomIcon/components/GoogleIcon/googleIcon.tsx
@@ -1,6 +1,5 @@
-import { SvgIcon } from "@mui/material";
+import { SvgIcon, SvgIconProps } from "@mui/material";
import React from "react";
-import { CustomSVGIconProps } from "../../common/entities";
/**
* Custom Google logo icon.
@@ -9,8 +8,8 @@ import { CustomSVGIconProps } from "../../common/entities";
export const GoogleIcon = ({
fontSize = "xsmall",
viewBox = "0 0 20 20",
- ...props /* Spread props to allow for Mui SvgIconProps specific prop overrides e.g. "htmlColor". */
-}: CustomSVGIconProps): JSX.Element => {
+ ...props
+}: SvgIconProps): JSX.Element => {
return (
{
+ anchorEl: E | null;
onClose: () => void;
onDisableScrollLock: () => void;
onEnableScrollLock: () => void;
- onOpen: (event: MouseEvent) => void;
- onToggleOpen: (event: MouseEvent) => void;
+ onOpen: (event: MouseEvent) => void;
+ onToggleOpen: (event: MouseEvent) => void;
open: boolean;
}
@@ -15,8 +17,10 @@ export interface UseMenu {
* Menu functionality for menu dropdown, with menu position.
* @returns menu functionality.
*/
-export const useMenu = (): UseMenu => {
- const [anchorEl, setAnchorEl] = useState(null);
+export const useMenu = <
+ E extends MPopperProps["anchorEl"] = MPopperProps["anchorEl"]
+>(): UseMenu => {
+ const [anchorEl, setAnchorEl] = useState(null);
const open = useMemo(() => Boolean(anchorEl), [anchorEl]);
// Closes menu.
@@ -35,13 +39,13 @@ export const useMenu = (): UseMenu => {
}, []);
// Opens menu.
- const onOpen = useCallback((event: MouseEvent): void => {
+ const onOpen = useCallback((event: MouseEvent): void => {
setAnchorEl(event.currentTarget);
}, []);
// Toggles menu open/close.
const onToggleOpen = useCallback(
- (event: MouseEvent): void => {
+ (event: MouseEvent): void => {
if (open) {
setAnchorEl(null);
} else {
diff --git a/src/config/entities.ts b/src/config/entities.ts
index 0f55d952..62657144 100644
--- a/src/config/entities.ts
+++ b/src/config/entities.ts
@@ -6,7 +6,9 @@ import { HeroTitle } from "../components/common/Title/title";
import { FooterProps } from "../components/Layout/components/Footer/footer";
import { HeaderProps } from "../components/Layout/components/Header/header";
import { ExploreMode } from "../hooks/useExploreMode";
-import { AuthContextProps } from "../providers/authentication";
+import { AuthState } from "../providers/authentication/auth/types";
+import { UserProfile } from "../providers/authentication/authentication/types";
+import { ProviderId } from "../providers/authentication/common/types";
import { ExploreState } from "../providers/exploreState";
import { FileManifestState } from "../providers/fileManifestState";
import { SystemStatus, SystemStatusResponse } from "../providers/systemStatus";
@@ -24,14 +26,19 @@ export interface AnalyticsConfig {
* Interface to define the authentication configuration for a given site.
*/
export interface AuthenticationConfig {
- googleGISAuthConfig?: GoogleGISAuthConfig;
+ providers?: OAuthProvider[];
+ services?: AuthService[];
termsOfService?: ReactNode;
- terraAuthConfig?: TerraAuthConfig;
text?: ReactNode;
title: string;
warning?: ReactNode;
}
+export interface AuthService {
+ endpoint: Record;
+ id: string;
+}
+
/**
* Interface to define the set of components that will be used for the back page.
*/
@@ -206,15 +213,6 @@ export type GetIdFunction = (detail: T) => string;
*/
export type GetTitleFunction = (detail?: T) => string | undefined;
-/**
- * Google GIS authentication configuration.
- */
-export interface GoogleGISAuthConfig {
- clientId: string;
- googleProfileEndpoint: string;
- scope: string;
-}
-
/**
* Grid track configuration.
*/
@@ -269,12 +267,15 @@ export interface ListViewConfig {
subTitleHero?: ComponentsConfig;
}
-/**
- * Interface to define the authentication login notice component.
- */
-export interface LoginNotice {
- conditionsUrl: string;
- privacyUrl: string;
+// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Use of `any` is intentional to allow for flexibility in the model.
+export interface OAuthProvider {
+ authorization: { params: { scope: string } };
+ clientId: string;
+ icon: ReactNode;
+ id: ProviderId;
+ name: string;
+ profile: (profile: P) => UserProfile;
+ userinfo: string;
}
/**
@@ -414,12 +415,6 @@ export interface TabConfig {
tabName?: string; // Used by the entity view to generate a title for the
component; when label is not typed string.
}
-export interface TerraAuthConfig {
- termsOfServiceEndpoint: string;
- terraNIHProfileEndpoint?: string;
- terraProfileEndpoint: string;
-}
-
/**
* Theme options function.
* Defines theme options, and provides a reference to the specified theme.
@@ -430,7 +425,7 @@ export type ThemeOptionsFn = (theme: Theme) => ThemeOptions;
* View context.
*/
export interface ViewContext {
- authState: Pick;
+ authState: Pick;
cellContext?: CellContext;
entityConfig: EntityConfig;
exploreState: ExploreState;
diff --git a/src/hooks/authentication/auth/useAuthReducer.ts b/src/hooks/authentication/auth/useAuthReducer.ts
new file mode 100644
index 00000000..8212b71f
--- /dev/null
+++ b/src/hooks/authentication/auth/useAuthReducer.ts
@@ -0,0 +1,16 @@
+import { useReducer } from "react";
+import { DEFAULT_AUTH_STATE } from "../../../providers/authentication/auth/constants";
+import { authReducer } from "../../../providers/authentication/auth/reducer";
+import { AuthContextProps } from "../../../providers/authentication/auth/types";
+import { initializer } from "../../../providers/authentication/common/utils";
+
+export const useAuthReducer = (
+ initialState = DEFAULT_AUTH_STATE
+): Omit => {
+ const [authState, authDispatch] = useReducer(
+ authReducer,
+ initialState,
+ initializer
+ );
+ return { authDispatch, authState };
+};
diff --git a/src/hooks/authentication/authentication/useAuthenticationReducer.ts b/src/hooks/authentication/authentication/useAuthenticationReducer.ts
new file mode 100644
index 00000000..5cd770b0
--- /dev/null
+++ b/src/hooks/authentication/authentication/useAuthenticationReducer.ts
@@ -0,0 +1,16 @@
+import { useReducer } from "react";
+import { DEFAULT_AUTHENTICATION_STATE } from "../../../providers/authentication/authentication/constants";
+import { authenticationReducer } from "../../../providers/authentication/authentication/reducer";
+import { AuthenticationContextProps } from "../../../providers/authentication/authentication/types";
+import { initializer } from "../../../providers/authentication/common/utils";
+
+export const useAuthenticationReducer = (
+ initialState = DEFAULT_AUTHENTICATION_STATE
+): AuthenticationContextProps => {
+ const [authenticationState, authenticationDispatch] = useReducer(
+ authenticationReducer,
+ initialState,
+ initializer
+ );
+ return { authenticationDispatch, authenticationState };
+};
diff --git a/src/hooks/authentication/config/useAuthenticationConfig.ts b/src/hooks/authentication/config/useAuthenticationConfig.ts
new file mode 100644
index 00000000..b2a16346
--- /dev/null
+++ b/src/hooks/authentication/config/useAuthenticationConfig.ts
@@ -0,0 +1,13 @@
+import { AuthenticationConfig } from "../../../config/entities";
+import { useConfig } from "../../useConfig";
+
+/**
+ * Returns the authentication configuration.
+ * @returns authentication configuration.
+ */
+export const useAuthenticationConfig = (): AuthenticationConfig | undefined => {
+ const {
+ config: { authentication },
+ } = useConfig();
+ return authentication;
+};
diff --git a/src/hooks/authentication/credentials/useCredentialsReducer.ts b/src/hooks/authentication/credentials/useCredentialsReducer.ts
new file mode 100644
index 00000000..4018f243
--- /dev/null
+++ b/src/hooks/authentication/credentials/useCredentialsReducer.ts
@@ -0,0 +1,13 @@
+import { useReducer } from "react";
+import { DEFAULT_CREDENTIALS_STATE } from "../../../providers/authentication/credentials/constants";
+import { credentialsReducer } from "../../../providers/authentication/credentials/reducer";
+import { CredentialsContextProps } from "../../../providers/authentication/credentials/types";
+
+export const useCredentialsReducer = (): CredentialsContextProps => {
+ const [credentialsState, credentialsDispatch] = useReducer(
+ credentialsReducer,
+ undefined,
+ () => DEFAULT_CREDENTIALS_STATE
+ );
+ return { credentialsDispatch, credentialsState };
+};
diff --git a/src/hooks/authentication/profile/types.ts b/src/hooks/authentication/profile/types.ts
new file mode 100644
index 00000000..5595432c
--- /dev/null
+++ b/src/hooks/authentication/profile/types.ts
@@ -0,0 +1,9 @@
+import {
+ Profile,
+ UserProfile,
+} from "../../../providers/authentication/authentication/types";
+
+export interface UseProfile {
+ isLoading: boolean;
+ profile: Profile;
+}
diff --git a/src/hooks/authentication/profile/useProfile.ts b/src/hooks/authentication/profile/useProfile.ts
new file mode 100644
index 00000000..e28f7d76
--- /dev/null
+++ b/src/hooks/authentication/profile/useProfile.ts
@@ -0,0 +1,21 @@
+import { useAuth } from "../../../providers/authentication/auth/hook";
+import { AUTH_STATUS } from "../../../providers/authentication/auth/types";
+import { useAuthentication } from "../../../providers/authentication/authentication/hook";
+import { UseProfile } from "./types";
+
+/**
+ * Profile hook - returns user profile.
+ * @returns user profile.
+ */
+export const useProfile = (): UseProfile => {
+ const {
+ authState: { status },
+ } = useAuth();
+ const {
+ authenticationState: { profile },
+ } = useAuthentication();
+ return {
+ isLoading: status === AUTH_STATUS.PENDING,
+ profile: profile,
+ };
+};
diff --git a/src/hooks/authentication/providers/types.ts b/src/hooks/authentication/providers/types.ts
new file mode 100644
index 00000000..e86cf2cb
--- /dev/null
+++ b/src/hooks/authentication/providers/types.ts
@@ -0,0 +1,7 @@
+import { OAuthProvider } from "../../../config/entities";
+import { ProviderId } from "../../../providers/authentication/common/types";
+
+export interface UseProviders {
+ findProvider: (providerId: ProviderId) => OAuthProvider | undefined;
+ providers: OAuthProvider[] | undefined;
+}
diff --git a/src/hooks/authentication/providers/useProviders.ts b/src/hooks/authentication/providers/useProviders.ts
new file mode 100644
index 00000000..44e82930
--- /dev/null
+++ b/src/hooks/authentication/providers/useProviders.ts
@@ -0,0 +1,23 @@
+import { useCallback } from "react";
+import { ProviderId } from "../../../providers/authentication/common/types";
+import { useConfig } from "../../useConfig";
+import { UseProviders } from "./types";
+
+/**
+ * Hook to facilitate the retrieval of authentication providers.
+ * @returns authentication providers and a function to find a provider by id.
+ */
+
+export const useProviders = (): UseProviders => {
+ const { config } = useConfig();
+ const providers = config.authentication?.providers;
+
+ const findProvider = useCallback(
+ (providerId: ProviderId) => {
+ return providers?.find(({ id }) => id === providerId);
+ },
+ [providers]
+ );
+
+ return { findProvider, providers };
+};
diff --git a/src/hooks/authentication/session/useSessionActive.ts b/src/hooks/authentication/session/useSessionActive.ts
new file mode 100644
index 00000000..cb14c6c4
--- /dev/null
+++ b/src/hooks/authentication/session/useSessionActive.ts
@@ -0,0 +1,39 @@
+import Router from "next/router";
+import { useEffect } from "react";
+import { escapeRegExp } from "../../../common/utils";
+import {
+ AUTH_STATUS,
+ AuthState,
+} from "../../../providers/authentication/auth/types";
+import { ROUTE } from "../../../routes/constants";
+import { useRouteHistory } from "../../useRouteHistory";
+import { INACTIVITY_PARAM } from "./useSessionTimeout";
+
+export const useSessionActive = (authState: AuthState): void => {
+ const { status } = authState;
+ const { callbackUrl } = useRouteHistory(2);
+ useEffect(() => {
+ if (status !== AUTH_STATUS.SETTLED) return;
+ Router.push(callbackUrl(transformRoute));
+ }, [callbackUrl, status]);
+};
+
+/**
+ * Finds the most recent route in the history that is not the login route and removes the inactivity timeout query parameter.
+ * The inactivity timeout query parameter is appended to a URL to indicate that a session has expired. This function iterates
+ * through the history of routes in reverse order, skipping any routes that lead to the login page, and returns the first route
+ * that isn't the login route with the inactivity timeout parameter removed.
+ * @param routes - An array of routes representing the navigation history, in order of navigation.
+ * @returns The most recent valid route without the inactivity timeout parameter, or `undefined` if no such route is found.
+ */
+export function transformRoute(routes: string[]): string | undefined {
+ for (const route of routes) {
+ if (route === ROUTE.LOGIN) {
+ continue;
+ }
+ return route?.replace(
+ new RegExp(`\\?${escapeRegExp(INACTIVITY_PARAM)}(?:$|[=&].*)`),
+ ""
+ );
+ }
+}
diff --git a/src/hooks/authentication/session/useSessionAuth.ts b/src/hooks/authentication/session/useSessionAuth.ts
new file mode 100644
index 00000000..6e16a91d
--- /dev/null
+++ b/src/hooks/authentication/session/useSessionAuth.ts
@@ -0,0 +1,61 @@
+import { useEffect } from "react";
+import { updateAuthState } from "../../../providers/authentication/auth/dispatch";
+import {
+ AUTH_STATUS,
+ AuthContextProps,
+ UpdateAuthStatePayload,
+} from "../../../providers/authentication/auth/types";
+import {
+ AUTHENTICATION_STATUS,
+ AuthenticationContextProps,
+} from "../../../providers/authentication/authentication/types";
+
+export const useSessionAuth = ({
+ authenticationReducer,
+ authReducer,
+}: {
+ authenticationReducer: AuthenticationContextProps;
+ authReducer: Omit;
+}): void => {
+ const { authDispatch } = authReducer;
+ const {
+ authenticationState: { profile, status },
+ } = authenticationReducer;
+ const isAuthenticated = !!profile;
+
+ useEffect(() => {
+ authDispatch?.(
+ updateAuthState(getSession(isAuthenticated, getSessionStatus(status)))
+ );
+ }, [authDispatch, isAuthenticated, status]);
+};
+
+/**
+ * Returns the auth session based on the authentication state.
+ * @param isAuthenticated - Authentication status.
+ * @param status - Auth status.
+ * @returns auth state payload.
+ */
+function getSession(
+ isAuthenticated: boolean,
+ status: AUTH_STATUS
+): UpdateAuthStatePayload {
+ switch (status) {
+ case AUTH_STATUS.PENDING:
+ return { isAuthenticated: false, status };
+ case AUTH_STATUS.SETTLED:
+ return { isAuthenticated, status };
+ default:
+ return { isAuthenticated: false, status };
+ }
+}
+
+/**
+ * Returns the session status based on the authentication status.
+ * @param status - Authentication status.
+ * @returns session status.
+ */
+function getSessionStatus(status: AUTHENTICATION_STATUS): AUTH_STATUS {
+ if (status === AUTHENTICATION_STATUS.PENDING) return AUTH_STATUS.PENDING;
+ return AUTH_STATUS.SETTLED;
+}
diff --git a/src/hooks/authentication/session/useSessionCallbackUrl.ts b/src/hooks/authentication/session/useSessionCallbackUrl.ts
new file mode 100644
index 00000000..91f686ec
--- /dev/null
+++ b/src/hooks/authentication/session/useSessionCallbackUrl.ts
@@ -0,0 +1,25 @@
+import { useMemo } from "react";
+import { useRouteRoot } from "../../useRouteRoot";
+import { INACTIVITY_PARAM } from "./useSessionTimeout";
+
+export interface UseSessionCallbackUrl {
+ callbackUrl: string | undefined;
+}
+
+export const useSessionCallbackUrl = (): UseSessionCallbackUrl => {
+ const route = useRouteRoot();
+ const callbackUrl = useMemo(() => getUrl(route), [route]);
+ return { callbackUrl };
+};
+
+/**
+ * Returns the URL with the inactivity query parameter set to true.
+ * @param url - URL.
+ * @returns URL.
+ */
+function getUrl(url: string): string | undefined {
+ if (typeof window === "undefined") return;
+ const urlObj = new URL(url, window.location.origin);
+ urlObj.searchParams.set(INACTIVITY_PARAM, "true");
+ return urlObj.href;
+}
diff --git a/src/hooks/authentication/session/useSessionIdleTimer.ts b/src/hooks/authentication/session/useSessionIdleTimer.ts
new file mode 100644
index 00000000..6d566175
--- /dev/null
+++ b/src/hooks/authentication/session/useSessionIdleTimer.ts
@@ -0,0 +1,10 @@
+import { useIdleTimer } from "react-idle-timer";
+import { IIdleTimerProps } from "react-idle-timer/dist/types/IIdleTimerProps";
+
+/**
+ * Sets a session timeout that triggers when the user has been idle for the specified duration.
+ * @param idleTimerProps - The parameters for the session timeout.
+ */
+export const useSessionIdleTimer = (idleTimerProps: IIdleTimerProps): void => {
+ useIdleTimer(idleTimerProps);
+};
diff --git a/src/hooks/useSessionTimeout.ts b/src/hooks/authentication/session/useSessionTimeout.ts
similarity index 91%
rename from src/hooks/useSessionTimeout.ts
rename to src/hooks/authentication/session/useSessionTimeout.ts
index b87d97a3..db1f75f3 100644
--- a/src/hooks/useSessionTimeout.ts
+++ b/src/hooks/authentication/session/useSessionTimeout.ts
@@ -1,7 +1,7 @@
import Router from "next/router";
import { useCallback, useEffect, useState } from "react";
-import { useConfig } from "./useConfig";
-import { useLocation } from "./useLocation";
+import { useConfig } from "../../useConfig";
+import { useLocation } from "../../useLocation";
export const INACTIVITY_PARAM = "inactivityTimeout";
diff --git a/src/hooks/useAuthentication/useAuthenticationForm.ts b/src/hooks/authentication/terra/useAuthenticationForm.ts
similarity index 84%
rename from src/hooks/useAuthentication/useAuthenticationForm.ts
rename to src/hooks/authentication/terra/useAuthenticationForm.ts
index 5b267691..0f1677c0 100644
--- a/src/hooks/useAuthentication/useAuthenticationForm.ts
+++ b/src/hooks/authentication/terra/useAuthenticationForm.ts
@@ -1,10 +1,9 @@
-import { AuthContextProps } from "../../providers/authentication";
+import { useTerraProfile } from "../../../providers/authentication/terra/hook";
import {
- AUTHENTICATION_STATUS,
LoginResponse,
LoginStatus,
-} from "./common/entities";
-import { useAuthentication } from "./useAuthentication";
+} from "../../../providers/authentication/terra/hooks/common/entities";
+import { TerraProfileContextProps } from "../../../providers/authentication/terra/types";
export interface OnboardingStatus {
active: boolean;
@@ -19,7 +18,6 @@ export enum ONBOARDING_STEP {
interface UseAuthenticationForm {
isComplete: boolean;
- isReady: boolean;
onboardingStatusByStep: Map;
}
@@ -28,16 +26,13 @@ interface UseAuthenticationForm {
* @returns onboarding steps and corresponding status.
*/
export const useAuthenticationForm = (): UseAuthenticationForm => {
- const authentication = useAuthentication();
- const isReady =
- authentication.authenticationStatus === AUTHENTICATION_STATUS.COMPLETED;
+ const terraProfile = useTerraProfile();
const loginStatuses =
- concatLoginStatuses(authentication).filter(filterLoginStatus);
+ concatLoginStatuses(terraProfile).filter(filterLoginStatus);
const onboardingStatusByStep = getOnboardingStatusByStep(loginStatuses);
const isComplete = isAuthenticationComplete(onboardingStatusByStep);
return {
isComplete,
- isReady,
onboardingStatusByStep,
};
};
@@ -60,17 +55,17 @@ function isAuthenticationComplete(
/**
* Returns all login statuses, ordered by onboarding step.
- * @param authentication - Authentication.
+ * @param terraProfile - Terra profile.
* @returns login statuses.
*/
function concatLoginStatuses(
- authentication: AuthContextProps
+ terraProfile: TerraProfileContextProps
): LoginStatus[] {
const {
terraNIHProfileLoginStatus,
terraProfileLoginStatus,
terraTOSLoginStatus,
- } = authentication;
+ } = terraProfile;
return [
terraProfileLoginStatus,
terraTOSLoginStatus,
diff --git a/src/hooks/useAuthentication/useAuthenticationNIHExpiry.ts b/src/hooks/authentication/terra/useAuthenticationNIHExpiry.ts
similarity index 88%
rename from src/hooks/useAuthentication/useAuthenticationNIHExpiry.ts
rename to src/hooks/authentication/terra/useAuthenticationNIHExpiry.ts
index bc15323b..6685d902 100644
--- a/src/hooks/useAuthentication/useAuthenticationNIHExpiry.ts
+++ b/src/hooks/authentication/terra/useAuthenticationNIHExpiry.ts
@@ -1,5 +1,5 @@
-import { REQUEST_STATUS } from "./common/entities";
-import { useAuthentication } from "./useAuthentication";
+import { useTerraProfile } from "../../../providers/authentication/terra/hook";
+import { REQUEST_STATUS } from "../../../providers/authentication/terra/hooks/common/entities";
const WARNING_WINDOW_SECONDS = 60 * 60 * 24 * 5; // 5 days.
@@ -15,8 +15,7 @@ interface UseAuthenticationNIHExpiry {
* @returns NIH expiry status.
*/
export const useAuthenticationNIHExpiry = (): UseAuthenticationNIHExpiry => {
- const authentication = useAuthentication();
- const { terraNIHProfileLoginStatus } = authentication;
+ const { terraNIHProfileLoginStatus } = useTerraProfile();
const { requestStatus, response } = terraNIHProfileLoginStatus;
const { linkExpireTime } = response || {};
const isReady = requestStatus === REQUEST_STATUS.COMPLETED;
diff --git a/src/hooks/authentication/token/types.ts b/src/hooks/authentication/token/types.ts
new file mode 100644
index 00000000..f0ce5d7d
--- /dev/null
+++ b/src/hooks/authentication/token/types.ts
@@ -0,0 +1,5 @@
+import { Credentials } from "../../../providers/authentication/credentials/types";
+
+export interface UseToken {
+ token: Credentials;
+}
diff --git a/src/hooks/authentication/token/useToken.ts b/src/hooks/authentication/token/useToken.ts
new file mode 100644
index 00000000..143bbbe7
--- /dev/null
+++ b/src/hooks/authentication/token/useToken.ts
@@ -0,0 +1,9 @@
+import { useCredentials } from "../../../providers/authentication/credentials/hook";
+import { UseToken } from "./types";
+
+export const useToken = (): UseToken => {
+ const {
+ credentialsState: { credentials: token },
+ } = useCredentials();
+ return { token };
+};
diff --git a/src/hooks/authentication/token/useTokenReducer.ts b/src/hooks/authentication/token/useTokenReducer.ts
new file mode 100644
index 00000000..3015d84f
--- /dev/null
+++ b/src/hooks/authentication/token/useTokenReducer.ts
@@ -0,0 +1,19 @@
+import { useReducer } from "react";
+import { DEFAULT_TOKEN_STATE } from "../../../providers/authentication/token/constants";
+import { tokenReducer } from "../../../providers/authentication/token/reducer";
+import { TokenContextProps } from "../../../providers/authentication/token/types";
+
+/**
+ * Token reducer: Manages the internal state of the token within the OAuth provider.
+ * This reducer handles the token locally until certain conditions are met.
+ * For releasing the token to the rest of the app, use the credentials reducer.
+ */
+
+export const useTokenReducer = (): TokenContextProps => {
+ const [tokenState, tokenDispatch] = useReducer(
+ tokenReducer,
+ undefined,
+ () => DEFAULT_TOKEN_STATE
+ );
+ return { tokenDispatch, tokenState };
+};
diff --git a/src/hooks/useAuthentication/useAuthentication.tsx b/src/hooks/useAuthentication/useAuthentication.tsx
deleted file mode 100644
index bf6ce354..00000000
--- a/src/hooks/useAuthentication/useAuthentication.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import { useContext } from "react";
-import { AuthContext, AuthContextProps } from "../../providers/authentication";
-
-/**
- * Returns authentication context.
- * @returns authentication context.
- */
-export const useAuthentication = (): AuthContextProps => {
- return useContext(AuthContext);
-};
diff --git a/src/hooks/useAuthentication/useAuthenticationComplete.ts b/src/hooks/useAuthentication/useAuthenticationComplete.ts
deleted file mode 100644
index 2d5ce57a..00000000
--- a/src/hooks/useAuthentication/useAuthenticationComplete.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import Router, { useRouter } from "next/router";
-import { useEffect, useMemo, useRef } from "react";
-import { escapeRegExp } from "../../common/utils";
-import { ROUTE_LOGIN } from "../../providers/authentication";
-import { INACTIVITY_PARAM } from "../useSessionTimeout";
-import { AUTHENTICATION_STATUS } from "./common/entities";
-
-/**
- * Handles the completion of the authentication process.
- * @param authenticationStatus - Authentication status.
- */
-export const useAuthenticationComplete = (
- authenticationStatus: AUTHENTICATION_STATUS
-): void => {
- const { asPath } = useRouter();
- const routeHistoryRef = useRef(initRouteHistory(asPath));
-
- // Maintain a history of routes that have been visited prior to authentication.
- routeHistoryRef.current = useMemo(
- () => updateRouteHistory(routeHistoryRef.current, asPath),
- [asPath]
- );
-
- // Redirect to the previous route after authentication is completed.
- useEffect(() => {
- if (authenticationStatus === AUTHENTICATION_STATUS.COMPLETED) {
- Router.push(routeHistoryRef.current);
- }
- }, [authenticationStatus]);
-};
-
-/**
- * Initializes route history with the current path.
- * Returns base path if current path is the login route.
- * @param path - current browser path.
- * @returns path to be used as the initial route history.
- */
-function initRouteHistory(path: string): string {
- return path === ROUTE_LOGIN ? "/" : removeInactivityTimeoutQueryParam(path);
-}
-
-/**
- * Removes the inactivity timeout query parameter from the path.
- * the inactivity timeout parameter is used to indicate that the session has timed out; remove the parameter to
- * clear the session timeout banner after the user logs in again.
- * @param path - Path.
- * @returns path without the inactivity timeout query parameter.
- */
-function removeInactivityTimeoutQueryParam(path: string): string {
- const regex = new RegExp(`\\?${escapeRegExp(INACTIVITY_PARAM)}(?:$|[=&].*)`);
- return path.replace(regex, "");
-}
-
-/**
- * Updates route history with the current path, unless the current path is the LoginView page.
- * @param prevPath - route history path.
- * @param path - current browser path.
- * @returns updated path to be used as the route history.
- */
-function updateRouteHistory(prevPath: string, path: string): string {
- let currentPath = prevPath;
- if (path !== ROUTE_LOGIN) {
- currentPath = path;
- }
- return removeInactivityTimeoutQueryParam(currentPath);
-}
diff --git a/src/hooks/useAuthentication/useAuthenticationStatus.ts b/src/hooks/useAuthentication/useAuthenticationStatus.ts
deleted file mode 100644
index cfb07ffc..00000000
--- a/src/hooks/useAuthentication/useAuthenticationStatus.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- AUTHENTICATION_STATUS,
- LoginResponse,
- LoginStatus,
- REQUEST_STATUS,
-} from "./common/entities";
-import { GoogleResponse } from "./useFetchGoogleProfile";
-import { TerraNIHResponse } from "./useFetchTerraNIHProfile";
-import { TerraResponse } from "./useFetchTerraProfile";
-import { TerraTermsOfServiceResponse } from "./useFetchTerraTermsOfService";
-
-/**
- * Handles the status of the authentication process.
- * @param userProfileLoginStatus - User profile login status.
- * @param terraProfileLoginStatus - Terra profile login status.
- * @param terraTOSLoginStatus - Terra terms of service login status.
- * @param terraNIHProfileLoginStatus - Terra NIH profile login status.
- * @returns authentication status.
- */
-export const useAuthenticationStatus = (
- userProfileLoginStatus: LoginStatus,
- terraProfileLoginStatus: LoginStatus,
- terraTOSLoginStatus: LoginStatus,
- terraNIHProfileLoginStatus: LoginStatus
-): AUTHENTICATION_STATUS => {
- return getAuthenticationStatus([
- terraNIHProfileLoginStatus,
- terraProfileLoginStatus,
- terraTOSLoginStatus,
- userProfileLoginStatus,
- ]);
-};
-
-/**
- * Returns the authentication status ("INCOMPLETE" or "COMPLETE").
- * @param loginStatuses - Login statuses.
- * @returns authentication status.
- */
-export function getAuthenticationStatus(
- loginStatuses: LoginStatus[]
-): AUTHENTICATION_STATUS {
- for (const loginStatus of loginStatuses) {
- if (!loginStatus.isSupported) continue;
- if (loginStatus.requestStatus === REQUEST_STATUS.NOT_STARTED) {
- return AUTHENTICATION_STATUS.INCOMPLETE;
- }
- }
- return AUTHENTICATION_STATUS.COMPLETED;
-}
diff --git a/src/hooks/useAuthentication/useFetchGoogleProfile.ts b/src/hooks/useAuthentication/useFetchGoogleProfile.ts
deleted file mode 100644
index bda5beec..00000000
--- a/src/hooks/useAuthentication/useFetchGoogleProfile.ts
+++ /dev/null
@@ -1,68 +0,0 @@
-import { useCallback, useEffect, useState } from "react";
-import { useAuthenticationConfig } from "../useAuthenticationConfig";
-import {
- LOGIN_STATUS_FAILED,
- LOGIN_STATUS_NOT_STARTED,
-} from "./common/constants";
-import { LoginStatus, REQUEST_STATUS } from "./common/entities";
-import { getAuthenticationRequestOptions } from "./common/utils";
-
-export type UserProfile = GoogleResponse;
-
-export interface GoogleResponse {
- email: string;
- email_verified: boolean;
- family_name: string;
- given_name: string;
- hd: string;
- locale: string;
- name: string;
- picture: string;
- sub: string;
-}
-
-type Status = LoginStatus;
-
-/**
- * Returns user profile login status from configured endpoint.
- * @param token - Token.
- * @returns google profile login status.
- */
-export const useFetchGoogleProfile = (token?: string): Status => {
- const authenticationConfig = useAuthenticationConfig();
- const { googleGISAuthConfig: { googleProfileEndpoint: endpoint } = {} } =
- authenticationConfig;
- const [loginStatus, setLoginStatus] = useState(
- LOGIN_STATUS_NOT_STARTED as Status
- );
-
- // Fetch google user profile.
- const fetchEndpointData = useCallback(
- (endpoint: string, accessToken: string): void => {
- fetch(endpoint, getAuthenticationRequestOptions(accessToken))
- .then((response) => response.json())
- .then((profile: GoogleResponse) => {
- setLoginStatus((prevStatus) => ({
- ...prevStatus,
- isSuccess: true,
- requestStatus: REQUEST_STATUS.COMPLETED,
- response: profile,
- }));
- })
- .catch((err) => {
- console.log(err); // TODO handle error.
- setLoginStatus(LOGIN_STATUS_FAILED as Status);
- });
- },
- []
- );
-
- // Fetches user profile.
- useEffect(() => {
- if (!token) return;
- if (!endpoint) return;
- fetchEndpointData(endpoint, token);
- }, [endpoint, fetchEndpointData, token]);
-
- return loginStatus;
-};
diff --git a/src/hooks/useAuthentication/useTokenClient.ts b/src/hooks/useAuthentication/useTokenClient.ts
deleted file mode 100644
index 304d37dd..00000000
--- a/src/hooks/useAuthentication/useTokenClient.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-import { useEffect, useState } from "react";
-import { useAuthenticationConfig } from "../useAuthenticationConfig";
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO see https://github.com/clevercanary/data-browser/issues/544.
-declare const google: any;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO see https://github.com/clevercanary/data-browser/issues/544.
-type TokenClient = any;
-
-interface TokenResponse {
- access_token: string;
- expires_in: number;
- refresh_token?: string;
- scope?: string;
- token_type: string;
-}
-
-export interface UseTokenClient {
- token: string | undefined;
- tokenClient: TokenClient | undefined;
-}
-
-/**
- * Initializes the token client, sets token from token client callback.
- * @returns initialized token client.
- */
-export const useTokenClient = (): UseTokenClient => {
- const [token, setToken] = useState();
- const [tokenClient, setTokenClient] = useState();
- const authenticationConfig = useAuthenticationConfig();
- const { googleGISAuthConfig: { clientId, scope } = {} } =
- authenticationConfig;
-
- // Initializes token client - (authorization client id must be configured).
- useEffect(() => {
- if (clientId) {
- setTokenClient(
- google.accounts.oauth2.initTokenClient({
- callback: (tokenResponse: TokenResponse) => {
- const access_token = tokenResponse.access_token;
- setToken(access_token);
- },
- client_id: clientId,
- scope,
- })
- );
- }
- }, [clientId, scope]);
-
- return {
- token,
- tokenClient,
- };
-};
diff --git a/src/hooks/useAuthenticationConfig.ts b/src/hooks/useAuthenticationConfig.ts
deleted file mode 100644
index c80573e8..00000000
--- a/src/hooks/useAuthenticationConfig.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { AuthenticationConfig } from "../config/entities";
-import { useConfig } from "./useConfig";
-
-/**
- * Hook to get the authentication config
- * @returns @see AuthenticationConfig used in the current config.
- */
-export const useAuthenticationConfig = (): AuthenticationConfig => {
- const { config } = useConfig();
-
- if (!config.authentication) {
- return {
- title: "",
- };
- }
-
- return config.authentication;
-};
diff --git a/src/hooks/useEntityList.ts b/src/hooks/useEntityList.ts
index eb7c955a..0516293b 100644
--- a/src/hooks/useEntityList.ts
+++ b/src/hooks/useEntityList.ts
@@ -13,8 +13,8 @@ import { EntityMapper } from "../config/entities";
import { getEntityConfig } from "../config/utils";
import { ExploreActionKind } from "../providers/exploreState";
import { DEFAULT_PAGINATION_STATE } from "../providers/exploreState/initializer/constants";
+import { useToken } from "./authentication/token/useToken";
import { useAsync } from "./useAsync";
-import { useAuthentication } from "./useAuthentication/useAuthentication";
import { useConfig } from "./useConfig";
import { useEntityService } from "./useEntityService";
import { EXPLORE_MODE, ExploreMode, useExploreMode } from "./useExploreMode";
@@ -31,7 +31,7 @@ export const useEntityList = (
staticResponse: AzulEntitiesStaticResponse
): void => {
const { data: staticData, entityListType } = staticResponse;
- const { token } = useAuthentication();
+ const { token } = useToken();
const { config } = useConfig();
const { apiPath } = getEntityConfig(config.entities, entityListType);
const exploreMode = useExploreMode();
diff --git a/src/hooks/useFetchEntity.tsx b/src/hooks/useFetchEntity.tsx
index 3b93d552..74311778 100644
--- a/src/hooks/useFetchEntity.tsx
+++ b/src/hooks/useFetchEntity.tsx
@@ -2,8 +2,8 @@ import { useRouter } from "next/router";
import { useEffect, useMemo } from "react";
import { PARAMS_INDEX_UUID } from "../common/constants";
import { EntityDetailViewProps } from "../views/EntityDetailView/entityDetailView";
+import { useToken } from "./authentication/token/useToken";
import { useAsync } from "./useAsync";
-import { useAuthentication } from "./useAuthentication/useAuthentication";
import { useEntityService } from "./useEntityService";
import { EXPLORE_MODE, useExploreMode } from "./useExploreMode";
import { useExploreState } from "./useExploreState";
@@ -23,7 +23,7 @@ export const useFetchEntity = (
detailViewProps?: EntityDetailViewProps
): UseEntityDetailResponse => {
const { data: staticData, entityListType } = detailViewProps || {};
- const { token } = useAuthentication();
+ const { token } = useToken();
const exploreMode = useExploreMode();
const { exploreState } = useExploreState();
const { catalogState } = exploreState;
diff --git a/src/hooks/useFileManifest/useFetchFilesFacets.ts b/src/hooks/useFileManifest/useFetchFilesFacets.ts
index ba9dbcae..7d91c00a 100644
--- a/src/hooks/useFileManifest/useFetchFilesFacets.ts
+++ b/src/hooks/useFileManifest/useFetchFilesFacets.ts
@@ -6,8 +6,8 @@ import {
import { Filters } from "../../common/entities";
import { fetchEntitiesFromURL } from "../../entity/common/service";
import { fetchQueryParams, SearchParams } from "../../utils/fetchQueryParams";
+import { useToken } from "../authentication/token/useToken";
import { useAsync } from "../useAsync";
-import { useAuthentication } from "../useAuthentication/useAuthentication";
import { useFetchRequestURL } from "../useFetchRequestURL";
import { FetchFilesFacets } from "./common/entities";
import { bindEntitySearchResultsResponse } from "./common/utils";
@@ -26,8 +26,7 @@ export const useFetchFilesFacets = (
searchParams: SearchParams | undefined,
isEnabled: boolean
): FetchFilesFacets => {
- // Grab token from authentication.
- const { token } = useAuthentication();
+ const { token } = useToken();
// Build request params.
const requestParams = fetchQueryParams(filters, catalog, searchParams);
// Build request URL.
diff --git a/src/hooks/useFileManifest/useFetchSummary.ts b/src/hooks/useFileManifest/useFetchSummary.ts
index 14c388d2..7b5e850a 100644
--- a/src/hooks/useFileManifest/useFetchSummary.ts
+++ b/src/hooks/useFileManifest/useFetchSummary.ts
@@ -6,8 +6,8 @@ import {
import { Filters } from "../../common/entities";
import { fetchSummaryFromURL } from "../../entity/api/service";
import { fetchQueryParams } from "../../utils/fetchQueryParams";
+import { useToken } from "../authentication/token/useToken";
import { useAsync } from "../useAsync";
-import { useAuthentication } from "../useAuthentication/useAuthentication";
import { useFetchRequestURL } from "../useFetchRequestURL";
import { FetchFileSummary } from "./common/entities";
@@ -23,8 +23,7 @@ export const useFetchSummary = (
catalog: string,
isEnabled: boolean
): FetchFileSummary => {
- // Grab token from authentication.
- const { token } = useAuthentication();
+ const { token } = useToken();
// Build request params.
const requestParams = fetchQueryParams(filters, catalog, undefined);
// Build request URL.
diff --git a/src/hooks/useRequestFileLocation.ts b/src/hooks/useRequestFileLocation.ts
index cd8424cd..cbf0928b 100644
--- a/src/hooks/useRequestFileLocation.ts
+++ b/src/hooks/useRequestFileLocation.ts
@@ -4,8 +4,8 @@ import {
FILE_LOCATION_SUCCESSFULLY,
} from "../apis/azul/common/constants";
import { FileLocationResponse } from "../apis/azul/common/entities";
+import { useToken } from "./authentication/token/useToken";
import { useAsync } from "./useAsync";
-import { useAuthentication } from "./useAuthentication/useAuthentication";
export interface FileLocation {
commandLine?: { [key: string]: string };
@@ -127,8 +127,7 @@ export const useRequestFileLocation = (
url?: string,
method?: Method
): UseRequestFileLocationResult => {
- // Grab token from authentication.
- const { token } = useAuthentication();
+ const { token } = useToken();
const {
data,
isIdle,
diff --git a/src/hooks/useRouteHistory.ts b/src/hooks/useRouteHistory.ts
new file mode 100644
index 00000000..9500ac90
--- /dev/null
+++ b/src/hooks/useRouteHistory.ts
@@ -0,0 +1,65 @@
+import Router, { useRouter } from "next/router";
+import { useCallback, useEffect, useRef } from "react";
+import { useRouteRoot } from "./useRouteRoot";
+
+const ROUTE_CHANGE_EVENT = "routeChangeComplete";
+const MAX_HISTORY_LENGTH = 4;
+
+export type TransformRouteFn = (routes: string[]) => string | undefined;
+
+export interface UseRouteHistory {
+ callbackUrl: (transformFn?: TransformRouteFn) => string;
+}
+
+export function useRouteHistory(
+ maxHistory = MAX_HISTORY_LENGTH
+): UseRouteHistory {
+ const { asPath } = useRouter();
+ const rootPath = useRouteRoot();
+ const historyRef = useRef([asPath]);
+
+ const onRouteChange = useCallback(
+ (route: string): void => {
+ if (route === historyRef.current[0]) return;
+ historyRef.current.unshift(route);
+ if (historyRef.current.length > maxHistory) {
+ historyRef.current.pop();
+ }
+ },
+ [maxHistory]
+ );
+
+ useEffect(() => {
+ Router.events.on(ROUTE_CHANGE_EVENT, onRouteChange);
+ return (): void => {
+ Router.events.off(ROUTE_CHANGE_EVENT, onRouteChange);
+ };
+ }, [onRouteChange]);
+
+ const callbackUrl = useCallback(
+ (transformFn?: TransformRouteFn): string =>
+ getCallbackUrl(historyRef.current, rootPath, transformFn),
+ [rootPath]
+ );
+
+ return { callbackUrl };
+}
+
+/**
+ * Generates a callback URL based on the provided history and root path.
+ * Returns the callback URL determined by the transform function or the second item in history and if neither condition is met, returns the root path.
+ * @param history - Navigation history.
+ * @param rootPath - The default root path to return if no other conditions are met.
+ * @param [transformFn] - An optional function that transforms the history array to a specific route.
+ * @returns {string} - The callback UR.
+ */
+export function getCallbackUrl(
+ history: string[],
+ rootPath: string,
+ transformFn?: TransformRouteFn
+): string {
+ if (transformFn) {
+ return transformFn(history) || rootPath;
+ }
+ return history[1] || rootPath;
+}
diff --git a/src/hooks/useRouteRoot.ts b/src/hooks/useRouteRoot.ts
new file mode 100644
index 00000000..5cae4a66
--- /dev/null
+++ b/src/hooks/useRouteRoot.ts
@@ -0,0 +1,11 @@
+import { useRouter } from "next/router";
+import { useMemo } from "react";
+import { useConfig } from "./useConfig";
+
+export function useRouteRoot(): string {
+ const {
+ config: { redirectRootToPath: path },
+ } = useConfig();
+ const { basePath } = useRouter();
+ return useMemo(() => `${basePath}${path}`, [basePath, path]);
+}
diff --git a/src/hooks/useSummary.ts b/src/hooks/useSummary.ts
index 5c83ae32..a3f25c63 100644
--- a/src/hooks/useSummary.ts
+++ b/src/hooks/useSummary.ts
@@ -1,7 +1,7 @@
import { useEffect } from "react";
import { AzulSummaryResponse } from "../apis/azul/common/entities";
+import { useToken } from "./authentication/token/useToken";
import { useAsync } from "./useAsync";
-import { useAuthentication } from "./useAuthentication/useAuthentication";
import { useConfig } from "./useConfig";
import { useEntityService } from "./useEntityService";
import { useExploreState } from "./useExploreState";
@@ -16,7 +16,7 @@ interface UseSummaryResponse {
* @returns an object with the loaded data and a flag indicating is the data is loading
*/
export const useSummary = (): UseSummaryResponse => {
- const { token } = useAuthentication();
+ const { token } = useToken();
const { config } = useConfig();
const { exploreState } = useExploreState();
const { filterState } = exploreState;
@@ -39,7 +39,7 @@ export const useSummary = (): UseSummaryResponse => {
return { isLoading: false }; //TODO: return a summary placeholder
}
- // Return the fetch status and summary data once fetch is complete..
+ // Return the fetch status and summary data once fetch is complete.
return {
isLoading: apiIsLoading,
response,
diff --git a/src/providers/authentication.tsx b/src/providers/authentication.tsx
deleted file mode 100644
index 36680c04..00000000
--- a/src/providers/authentication.tsx
+++ /dev/null
@@ -1,181 +0,0 @@
-import Router, { useRouter } from "next/router";
-import React, { createContext, ReactNode, useCallback } from "react";
-import { useIdleTimer } from "react-idle-timer";
-import { LOGIN_STATUS_NOT_STARTED } from "../hooks/useAuthentication/common/constants";
-import {
- AUTHENTICATION_STATUS,
- LoginStatus,
-} from "../hooks/useAuthentication/common/entities";
-import { useAuthenticationComplete } from "../hooks/useAuthentication/useAuthenticationComplete";
-import { useAuthenticationStatus } from "../hooks/useAuthentication/useAuthenticationStatus";
-import {
- useFetchGoogleProfile,
- UserProfile,
-} from "../hooks/useAuthentication/useFetchGoogleProfile";
-import {
- TerraNIHResponse,
- useFetchTerraNIHProfile,
-} from "../hooks/useAuthentication/useFetchTerraNIHProfile";
-import {
- TerraResponse,
- useFetchTerraProfile,
-} from "../hooks/useAuthentication/useFetchTerraProfile";
-import {
- TerraTermsOfServiceResponse,
- useFetchTerraTermsOfService,
-} from "../hooks/useAuthentication/useFetchTerraTermsOfService";
-import { useTokenClient } from "../hooks/useAuthentication/useTokenClient";
-import { useConfig } from "../hooks/useConfig";
-import { INACTIVITY_PARAM } from "../hooks/useSessionTimeout";
-
-// Template constants
-export const ROUTE_LOGIN = "/login";
-
-type AuthenticateUserFn = () => void;
-type RequestAuthenticationFn = () => void;
-
-/**
- * Model of authentication context.
- */
-export interface AuthContextProps {
- authenticateUser: AuthenticateUserFn;
- authenticationStatus: AUTHENTICATION_STATUS;
- isAuthenticated: boolean;
- isEnabled: boolean;
- requestAuthentication: RequestAuthenticationFn;
- terraNIHProfileLoginStatus: LoginStatus;
- terraProfileLoginStatus: LoginStatus;
- terraTOSLoginStatus: LoginStatus;
- token?: string;
- userProfile?: UserProfile;
-}
-
-/**
- * Auth context for storing and using auth-related state.
- */
-export const AuthContext = createContext({
- // eslint-disable-next-line @typescript-eslint/no-empty-function -- allow dummy function for default state.
- authenticateUser: () => {},
- authenticationStatus: AUTHENTICATION_STATUS.INCOMPLETE,
- isAuthenticated: false,
- isEnabled: false,
- // eslint-disable-next-line @typescript-eslint/no-empty-function -- allow dummy function for default state.
- requestAuthentication: () => {},
- terraNIHProfileLoginStatus:
- LOGIN_STATUS_NOT_STARTED as LoginStatus,
- terraProfileLoginStatus:
- LOGIN_STATUS_NOT_STARTED as LoginStatus,
- terraTOSLoginStatus:
- LOGIN_STATUS_NOT_STARTED as LoginStatus,
- token: undefined,
- userProfile: undefined,
-});
-
-interface Props {
- children: ReactNode | ReactNode[];
- sessionTimeout?: number;
-}
-
-/**
- * Auth provider for consuming components to subscribe to changes in auth-related state.
- * @param props - Component inputs.
- * @param props.children - Set of children components that can possibly consume the query provider.
- * @param props.sessionTimeout - If provided, will set the value for a session timeout (in milliseconds).
- * @returns Provider element to be used by consumers to both update authentication state and subscribe to changes in authentication state.
- */
-export function AuthProvider({ children, sessionTimeout }: Props): JSX.Element {
- const { config } = useConfig();
- const { authentication, redirectRootToPath } = config;
- const { basePath } = useRouter();
- const { token, tokenClient } = useTokenClient();
- const terraNIHProfileLoginStatus = useFetchTerraNIHProfile(token);
- const terraProfileLoginStatus = useFetchTerraProfile(token);
- const terraTOSLoginStatus = useFetchTerraTermsOfService(token);
- const userProfileLoginStatus = useFetchGoogleProfile(token);
- const isEnabled = Boolean(authentication);
- const isAuthenticated = userProfileLoginStatus.isSuccess;
- const releaseToken = shouldReleaseToken(
- userProfileLoginStatus,
- terraProfileLoginStatus,
- terraTOSLoginStatus
- );
- const authenticationStatus = useAuthenticationStatus(
- userProfileLoginStatus,
- terraProfileLoginStatus,
- terraTOSLoginStatus,
- terraNIHProfileLoginStatus
- );
-
- // Handle completion of authentication process.
- useAuthenticationComplete(authenticationStatus);
-
- /**
- * If sessionTimeout is set and user is authenticated, the app will reload and redirect to
- * origin, including base path, root path, and query param.
- */
- useIdleTimer({
- onIdle: () =>
- isAuthenticated &&
- sessionTimeout &&
- (window.location.href =
- window.location.origin +
- basePath +
- redirectRootToPath +
- "?" +
- `${INACTIVITY_PARAM}=true`),
- timeout: sessionTimeout,
- });
-
- /**
- * Requests access token and authenticates user.
- */
- const authenticateUser = useCallback((): void => {
- tokenClient.requestAccessToken();
- }, [tokenClient]);
-
- /**
- * Navigates to login page.
- */
- const requestAuthentication = useCallback((): void => {
- Router.push(ROUTE_LOGIN);
- }, []);
-
- return (
-
- {children}
-
- );
-}
-
-/**
- * Token is released for the following conditions:
- * - Terra endpoint is configured and the terms of service response is successful, or
- * - Terra endpoint is not configured and the user profile response is successful.
- * @param userProfileLoginStatus - User profile login status.
- * @param terraProfileLoginStatus - Terra profile login status.
- * @param terraTOSLoginStatus - Terra terms of service login status.
- * @returns true if the token should be released.
- */
-export function shouldReleaseToken(
- userProfileLoginStatus: LoginStatus,
- terraProfileLoginStatus: LoginStatus,
- terraTOSLoginStatus: LoginStatus
-): boolean {
- if (terraProfileLoginStatus.isSupported) {
- return terraTOSLoginStatus.isSuccess;
- }
- return userProfileLoginStatus.isSuccess;
-}
diff --git a/src/providers/authentication/auth/actions.ts b/src/providers/authentication/auth/actions.ts
new file mode 100644
index 00000000..410f1b81
--- /dev/null
+++ b/src/providers/authentication/auth/actions.ts
@@ -0,0 +1,17 @@
+import { AuthState, UpdateAuthStatePayload } from "./types";
+
+/**
+ * Update auth state.
+ * @param state - State.
+ * @param payload - Payload.
+ * @returns state.
+ */
+export function updateAuthState(
+ state: AuthState,
+ payload: UpdateAuthStatePayload
+): AuthState {
+ return {
+ ...state,
+ ...payload,
+ };
+}
diff --git a/src/providers/authentication/auth/constants.ts b/src/providers/authentication/auth/constants.ts
new file mode 100644
index 00000000..99f4d156
--- /dev/null
+++ b/src/providers/authentication/auth/constants.ts
@@ -0,0 +1,6 @@
+import { AUTH_STATUS, AuthState } from "./types";
+
+export const DEFAULT_AUTH_STATE: AuthState = {
+ isAuthenticated: false,
+ status: AUTH_STATUS.PENDING,
+};
diff --git a/src/providers/authentication/auth/context.ts b/src/providers/authentication/auth/context.ts
new file mode 100644
index 00000000..cb71ec08
--- /dev/null
+++ b/src/providers/authentication/auth/context.ts
@@ -0,0 +1,9 @@
+import { createContext } from "react";
+import { DEFAULT_AUTH_STATE } from "./constants";
+import { AuthContextProps } from "./types";
+
+export const AuthContext = createContext({
+ authDispatch: null,
+ authState: DEFAULT_AUTH_STATE,
+ service: undefined,
+});
diff --git a/src/providers/authentication/auth/dispatch.ts b/src/providers/authentication/auth/dispatch.ts
new file mode 100644
index 00000000..7250b197
--- /dev/null
+++ b/src/providers/authentication/auth/dispatch.ts
@@ -0,0 +1,31 @@
+import {
+ AuthActionKind,
+ ResetStateAction,
+ UpdateAuthStateAction,
+ UpdateAuthStatePayload,
+} from "./types";
+
+/**
+ * Reset state action.
+ * @returns state.
+ */
+export function resetState(): ResetStateAction {
+ return {
+ payload: undefined,
+ type: AuthActionKind.ResetState,
+ };
+}
+
+/**
+ * Update auth state action.
+ * @param payload - Payload.
+ * @returns Action.
+ */
+export function updateAuthState(
+ payload: UpdateAuthStatePayload
+): UpdateAuthStateAction {
+ return {
+ payload,
+ type: AuthActionKind.UpdateAuthState,
+ };
+}
diff --git a/src/providers/authentication/auth/hook.ts b/src/providers/authentication/auth/hook.ts
new file mode 100644
index 00000000..59c574ed
--- /dev/null
+++ b/src/providers/authentication/auth/hook.ts
@@ -0,0 +1,11 @@
+import { useContext } from "react";
+import { AuthContext } from "./context";
+import { AuthContextProps } from "./types";
+
+/**
+ * Auth hook.
+ * @returns auth context.
+ */
+export const useAuth = (): AuthContextProps => {
+ return useContext(AuthContext);
+};
diff --git a/src/providers/authentication/auth/reducer.ts b/src/providers/authentication/auth/reducer.ts
new file mode 100644
index 00000000..db9512d2
--- /dev/null
+++ b/src/providers/authentication/auth/reducer.ts
@@ -0,0 +1,22 @@
+import { updateAuthState } from "./actions";
+import { AuthAction, AuthActionKind, AuthState } from "./types";
+
+/**
+ * Auth reducer.
+ * @param state - State.
+ * @param action - Action.
+ * @returns state.
+ */
+export function authReducer(state: AuthState, action: AuthAction): AuthState {
+ const { payload, type } = action;
+ switch (type) {
+ case AuthActionKind.ResetState: {
+ return { ...state, ...state.initialState };
+ }
+ case AuthActionKind.UpdateAuthState: {
+ return updateAuthState(state, payload);
+ }
+ default:
+ return state;
+ }
+}
diff --git a/src/providers/authentication/auth/types.ts b/src/providers/authentication/auth/types.ts
new file mode 100644
index 00000000..44b009bc
--- /dev/null
+++ b/src/providers/authentication/auth/types.ts
@@ -0,0 +1,52 @@
+import { Dispatch } from "react";
+import { ProviderId } from "../common/types";
+
+export type AuthAction = ResetStateAction | UpdateAuthStateAction;
+
+export enum AuthActionKind {
+ ResetState = "RESET_STATE",
+ UpdateAuthState = "UPDATE_AUTH_STATE",
+}
+
+export interface AuthContextProps {
+ authDispatch: Dispatch | null;
+ authState: AuthState;
+ service: Service | undefined;
+}
+
+export interface AuthState {
+ initialState?: AuthState;
+ isAuthenticated: boolean;
+ status: AUTH_STATUS;
+}
+
+export type ResetStateAction = {
+ payload: ResetStatePayload;
+ type: AuthActionKind.ResetState;
+};
+
+export type ResetStatePayload = undefined;
+
+export interface Service {
+ [key: string]: unknown;
+ requestLogin: (providerId: ProviderId) => void;
+ requestLogout: (options?: {
+ callbackUrl?: string;
+ redirect?: boolean;
+ }) => void;
+}
+
+export enum AUTH_STATUS {
+ PENDING = "PENDING",
+ SETTLED = "SETTLED",
+}
+
+export interface UpdateAuthStateAction {
+ payload: UpdateAuthStatePayload;
+ type: AuthActionKind.UpdateAuthState;
+}
+
+export interface UpdateAuthStatePayload {
+ isAuthenticated?: boolean;
+ status?: AUTH_STATUS;
+}
diff --git a/src/providers/authentication/authentication/actions.ts b/src/providers/authentication/authentication/actions.ts
new file mode 100644
index 00000000..7d82b41a
--- /dev/null
+++ b/src/providers/authentication/authentication/actions.ts
@@ -0,0 +1,17 @@
+import { AuthenticationState, UpdateAuthenticationPayload } from "./types";
+
+/**
+ * Update authentication action.
+ * @param state - State.
+ * @param payload - Payload.
+ * @returns state.
+ */
+export function updateAuthenticationAction(
+ state: AuthenticationState,
+ payload: UpdateAuthenticationPayload
+): AuthenticationState {
+ return {
+ ...state,
+ ...payload,
+ };
+}
diff --git a/src/providers/authentication/authentication/constants.ts b/src/providers/authentication/authentication/constants.ts
new file mode 100644
index 00000000..d09cc9b2
--- /dev/null
+++ b/src/providers/authentication/authentication/constants.ts
@@ -0,0 +1,6 @@
+import { AUTHENTICATION_STATUS, AuthenticationState } from "./types";
+
+export const DEFAULT_AUTHENTICATION_STATE: AuthenticationState = {
+ profile: undefined,
+ status: AUTHENTICATION_STATUS.PENDING,
+};
diff --git a/src/providers/authentication/authentication/context.ts b/src/providers/authentication/authentication/context.ts
new file mode 100644
index 00000000..fa66b3c9
--- /dev/null
+++ b/src/providers/authentication/authentication/context.ts
@@ -0,0 +1,8 @@
+import { createContext } from "react";
+import { DEFAULT_AUTHENTICATION_STATE } from "./constants";
+import { AuthenticationContextProps } from "./types";
+
+export const AuthenticationContext = createContext({
+ authenticationDispatch: null,
+ authenticationState: DEFAULT_AUTHENTICATION_STATE,
+});
diff --git a/src/providers/authentication/authentication/dispatch.ts b/src/providers/authentication/authentication/dispatch.ts
new file mode 100644
index 00000000..197d5363
--- /dev/null
+++ b/src/providers/authentication/authentication/dispatch.ts
@@ -0,0 +1,55 @@
+import {
+ AUTHENTICATION_STATUS,
+ AuthenticationActionKind,
+ RequestAuthenticationAction,
+ ResetStateAction,
+ UpdateAuthenticationAction,
+ UpdateAuthenticationPayload,
+} from "./types";
+
+/**
+ * Authentication is complete.
+ * @returns Action.
+ */
+export function authenticationComplete(): UpdateAuthenticationAction {
+ return {
+ payload: { status: AUTHENTICATION_STATUS.SETTLED },
+ type: AuthenticationActionKind.UpdateAuthentication,
+ };
+}
+
+/**
+ * Request authentication action.
+ * @returns Action.
+ */
+export function requestAuthentication(): RequestAuthenticationAction {
+ return {
+ payload: undefined,
+ type: AuthenticationActionKind.RequestAuthentication,
+ };
+}
+
+/**
+ * Reset authentication action.
+ * @returns Action.
+ */
+export function resetState(): ResetStateAction {
+ return {
+ payload: undefined,
+ type: AuthenticationActionKind.ResetState,
+ };
+}
+
+/**
+ * Update authentication action.
+ * @param payload - Payload.
+ * @returns Action.
+ */
+export function updateAuthentication(
+ payload: UpdateAuthenticationPayload
+): UpdateAuthenticationAction {
+ return {
+ payload,
+ type: AuthenticationActionKind.UpdateAuthentication,
+ };
+}
diff --git a/src/providers/authentication/authentication/hook.ts b/src/providers/authentication/authentication/hook.ts
new file mode 100644
index 00000000..a5a32100
--- /dev/null
+++ b/src/providers/authentication/authentication/hook.ts
@@ -0,0 +1,11 @@
+import { useContext } from "react";
+import { AuthenticationContext } from "./context";
+import { AuthenticationContextProps } from "./types";
+
+/**
+ * Authentication hook.
+ * @returns authentication context.
+ */
+export const useAuthentication = (): AuthenticationContextProps => {
+ return useContext(AuthenticationContext);
+};
diff --git a/src/providers/authentication/authentication/reducer.ts b/src/providers/authentication/authentication/reducer.ts
new file mode 100644
index 00000000..d21ddc90
--- /dev/null
+++ b/src/providers/authentication/authentication/reducer.ts
@@ -0,0 +1,33 @@
+import { updateAuthenticationAction } from "./actions";
+import {
+ AUTHENTICATION_STATUS,
+ AuthenticationAction,
+ AuthenticationActionKind,
+ AuthenticationState,
+} from "./types";
+
+/**
+ * Authentication reducer.
+ * @param state - State.
+ * @param action - Action.
+ * @returns state.
+ */
+export function authenticationReducer(
+ state: AuthenticationState,
+ action: AuthenticationAction
+): AuthenticationState {
+ const { payload, type } = action;
+ switch (type) {
+ case AuthenticationActionKind.RequestAuthentication: {
+ return { ...state, status: AUTHENTICATION_STATUS.PENDING };
+ }
+ case AuthenticationActionKind.ResetState: {
+ return { ...state, ...state.initialState };
+ }
+ case AuthenticationActionKind.UpdateAuthentication: {
+ return updateAuthenticationAction(state, payload);
+ }
+ default:
+ return state;
+ }
+}
diff --git a/src/providers/authentication/authentication/types.ts b/src/providers/authentication/authentication/types.ts
new file mode 100644
index 00000000..d9562a97
--- /dev/null
+++ b/src/providers/authentication/authentication/types.ts
@@ -0,0 +1,64 @@
+import { Dispatch } from "react";
+
+export enum AUTHENTICATION_STATUS {
+ PENDING = "PENDING",
+ SETTLED = "SETTLED",
+}
+
+export type AuthenticationAction =
+ | RequestAuthenticationAction
+ | ResetStateAction
+ | UpdateAuthenticationAction;
+
+export enum AuthenticationActionKind {
+ RequestAuthentication = "REQUEST_AUTHENTICATION",
+ ResetState = "RESET_STATE",
+ UpdateAuthentication = "UPDATE_AUTHENTICATION",
+}
+
+export interface AuthenticationContextProps {
+ authenticationDispatch: Dispatch | null;
+ authenticationState: AuthenticationState;
+}
+
+export interface AuthenticationState {
+ initialState?: AuthenticationState;
+ profile: Profile;
+ status: AUTHENTICATION_STATUS;
+}
+
+export interface BaseProfile {
+ id?: string;
+ name: string;
+}
+
+export type Profile = P | undefined;
+
+export type RequestAuthenticationAction = {
+ payload: RequestAuthenticationPayload;
+ type: AuthenticationActionKind.RequestAuthentication;
+};
+
+export type RequestAuthenticationPayload = undefined;
+
+export type ResetStateAction = {
+ payload: ResetStatePayload;
+ type: AuthenticationActionKind.ResetState;
+};
+
+export type ResetStatePayload = undefined;
+
+export type UpdateAuthenticationAction = {
+ payload: UpdateAuthenticationPayload;
+ type: AuthenticationActionKind.UpdateAuthentication;
+};
+
+export interface UpdateAuthenticationPayload {
+ profile?: Profile;
+ status?: AUTHENTICATION_STATUS;
+}
+
+export interface UserProfile extends BaseProfile {
+ email: string;
+ image?: string;
+}
diff --git a/src/providers/authentication/authentication/utils.ts b/src/providers/authentication/authentication/utils.ts
new file mode 100644
index 00000000..389f6153
--- /dev/null
+++ b/src/providers/authentication/authentication/utils.ts
@@ -0,0 +1,25 @@
+/**
+ * Fetches data from given endpoint and options.
+ * @param endpoint - Endpoint.
+ * @param options - Request options.
+ * @param callback - Callback.
+ * @param callback.onError - Error callback.
+ * @param callback.onSuccess - Success callback.
+ */
+export function fetchProfile(
+ endpoint: string,
+ options?: RequestInit,
+ callback?: {
+ onError: (error: E) => void;
+ onSuccess: (response: R) => void;
+ }
+): void {
+ fetch(endpoint, options)
+ .then((response) => response.json())
+ .then((r: R) => {
+ callback?.onSuccess(r);
+ })
+ .catch((e: E) => {
+ callback?.onError(e);
+ });
+}
diff --git a/src/providers/authentication/common/types.ts b/src/providers/authentication/common/types.ts
new file mode 100644
index 00000000..42c5c76e
--- /dev/null
+++ b/src/providers/authentication/common/types.ts
@@ -0,0 +1 @@
+export type ProviderId = string;
diff --git a/src/providers/authentication/common/utils.ts b/src/providers/authentication/common/utils.ts
new file mode 100644
index 00000000..71567845
--- /dev/null
+++ b/src/providers/authentication/common/utils.ts
@@ -0,0 +1,11 @@
+/**
+ * Reducer state initializer.
+ * @param initialState - Initial state.
+ * @returns initial reducer state.
+ */
+export function initializer(initialState: S): S {
+ return {
+ ...initialState,
+ initialState,
+ };
+}
diff --git a/src/providers/authentication/credentials/actions.ts b/src/providers/authentication/credentials/actions.ts
new file mode 100644
index 00000000..668c4fe9
--- /dev/null
+++ b/src/providers/authentication/credentials/actions.ts
@@ -0,0 +1,17 @@
+import { CredentialsState, UpdateCredentialsPayload } from "./types";
+
+/**
+ * Update credentials action.
+ * @param state - State.
+ * @param payload - Payload.
+ * @returns state.
+ */
+export function updateCredentialsAction(
+ state: CredentialsState,
+ payload: UpdateCredentialsPayload
+): CredentialsState {
+ return {
+ ...state,
+ credentials: payload,
+ };
+}
diff --git a/src/providers/authentication/credentials/constants.ts b/src/providers/authentication/credentials/constants.ts
new file mode 100644
index 00000000..6c1a0a9a
--- /dev/null
+++ b/src/providers/authentication/credentials/constants.ts
@@ -0,0 +1,5 @@
+import { CredentialsState } from "./types";
+
+export const DEFAULT_CREDENTIALS_STATE: CredentialsState = {
+ credentials: undefined,
+};
diff --git a/src/providers/authentication/credentials/context.ts b/src/providers/authentication/credentials/context.ts
new file mode 100644
index 00000000..ee1729ea
--- /dev/null
+++ b/src/providers/authentication/credentials/context.ts
@@ -0,0 +1,8 @@
+import { createContext } from "react";
+import { DEFAULT_CREDENTIALS_STATE } from "./constants";
+import { CredentialsContextProps } from "./types";
+
+export const CredentialsContext = createContext({
+ credentialsDispatch: null,
+ credentialsState: DEFAULT_CREDENTIALS_STATE,
+});
diff --git a/src/providers/authentication/credentials/dispatch.ts b/src/providers/authentication/credentials/dispatch.ts
new file mode 100644
index 00000000..37d34cf9
--- /dev/null
+++ b/src/providers/authentication/credentials/dispatch.ts
@@ -0,0 +1,31 @@
+import {
+ CredentialsActionKind,
+ ResetStateAction,
+ UpdateCredentialsAction,
+ UpdateCredentialsPayload,
+} from "./types";
+
+/**
+ * Update credentials action.
+ * @returns Action.
+ */
+export function resetState(): ResetStateAction {
+ return {
+ payload: undefined,
+ type: CredentialsActionKind.ResetState,
+ };
+}
+
+/**
+ * Update credentials action.
+ * @param payload - Payload.
+ * @returns Action.
+ */
+export function updateCredentials(
+ payload: UpdateCredentialsPayload
+): UpdateCredentialsAction {
+ return {
+ payload,
+ type: CredentialsActionKind.UpdateCredentials,
+ };
+}
diff --git a/src/providers/authentication/credentials/hook.ts b/src/providers/authentication/credentials/hook.ts
new file mode 100644
index 00000000..1b34a43a
--- /dev/null
+++ b/src/providers/authentication/credentials/hook.ts
@@ -0,0 +1,11 @@
+import { useContext } from "react";
+import { CredentialsContext } from "./context";
+import { CredentialsContextProps } from "./types";
+
+/**
+ * Credentials hook.
+ * @returns credentials context.
+ */
+export const useCredentials = (): CredentialsContextProps => {
+ return useContext(CredentialsContext);
+};
diff --git a/src/providers/authentication/credentials/reducer.ts b/src/providers/authentication/credentials/reducer.ts
new file mode 100644
index 00000000..d6b9c981
--- /dev/null
+++ b/src/providers/authentication/credentials/reducer.ts
@@ -0,0 +1,30 @@
+import { updateCredentialsAction } from "./actions";
+import { DEFAULT_CREDENTIALS_STATE } from "./constants";
+import {
+ CredentialsAction,
+ CredentialsActionKind,
+ CredentialsState,
+} from "./types";
+
+/**
+ * Credentials reducer.
+ * @param state - State.
+ * @param action - Action.
+ * @returns state.
+ */
+export function credentialsReducer(
+ state: CredentialsState,
+ action: CredentialsAction
+): CredentialsState {
+ const { payload, type } = action;
+ switch (type) {
+ case CredentialsActionKind.ResetState: {
+ return DEFAULT_CREDENTIALS_STATE;
+ }
+ case CredentialsActionKind.UpdateCredentials: {
+ return updateCredentialsAction(state, payload);
+ }
+ default:
+ return state;
+ }
+}
diff --git a/src/providers/authentication/credentials/types.ts b/src/providers/authentication/credentials/types.ts
new file mode 100644
index 00000000..84690ce8
--- /dev/null
+++ b/src/providers/authentication/credentials/types.ts
@@ -0,0 +1,33 @@
+import { Dispatch } from "react";
+
+export type Credentials = C;
+
+export type CredentialsAction = ResetStateAction | UpdateCredentialsAction;
+
+export enum CredentialsActionKind {
+ ResetState = "RESET_STATE",
+ UpdateCredentials = "STORE_CREDENTIALS",
+}
+
+export interface CredentialsContextProps {
+ credentialsDispatch: Dispatch | null;
+ credentialsState: CredentialsState;
+}
+
+export interface CredentialsState {
+ credentials: Credentials;
+}
+
+export type ResetStateAction = {
+ payload: ResetStatePayload;
+ type: CredentialsActionKind.ResetState;
+};
+
+export type ResetStatePayload = undefined;
+
+export type UpdateCredentialsAction = {
+ payload: UpdateCredentialsPayload;
+ type: CredentialsActionKind.UpdateCredentials;
+};
+
+export type UpdateCredentialsPayload = Credentials;
diff --git a/src/providers/authentication/terra/context.ts b/src/providers/authentication/terra/context.ts
new file mode 100644
index 00000000..6ccf5bac
--- /dev/null
+++ b/src/providers/authentication/terra/context.ts
@@ -0,0 +1,16 @@
+import { createContext } from "react";
+import { LOGIN_STATUS_NOT_STARTED } from "./hooks/common/constants";
+import { LoginStatus } from "./hooks/common/entities";
+import { TerraNIHResponse } from "./hooks/useFetchTerraNIHProfile";
+import { TerraResponse } from "./hooks/useFetchTerraProfile";
+import { TerraTermsOfServiceResponse } from "./hooks/useFetchTerraTermsOfService";
+import { TerraProfileContextProps } from "./types";
+
+export const TerraProfileContext = createContext({
+ terraNIHProfileLoginStatus:
+ LOGIN_STATUS_NOT_STARTED as LoginStatus,
+ terraProfileLoginStatus:
+ LOGIN_STATUS_NOT_STARTED as LoginStatus,
+ terraTOSLoginStatus:
+ LOGIN_STATUS_NOT_STARTED as LoginStatus,
+});
diff --git a/src/providers/authentication/terra/hook.ts b/src/providers/authentication/terra/hook.ts
new file mode 100644
index 00000000..9a16acc8
--- /dev/null
+++ b/src/providers/authentication/terra/hook.ts
@@ -0,0 +1,11 @@
+import { useContext } from "react";
+import { TerraProfileContext } from "./context";
+import { TerraProfileContextProps } from "./types";
+
+/**
+ * Terra profile hook.
+ * @returns terra profile context.
+ */
+export const useTerraProfile = (): TerraProfileContextProps => {
+ return useContext(TerraProfileContext);
+};
diff --git a/src/hooks/useAuthentication/common/constants.ts b/src/providers/authentication/terra/hooks/common/constants.ts
similarity index 73%
rename from src/hooks/useAuthentication/common/constants.ts
rename to src/providers/authentication/terra/hooks/common/constants.ts
index af025127..361a9a3f 100644
--- a/src/hooks/useAuthentication/common/constants.ts
+++ b/src/providers/authentication/terra/hooks/common/constants.ts
@@ -20,3 +20,12 @@ export const LOGIN_STATUS_NOT_SUPPORTED: LoginStatus = {
requestStatus: REQUEST_STATUS.NOT_STARTED,
response: undefined,
};
+
+export const LOGIN_STATUS_PENDING: LoginStatus = {
+ isSuccess: false,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.PENDING,
+ response: undefined,
+};
+
+export const TERRA_SERVICE_ID = "terra";
diff --git a/src/hooks/useAuthentication/common/entities.ts b/src/providers/authentication/terra/hooks/common/entities.ts
similarity index 79%
rename from src/hooks/useAuthentication/common/entities.ts
rename to src/providers/authentication/terra/hooks/common/entities.ts
index b0e66cd0..47632102 100644
--- a/src/hooks/useAuthentication/common/entities.ts
+++ b/src/providers/authentication/terra/hooks/common/entities.ts
@@ -1,15 +1,8 @@
-import { GoogleResponse } from "../useFetchGoogleProfile";
import { TerraNIHResponse } from "../useFetchTerraNIHProfile";
import { TerraResponse } from "../useFetchTerraProfile";
import { TerraTermsOfServiceResponse } from "../useFetchTerraTermsOfService";
-export enum AUTHENTICATION_STATUS {
- COMPLETED = "COMPLETED",
- INCOMPLETE = "INCOMPLETE",
-}
-
export type LoginResponse =
- | GoogleResponse
| TerraResponse
| TerraNIHResponse
| TerraTermsOfServiceResponse;
@@ -31,4 +24,5 @@ export enum REQUEST_STATUS {
COMPLETED = "COMPLETED",
FAILED = "FAILED",
NOT_STARTED = "NOT_STARTED",
+ PENDING = "PENDING",
}
diff --git a/src/hooks/useAuthentication/common/utils.ts b/src/providers/authentication/terra/hooks/common/utils.ts
similarity index 100%
rename from src/hooks/useAuthentication/common/utils.ts
rename to src/providers/authentication/terra/hooks/common/utils.ts
diff --git a/src/providers/authentication/terra/hooks/useFetchProfiles.ts b/src/providers/authentication/terra/hooks/useFetchProfiles.ts
new file mode 100644
index 00000000..47ab2fdc
--- /dev/null
+++ b/src/providers/authentication/terra/hooks/useFetchProfiles.ts
@@ -0,0 +1,59 @@
+import { useEffect, useState } from "react";
+import { useAuthentication } from "../../authentication/hook";
+import { TERRA_PROFILE_STATUS } from "../types";
+import { getProfileStatus } from "../utils";
+import { LoginStatus } from "./common/entities";
+import {
+ TerraNIHResponse,
+ useFetchTerraNIHProfile,
+} from "./useFetchTerraNIHProfile";
+import { TerraResponse, useFetchTerraProfile } from "./useFetchTerraProfile";
+import {
+ TerraTermsOfServiceResponse,
+ useFetchTerraTermsOfService,
+} from "./useFetchTerraTermsOfService";
+
+export interface UseFetchProfiles {
+ isComplete: boolean;
+ isProfileActive: boolean;
+ terraNIHProfileLoginStatus: LoginStatus;
+ terraProfileLoginStatus: LoginStatus;
+ terraTOSLoginStatus: LoginStatus;
+}
+
+export const useFetchProfiles = (token?: string): UseFetchProfiles => {
+ const [status, setStatus] = useState(
+ TERRA_PROFILE_STATUS.PENDING
+ );
+ const {
+ authenticationState: { profile },
+ } = useAuthentication();
+ const isUserAuthenticated = !!profile;
+ const terraNIHProfileLoginStatus = useFetchTerraNIHProfile(token);
+ const terraProfileLoginStatus = useFetchTerraProfile(token);
+ const terraTOSLoginStatus = useFetchTerraTermsOfService(token);
+
+ useEffect(() => {
+ setStatus(
+ getProfileStatus(
+ isUserAuthenticated,
+ terraNIHProfileLoginStatus,
+ terraProfileLoginStatus,
+ terraTOSLoginStatus
+ )
+ );
+ }, [
+ isUserAuthenticated,
+ terraNIHProfileLoginStatus,
+ terraProfileLoginStatus,
+ terraTOSLoginStatus,
+ ]);
+
+ return {
+ isComplete: status !== TERRA_PROFILE_STATUS.PENDING,
+ isProfileActive: status === TERRA_PROFILE_STATUS.AUTHENTICATED,
+ terraNIHProfileLoginStatus,
+ terraProfileLoginStatus,
+ terraTOSLoginStatus,
+ };
+};
diff --git a/src/hooks/useAuthentication/useFetchTerraNIHProfile.ts b/src/providers/authentication/terra/hooks/useFetchTerraNIHProfile.ts
similarity index 77%
rename from src/hooks/useAuthentication/useFetchTerraNIHProfile.ts
rename to src/providers/authentication/terra/hooks/useFetchTerraNIHProfile.ts
index 25e6f42a..4dc90b40 100644
--- a/src/hooks/useAuthentication/useFetchTerraNIHProfile.ts
+++ b/src/providers/authentication/terra/hooks/useFetchTerraNIHProfile.ts
@@ -1,6 +1,11 @@
import { useCallback, useEffect, useState } from "react";
-import { useAuthenticationConfig } from "../useAuthenticationConfig";
-import { LOGIN_STATUS_FAILED } from "./common/constants";
+import { useAuthenticationConfig } from "../../../../hooks/authentication/config/useAuthenticationConfig";
+import {
+ LOGIN_STATUS_FAILED,
+ LOGIN_STATUS_NOT_STARTED,
+ LOGIN_STATUS_PENDING,
+ TERRA_SERVICE_ID,
+} from "./common/constants";
import {
LoginResponseError,
LoginStatus,
@@ -10,6 +15,9 @@ import {
getAuthenticationRequestOptions,
initLoginStatus,
} from "./common/utils";
+import { getServiceEndpoint } from "./utils";
+
+const ENDPOINT_ID = "nihStatus";
interface DatasetPermission {
authorized: boolean;
@@ -30,16 +38,20 @@ export interface TerraNIHResponse {
* @returns Terra NIH login status.
*/
export const useFetchTerraNIHProfile = (token?: string): Status => {
- const authenticationConfig = useAuthenticationConfig();
- const { terraAuthConfig: { terraNIHProfileEndpoint: endpoint } = {} } =
- authenticationConfig;
+ const { services } = useAuthenticationConfig() || {};
+ const endpoint = getServiceEndpoint(services, TERRA_SERVICE_ID, ENDPOINT_ID);
const [loginStatus, setLoginStatus] = useState(
initLoginStatus(endpoint) as Status
);
// Fetch Terra NIH account profile.
const fetchEndpointData = useCallback(
- (endpoint: string, accessToken: string): void => {
+ (endpoint: string, accessToken?: string): void => {
+ if (!accessToken) {
+ setLoginStatus(LOGIN_STATUS_NOT_STARTED as Status);
+ return;
+ }
+ setLoginStatus(LOGIN_STATUS_PENDING as Status);
fetch(endpoint, getAuthenticationRequestOptions(accessToken))
.then((response) => response.json())
.then((response: LoginResponseError | TerraNIHResponse) => {
@@ -64,7 +76,6 @@ export const useFetchTerraNIHProfile = (token?: string): Status => {
// Fetches Terra NIH account profile.
useEffect(() => {
- if (!token) return;
if (!endpoint) return;
fetchEndpointData(endpoint, token);
}, [endpoint, fetchEndpointData, token]);
diff --git a/src/hooks/useAuthentication/useFetchTerraProfile.ts b/src/providers/authentication/terra/hooks/useFetchTerraProfile.ts
similarity index 78%
rename from src/hooks/useAuthentication/useFetchTerraProfile.ts
rename to src/providers/authentication/terra/hooks/useFetchTerraProfile.ts
index b91657e2..83ec4c9a 100644
--- a/src/hooks/useAuthentication/useFetchTerraProfile.ts
+++ b/src/providers/authentication/terra/hooks/useFetchTerraProfile.ts
@@ -1,6 +1,11 @@
import { useCallback, useEffect, useState } from "react";
-import { useAuthenticationConfig } from "../useAuthenticationConfig";
-import { LOGIN_STATUS_FAILED } from "./common/constants";
+import { useAuthenticationConfig } from "../../../../hooks/authentication/config/useAuthenticationConfig";
+import {
+ LOGIN_STATUS_FAILED,
+ LOGIN_STATUS_NOT_STARTED,
+ LOGIN_STATUS_PENDING,
+ TERRA_SERVICE_ID,
+} from "./common/constants";
import {
LoginResponseError,
LoginStatus,
@@ -10,6 +15,9 @@ import {
getAuthenticationRequestOptions,
initLoginStatus,
} from "./common/utils";
+import { getServiceEndpoint } from "./utils";
+
+const ENDPOINT_ID = "profile";
type Status = LoginStatus;
@@ -37,16 +45,20 @@ interface TerraResponseUserInfo {
* @returns Terra profile login status.
*/
export const useFetchTerraProfile = (token?: string): Status => {
- const authenticationConfig = useAuthenticationConfig();
- const { terraAuthConfig: { terraProfileEndpoint: endpoint } = {} } =
- authenticationConfig;
+ const { services } = useAuthenticationConfig() || {};
+ const endpoint = getServiceEndpoint(services, TERRA_SERVICE_ID, ENDPOINT_ID);
const [loginStatus, setLoginStatus] = useState(
initLoginStatus(endpoint) as Status
);
// Fetch Terra profile.
const fetchEndpointData = useCallback(
- (endpoint: string, accessToken: string): void => {
+ (endpoint: string, accessToken?: string): void => {
+ if (!accessToken) {
+ setLoginStatus(LOGIN_STATUS_NOT_STARTED as Status);
+ return;
+ }
+ setLoginStatus(LOGIN_STATUS_PENDING as Status);
fetch(endpoint, getAuthenticationRequestOptions(accessToken))
.then((response) => response.json())
.then((response: TerraResponse | LoginResponseError) => {
@@ -71,7 +83,6 @@ export const useFetchTerraProfile = (token?: string): Status => {
// Fetches Terra profile.
useEffect(() => {
- if (!token) return;
if (!endpoint) return;
fetchEndpointData(endpoint, token);
}, [endpoint, fetchEndpointData, token]);
diff --git a/src/hooks/useAuthentication/useFetchTerraTermsOfService.ts b/src/providers/authentication/terra/hooks/useFetchTerraTermsOfService.ts
similarity index 78%
rename from src/hooks/useAuthentication/useFetchTerraTermsOfService.ts
rename to src/providers/authentication/terra/hooks/useFetchTerraTermsOfService.ts
index 1a54f65b..d9588abf 100644
--- a/src/hooks/useAuthentication/useFetchTerraTermsOfService.ts
+++ b/src/providers/authentication/terra/hooks/useFetchTerraTermsOfService.ts
@@ -1,6 +1,11 @@
import { useCallback, useEffect, useState } from "react";
-import { useAuthenticationConfig } from "../useAuthenticationConfig";
-import { LOGIN_STATUS_FAILED } from "./common/constants";
+import { useAuthenticationConfig } from "../../../../hooks/authentication/config/useAuthenticationConfig";
+import {
+ LOGIN_STATUS_FAILED,
+ LOGIN_STATUS_NOT_STARTED,
+ LOGIN_STATUS_PENDING,
+ TERRA_SERVICE_ID,
+} from "./common/constants";
import {
LoginResponseError,
LoginStatus,
@@ -10,6 +15,9 @@ import {
getAuthenticationRequestOptions,
initLoginStatus,
} from "./common/utils";
+import { getServiceEndpoint } from "./utils";
+
+const ENDPOINT_ID = "tos";
type Status = LoginStatus;
@@ -26,16 +34,20 @@ export interface TerraTermsOfServiceResponse {
* @returns Terra terms of service login status.
*/
export const useFetchTerraTermsOfService = (token?: string): Status => {
- const authenticationConfig = useAuthenticationConfig();
- const { terraAuthConfig: { termsOfServiceEndpoint: endpoint } = {} } =
- authenticationConfig;
+ const { services } = useAuthenticationConfig() || {};
+ const endpoint = getServiceEndpoint(services, TERRA_SERVICE_ID, ENDPOINT_ID);
const [loginStatus, setLoginStatus] = useState(
initLoginStatus(endpoint) as Status
);
// Fetch Terra terms of service.
const fetchEndpointData = useCallback(
- (endpoint: string, accessToken: string): void => {
+ (endpoint: string, accessToken?: string): void => {
+ if (!accessToken) {
+ setLoginStatus(LOGIN_STATUS_NOT_STARTED as Status);
+ return;
+ }
+ setLoginStatus(LOGIN_STATUS_PENDING as Status);
fetch(endpoint, getAuthenticationRequestOptions(accessToken))
.then((response) => response.json())
.then((response: LoginResponseError | TerraTermsOfServiceResponse) => {
@@ -60,7 +72,6 @@ export const useFetchTerraTermsOfService = (token?: string): Status => {
// Fetches Terra terms of service.
useEffect(() => {
- if (!token) return;
if (!endpoint) return;
fetchEndpointData(endpoint, token);
}, [endpoint, fetchEndpointData, token]);
diff --git a/src/providers/authentication/terra/hooks/utils.ts b/src/providers/authentication/terra/hooks/utils.ts
new file mode 100644
index 00000000..b313a49d
--- /dev/null
+++ b/src/providers/authentication/terra/hooks/utils.ts
@@ -0,0 +1,29 @@
+import { AuthService } from "../../../../config/entities";
+
+/**
+ * Returns service endpoint.
+ * @param services - Services.
+ * @param serviceId - Service ID.
+ * @param endpointId - Endpoint ID.
+ * @returns Service endpoint.
+ */
+export function getServiceEndpoint(
+ services: AuthService[] | undefined,
+ serviceId: string,
+ endpointId: string
+): string | undefined {
+ return findService(services, serviceId)?.endpoint[endpointId];
+}
+
+/**
+ * Find a service by service ID.
+ * @param services - Services.
+ * @param serviceId - Service ID.
+ * @returns Service.
+ */
+export function findService(
+ services: AuthService[] | undefined,
+ serviceId: string
+): AuthService | undefined {
+ return services?.find(({ id }) => id === serviceId);
+}
diff --git a/src/providers/authentication/terra/provider.tsx b/src/providers/authentication/terra/provider.tsx
new file mode 100644
index 00000000..d7639c57
--- /dev/null
+++ b/src/providers/authentication/terra/provider.tsx
@@ -0,0 +1,48 @@
+import React, { useEffect } from "react";
+import { authenticationComplete } from "../authentication/dispatch";
+import { useAuthentication } from "../authentication/hook";
+import { updateCredentials } from "../credentials/dispatch";
+import { useCredentials } from "../credentials/hook";
+import { TerraProfileContext } from "./context";
+import { useFetchProfiles } from "./hooks/useFetchProfiles";
+import { TerraProfileProviderProps } from "./types";
+
+export function TerraProfileProvider({
+ children,
+ token,
+}: TerraProfileProviderProps): JSX.Element {
+ const { authenticationDispatch } = useAuthentication();
+ const { credentialsDispatch } = useCredentials();
+ const {
+ isComplete,
+ isProfileActive,
+ terraNIHProfileLoginStatus,
+ terraProfileLoginStatus,
+ terraTOSLoginStatus,
+ } = useFetchProfiles(token);
+
+ useEffect(() => {
+ if (!isComplete) return;
+ authenticationDispatch?.(authenticationComplete());
+ if (!isProfileActive) return;
+ credentialsDispatch?.(updateCredentials(token));
+ }, [
+ authenticationDispatch,
+ credentialsDispatch,
+ isComplete,
+ isProfileActive,
+ token,
+ ]);
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/src/providers/authentication/terra/types.ts b/src/providers/authentication/terra/types.ts
new file mode 100644
index 00000000..a1b0e17a
--- /dev/null
+++ b/src/providers/authentication/terra/types.ts
@@ -0,0 +1,23 @@
+import { ReactNode } from "react";
+import { TokenState } from "../token/types";
+import { LoginStatus } from "./hooks/common/entities";
+import { TerraNIHResponse } from "./hooks/useFetchTerraNIHProfile";
+import { TerraResponse } from "./hooks/useFetchTerraProfile";
+import { TerraTermsOfServiceResponse } from "./hooks/useFetchTerraTermsOfService";
+
+export enum TERRA_PROFILE_STATUS {
+ AUTHENTICATED = "AUTHENTICATED",
+ PENDING = "PENDING",
+ UNAUTHENTICATED = "UNAUTHENTICATED",
+}
+
+export interface TerraProfileProviderProps {
+ children: ReactNode;
+ token: TokenState["token"];
+}
+
+export interface TerraProfileContextProps {
+ terraNIHProfileLoginStatus: LoginStatus;
+ terraProfileLoginStatus: LoginStatus;
+ terraTOSLoginStatus: LoginStatus;
+}
diff --git a/src/providers/authentication/terra/utils.ts b/src/providers/authentication/terra/utils.ts
new file mode 100644
index 00000000..c40d3e95
--- /dev/null
+++ b/src/providers/authentication/terra/utils.ts
@@ -0,0 +1,49 @@
+import { LoginStatus, REQUEST_STATUS } from "./hooks/common/entities";
+import { TerraNIHResponse } from "./hooks/useFetchTerraNIHProfile";
+import { TerraResponse } from "./hooks/useFetchTerraProfile";
+import { TerraTermsOfServiceResponse } from "./hooks/useFetchTerraTermsOfService";
+import { TERRA_PROFILE_STATUS } from "./types";
+
+/**
+ * Determines the status of a user based on authentication and Terra service statuses.
+ * **Logic:**
+ * - **Pending** if the user is not authenticated.
+ * - **Pending** if any supported Terra service request is not started or pending.
+ * - **Unauthenticated** if the Terra profile is supported but the Terms of Service have not been accepted.
+ * - **Authenticated** in all other cases.
+ * @param isUserAuthenticated - User authentication status.
+ * @param terraNIHProfileLoginStatus - Terra NIH profile login status.
+ * @param terraProfileLoginStatus - Terra profile login status.
+ * @param terraTOSLoginStatus - Terra terms of service login status.
+ * @returns Terra profile status.
+ */
+export function getProfileStatus(
+ isUserAuthenticated: boolean,
+ terraNIHProfileLoginStatus: LoginStatus,
+ terraProfileLoginStatus: LoginStatus,
+ terraTOSLoginStatus: LoginStatus
+): TERRA_PROFILE_STATUS {
+ if (!isUserAuthenticated) return TERRA_PROFILE_STATUS.PENDING;
+
+ // Check if any supported Terra service request is not started or pending.
+ const terraServices = [
+ terraNIHProfileLoginStatus,
+ terraProfileLoginStatus,
+ terraTOSLoginStatus,
+ ];
+ const isAnyServicePending = terraServices.some(
+ ({ isSupported, requestStatus }) =>
+ isSupported &&
+ (requestStatus === REQUEST_STATUS.NOT_STARTED ||
+ requestStatus === REQUEST_STATUS.PENDING)
+ );
+ if (isAnyServicePending) return TERRA_PROFILE_STATUS.PENDING;
+
+ // If Terra profile is supported but Terms of Service not accepted.
+ if (terraProfileLoginStatus.isSupported && !terraTOSLoginStatus.isSuccess) {
+ return TERRA_PROFILE_STATUS.UNAUTHENTICATED;
+ }
+
+ // Authenticated in all other cases.
+ return TERRA_PROFILE_STATUS.AUTHENTICATED;
+}
diff --git a/src/providers/authentication/token/constants.ts b/src/providers/authentication/token/constants.ts
new file mode 100644
index 00000000..59fa56bf
--- /dev/null
+++ b/src/providers/authentication/token/constants.ts
@@ -0,0 +1,6 @@
+import { TokenState } from "./types";
+
+export const DEFAULT_TOKEN_STATE: TokenState = {
+ providerId: undefined,
+ token: undefined,
+};
diff --git a/src/providers/authentication/token/dispatch.ts b/src/providers/authentication/token/dispatch.ts
new file mode 100644
index 00000000..c4af8a70
--- /dev/null
+++ b/src/providers/authentication/token/dispatch.ts
@@ -0,0 +1,29 @@
+import {
+ ResetStateAction,
+ TokenActionKind,
+ UpdateTokenAction,
+ UpdateTokenPayload,
+} from "./types";
+
+/**
+ * Reset state action.
+ * @returns Action.
+ */
+export function resetState(): ResetStateAction {
+ return {
+ payload: undefined,
+ type: TokenActionKind.ResetState,
+ };
+}
+
+/**
+ * Update token action.
+ * @param payload - Payload.
+ * @returns Action.
+ */
+export function updateToken(payload: UpdateTokenPayload): UpdateTokenAction {
+ return {
+ payload,
+ type: TokenActionKind.UpdateToken,
+ };
+}
diff --git a/src/providers/authentication/token/reducer.ts b/src/providers/authentication/token/reducer.ts
new file mode 100644
index 00000000..ecad03ef
--- /dev/null
+++ b/src/providers/authentication/token/reducer.ts
@@ -0,0 +1,26 @@
+import { DEFAULT_TOKEN_STATE } from "./constants";
+import { TokenAction, TokenActionKind, TokenState } from "./types";
+
+/**
+ * Token reducer.
+ * @param state - State.
+ * @param action - Action.
+ * @returns state.
+ */
+export function tokenReducer(
+ state: TokenState,
+ action: TokenAction
+): TokenState {
+ const { payload, type } = action;
+ switch (type) {
+ case TokenActionKind.ResetState:
+ return DEFAULT_TOKEN_STATE;
+ case TokenActionKind.UpdateToken:
+ return {
+ ...state,
+ ...payload,
+ };
+ default:
+ return state;
+ }
+}
diff --git a/src/providers/authentication/token/types.ts b/src/providers/authentication/token/types.ts
new file mode 100644
index 00000000..a75a8cde
--- /dev/null
+++ b/src/providers/authentication/token/types.ts
@@ -0,0 +1,36 @@
+import { Dispatch } from "react";
+import { ProviderId } from "../common/types";
+
+export interface ResetStateAction {
+ payload: ResetStatePayload;
+ type: TokenActionKind.ResetState;
+}
+
+export type ResetStatePayload = undefined;
+
+export type TokenAction = ResetStateAction | UpdateTokenAction;
+
+export enum TokenActionKind {
+ ResetState = "RESET_TOKEN",
+ UpdateToken = "UPDATE_TOKEN",
+}
+
+export interface TokenContextProps {
+ tokenDispatch: Dispatch | null;
+ tokenState: TokenState;
+}
+
+export interface TokenState {
+ providerId: ProviderId | undefined;
+ token: string | undefined;
+}
+
+export interface UpdateTokenAction {
+ payload: UpdateTokenPayload;
+ type: TokenActionKind.UpdateToken;
+}
+
+export interface UpdateTokenPayload {
+ providerId: ProviderId | undefined;
+ token: string | undefined;
+}
diff --git a/src/providers/exploreState.tsx b/src/providers/exploreState.tsx
index abefaddd..0e2096e5 100644
--- a/src/providers/exploreState.tsx
+++ b/src/providers/exploreState.tsx
@@ -12,7 +12,7 @@ import { AzulSearchIndex } from "../apis/azul/common/entities";
import { SelectCategoryView, SelectedFilter } from "../common/entities";
import { RowPreviewState } from "../components/Table/features/RowPreview/entities";
import { CategoryGroup, SiteConfig } from "../config/entities";
-import { useAuthentication } from "../hooks/useAuthentication/useAuthentication";
+import { useToken } from "../hooks/authentication/token/useToken";
import {
buildCategoryViews,
buildNextFilterState,
@@ -168,7 +168,7 @@ export function ExploreStateProvider({
const { config, defaultEntityListType } = useConfig();
const { decodedCatalogParam, decodedFeatureFlagParam, decodedFilterParam } =
useURLFilterParams();
- const { isEnabled: isAuthEnabled, token } = useAuthentication();
+ const { token } = useToken();
const entityList = entityListType || defaultEntityListType;
const [initializerArg] = useState(() =>
initReducerArguments(
@@ -196,12 +196,11 @@ export function ExploreStateProvider({
// Reset explore response when token changes.
useEffect(() => {
- if (!isAuthEnabled) return;
exploreDispatch({
payload: undefined,
type: ExploreActionKind.ResetExploreResponse,
});
- }, [exploreDispatch, isAuthEnabled, token]);
+ }, [exploreDispatch, token]);
return (
diff --git a/src/providers/googleSignInAuthentication/common/types.ts b/src/providers/googleSignInAuthentication/common/types.ts
new file mode 100644
index 00000000..8e187e22
--- /dev/null
+++ b/src/providers/googleSignInAuthentication/common/types.ts
@@ -0,0 +1,25 @@
+import { Dispatch } from "react";
+import {
+ AuthenticationAction,
+ AuthenticationContextProps,
+} from "../../authentication/authentication/types";
+import {
+ CredentialsAction,
+ CredentialsContextProps,
+} from "../../authentication/credentials/types";
+import {
+ TokenAction,
+ TokenContextProps,
+} from "../../authentication/token/types";
+
+export interface SessionReducer {
+ authenticationReducer: AuthenticationContextProps;
+ credentialsReducer: CredentialsContextProps;
+ tokenReducer: TokenContextProps;
+}
+
+export interface SessionDispatch {
+ authenticationDispatch: Dispatch | null;
+ credentialsDispatch: Dispatch | null;
+ tokenDispatch: Dispatch | null;
+}
diff --git a/src/providers/googleSignInAuthentication/constants.ts b/src/providers/googleSignInAuthentication/constants.ts
new file mode 100644
index 00000000..2b565cd6
--- /dev/null
+++ b/src/providers/googleSignInAuthentication/constants.ts
@@ -0,0 +1,17 @@
+import { DEFAULT_AUTH_STATE } from "../authentication/auth/constants";
+import { AUTH_STATUS, AuthState } from "../authentication/auth/types";
+import { DEFAULT_AUTHENTICATION_STATE } from "../authentication/authentication/constants";
+import {
+ AUTHENTICATION_STATUS,
+ AuthenticationState,
+} from "../authentication/authentication/types";
+
+export const AUTH_STATE: AuthState = {
+ ...DEFAULT_AUTH_STATE,
+ status: AUTH_STATUS.SETTLED,
+};
+
+export const AUTHENTICATION_STATE: AuthenticationState = {
+ ...DEFAULT_AUTHENTICATION_STATE,
+ status: AUTHENTICATION_STATUS.SETTLED,
+};
diff --git a/src/providers/googleSignInAuthentication/hooks/useGoogleSignInService.ts b/src/providers/googleSignInAuthentication/hooks/useGoogleSignInService.ts
new file mode 100644
index 00000000..1cee20d8
--- /dev/null
+++ b/src/providers/googleSignInAuthentication/hooks/useGoogleSignInService.ts
@@ -0,0 +1,40 @@
+import Router from "next/router";
+import { useCallback } from "react";
+import { useProviders } from "../../../hooks/authentication/providers/useProviders";
+import { Service } from "../../authentication/auth/types";
+import { ProviderId } from "../../authentication/common/types";
+import { SessionReducer } from "../common/types";
+import { service } from "../service/service";
+
+export const useGoogleSignInService = (reducer: SessionReducer): Service => {
+ const { findProvider } = useProviders();
+ const {
+ authenticationReducer: { authenticationDispatch },
+ credentialsReducer: { credentialsDispatch },
+ tokenReducer: { tokenDispatch },
+ } = reducer;
+
+ const onLogin = useCallback(
+ (providerId: ProviderId) => {
+ const provider = findProvider(providerId);
+ if (!provider) return;
+ service.login(provider, { authenticationDispatch, tokenDispatch });
+ },
+ [authenticationDispatch, findProvider, tokenDispatch]
+ );
+
+ const onLogout = useCallback(
+ (options?: { callbackUrl?: string }) => {
+ service.logout({
+ authenticationDispatch,
+ credentialsDispatch,
+ tokenDispatch,
+ });
+ if (!options?.callbackUrl) return;
+ Router.push(options?.callbackUrl).catch((e) => console.error(e));
+ },
+ [authenticationDispatch, credentialsDispatch, tokenDispatch]
+ );
+
+ return { requestLogin: onLogin, requestLogout: onLogout };
+};
diff --git a/src/providers/googleSignInAuthentication/profile/types.ts b/src/providers/googleSignInAuthentication/profile/types.ts
new file mode 100644
index 00000000..c99079e5
--- /dev/null
+++ b/src/providers/googleSignInAuthentication/profile/types.ts
@@ -0,0 +1,15 @@
+export interface GoogleProfile {
+ email: string;
+ email_verified: boolean;
+ family_name: string;
+ given_name: string;
+ hd: string;
+ locale: string;
+ name: string;
+ picture: string;
+ sub: string;
+}
+
+export interface TokenSetParameters {
+ access_token: string;
+}
diff --git a/src/providers/googleSignInAuthentication/profile/utils.ts b/src/providers/googleSignInAuthentication/profile/utils.ts
new file mode 100644
index 00000000..dda481dc
--- /dev/null
+++ b/src/providers/googleSignInAuthentication/profile/utils.ts
@@ -0,0 +1,29 @@
+import { UserProfile } from "../../authentication/authentication/types";
+import { GOOGLE_SIGN_IN_PROVIDER_ID } from "../service/constants";
+import { GoogleProfile } from "./types";
+
+/**
+ * Returns full name, from given and family name.
+ * @param profile - Google response.
+ * @returns full name.
+ */
+function getFullName(profile: GoogleProfile): string {
+ const { family_name: lastName = "", given_name: firstName = "" } = profile;
+ return `${firstName} ${lastName}`.trim();
+}
+
+/**
+ * Returns user profile from google response.
+ * @param profile - Google response.
+ * @returns user profile.
+ */
+export function mapProfile(profile: GoogleProfile): UserProfile {
+ const { email, picture: image } = profile;
+ const name = getFullName(profile);
+ return {
+ email,
+ id: GOOGLE_SIGN_IN_PROVIDER_ID,
+ image,
+ name,
+ };
+}
diff --git a/src/providers/googleSignInAuthentication/provider.tsx b/src/providers/googleSignInAuthentication/provider.tsx
new file mode 100644
index 00000000..9d63f871
--- /dev/null
+++ b/src/providers/googleSignInAuthentication/provider.tsx
@@ -0,0 +1,52 @@
+import React from "react";
+import { useAuthReducer } from "../../hooks/authentication/auth/useAuthReducer";
+import { useAuthenticationReducer } from "../../hooks/authentication/authentication/useAuthenticationReducer";
+import { useCredentialsReducer } from "../../hooks/authentication/credentials/useCredentialsReducer";
+import { useSessionActive } from "../../hooks/authentication/session/useSessionActive";
+import { useSessionAuth } from "../../hooks/authentication/session/useSessionAuth";
+import { useSessionCallbackUrl } from "../../hooks/authentication/session/useSessionCallbackUrl";
+import { useSessionIdleTimer } from "../../hooks/authentication/session/useSessionIdleTimer";
+import { useTokenReducer } from "../../hooks/authentication/token/useTokenReducer";
+import { AuthContext } from "../authentication/auth/context";
+import { AuthenticationContext } from "../authentication/authentication/context";
+import { CredentialsContext } from "../authentication/credentials/context";
+import { AUTH_STATE, AUTHENTICATION_STATE } from "./constants";
+import { useGoogleSignInService } from "./hooks/useGoogleSignInService";
+import { GoogleSignInAuthenticationProviderProps } from "./types";
+
+export function GoogleSignInAuthenticationProvider({
+ APIServicesProvider,
+ children,
+ timeout,
+}: GoogleSignInAuthenticationProviderProps): JSX.Element {
+ const authReducer = useAuthReducer(AUTH_STATE);
+ const authenticationReducer = useAuthenticationReducer(AUTHENTICATION_STATE);
+ const credentialsReducer = useCredentialsReducer();
+ const tokenReducer = useTokenReducer(); // Reducer, local to Google Sign-In process only.
+ const service = useGoogleSignInService({
+ authenticationReducer,
+ credentialsReducer,
+ tokenReducer,
+ });
+ const { callbackUrl } = useSessionCallbackUrl();
+ const { authDispatch, authState } = authReducer;
+ const { isAuthenticated } = authState;
+ useSessionActive(authState);
+ useSessionIdleTimer({
+ disabled: !isAuthenticated,
+ onIdle: () => service.requestLogout({ callbackUrl }),
+ timeout,
+ });
+ useSessionAuth({ authReducer, authenticationReducer });
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ );
+}
diff --git a/src/providers/googleSignInAuthentication/service/constants.ts b/src/providers/googleSignInAuthentication/service/constants.ts
new file mode 100644
index 00000000..ea875ae6
--- /dev/null
+++ b/src/providers/googleSignInAuthentication/service/constants.ts
@@ -0,0 +1,16 @@
+import { GoogleIcon } from "../../../components/common/CustomIcon/components/GoogleIcon/googleIcon";
+import { OAuthProvider } from "../../../config/entities";
+import { GoogleProfile } from "../profile/types";
+import { mapProfile } from "../profile/utils";
+
+export const GOOGLE_SIGN_IN_PROVIDER_ID = "google";
+
+export const GOOGLE_SIGN_IN_PROVIDER: Pick<
+ OAuthProvider,
+ "icon" | "id" | "name" | "profile"
+> = {
+ icon: GoogleIcon({}),
+ id: GOOGLE_SIGN_IN_PROVIDER_ID,
+ name: "Google",
+ profile: mapProfile,
+};
diff --git a/src/providers/googleSignInAuthentication/service/service.ts b/src/providers/googleSignInAuthentication/service/service.ts
new file mode 100644
index 00000000..9592644f
--- /dev/null
+++ b/src/providers/googleSignInAuthentication/service/service.ts
@@ -0,0 +1,59 @@
+import { OAuthProvider } from "../../../config/entities";
+import {
+ requestAuthentication,
+ resetState as resetAuthenticationState,
+ updateAuthentication,
+} from "../../authentication/authentication/dispatch";
+import { AUTHENTICATION_STATUS } from "../../authentication/authentication/types";
+import { fetchProfile } from "../../authentication/authentication/utils";
+import { resetState as resetCredentialsState } from "../../authentication/credentials/dispatch";
+import { getAuthenticationRequestOptions } from "../../authentication/terra/hooks/common/utils";
+import {
+ resetState as resetTokenState,
+ updateToken,
+} from "../../authentication/token/dispatch";
+import { SessionDispatch } from "../common/types";
+import { GoogleProfile, TokenSetParameters } from "../profile/types";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO see https://github.com/clevercanary/data-browser/issues/544.
+declare const google: any;
+
+export const service = {
+ login: (
+ provider: OAuthProvider,
+ dispatch: Pick
+ ): void => {
+ const client = google.accounts.oauth2.initTokenClient({
+ callback: (response: TokenSetParameters) => {
+ const { id, profile, userinfo } = provider;
+ const { access_token: token } = response;
+ dispatch.authenticationDispatch?.(requestAuthentication());
+ dispatch.tokenDispatch?.(updateToken({ providerId: id, token }));
+ fetchProfile(userinfo, getAuthenticationRequestOptions(token), {
+ onError: () =>
+ dispatch.authenticationDispatch?.(
+ updateAuthentication({
+ profile: undefined,
+ status: AUTHENTICATION_STATUS.SETTLED,
+ })
+ ),
+ onSuccess: (r: GoogleProfile) =>
+ dispatch.authenticationDispatch?.(
+ updateAuthentication({
+ profile: profile(r),
+ status: AUTHENTICATION_STATUS.PENDING, // Authentication is pending until Terra profile status is resolved.
+ })
+ ),
+ });
+ },
+ client_id: provider.clientId,
+ scope: provider.authorization.params.scope,
+ });
+ client.requestAccessToken();
+ },
+ logout: (dispatch: SessionDispatch): void => {
+ dispatch.authenticationDispatch?.(resetAuthenticationState());
+ dispatch.credentialsDispatch?.(resetCredentialsState());
+ dispatch.tokenDispatch?.(resetTokenState());
+ },
+};
diff --git a/src/providers/googleSignInAuthentication/types.ts b/src/providers/googleSignInAuthentication/types.ts
new file mode 100644
index 00000000..bdf4a594
--- /dev/null
+++ b/src/providers/googleSignInAuthentication/types.ts
@@ -0,0 +1,7 @@
+import { ElementType, ReactNode } from "react";
+
+export interface GoogleSignInAuthenticationProviderProps {
+ APIServicesProvider: ElementType;
+ children: ReactNode | ReactNode[];
+ timeout?: number;
+}
diff --git a/src/providers/nextAuthAuthentication/hooks/useNextAuthService.ts b/src/providers/nextAuthAuthentication/hooks/useNextAuthService.ts
new file mode 100644
index 00000000..7c5c2151
--- /dev/null
+++ b/src/providers/nextAuthAuthentication/hooks/useNextAuthService.ts
@@ -0,0 +1,26 @@
+import { useCallback } from "react";
+import { transformRoute } from "../../../hooks/authentication/session/useSessionActive";
+import { useRouteHistory } from "../../../hooks/useRouteHistory";
+import { Service } from "../../authentication/auth/types";
+import { ProviderId } from "../../authentication/common/types";
+import { service } from "../service/service";
+
+export const useNextAuthService = (): Service => {
+ const { callbackUrl } = useRouteHistory(2);
+
+ const onLogin = useCallback(
+ (providerId: ProviderId) => {
+ service.login(providerId, { callbackUrl: callbackUrl(transformRoute) });
+ },
+ [callbackUrl]
+ );
+
+ const onLogout = useCallback(
+ (options?: { callbackUrl?: string; redirect?: boolean }) => {
+ service.logout(options);
+ },
+ []
+ );
+
+ return { requestLogin: onLogin, requestLogout: onLogout };
+};
diff --git a/src/providers/nextAuthAuthentication/provider.tsx b/src/providers/nextAuthAuthentication/provider.tsx
new file mode 100644
index 00000000..52b1e120
--- /dev/null
+++ b/src/providers/nextAuthAuthentication/provider.tsx
@@ -0,0 +1,44 @@
+import { SessionProvider } from "next-auth/react";
+import React from "react";
+import { SessionController } from "../../components/Authentication/components/SessionController/SessionController";
+import { useAuthReducer } from "../../hooks/authentication/auth/useAuthReducer";
+import { useAuthenticationReducer } from "../../hooks/authentication/authentication/useAuthenticationReducer";
+import { useSessionAuth } from "../../hooks/authentication/session/useSessionAuth";
+import { useSessionCallbackUrl } from "../../hooks/authentication/session/useSessionCallbackUrl";
+import { useSessionIdleTimer } from "../../hooks/authentication/session/useSessionIdleTimer";
+import { AuthContext } from "../authentication/auth/context";
+import { AuthenticationContext } from "../authentication/authentication/context";
+import { useNextAuthService } from "./hooks/useNextAuthService";
+import { NextAuthAuthenticationProviderProps } from "./types";
+
+export function NextAuthAuthenticationProvider({
+ children,
+ refetchInterval = 0,
+ session,
+ timeout,
+}: NextAuthAuthenticationProviderProps): JSX.Element {
+ const authReducer = useAuthReducer();
+ const authenticationReducer = useAuthenticationReducer();
+ const service = useNextAuthService();
+ const { authDispatch, authState } = authReducer;
+ const { isAuthenticated } = authState;
+ const { callbackUrl } = useSessionCallbackUrl();
+ useSessionIdleTimer({
+ crossTab: true,
+ disabled: !isAuthenticated,
+ onIdle: () => {
+ service.requestLogout({ callbackUrl, redirect: true });
+ },
+ timeout,
+ });
+ useSessionAuth({ authReducer, authenticationReducer });
+ return (
+
+
+
+ {children}
+
+
+
+ );
+}
diff --git a/src/providers/nextAuthAuthentication/service/service.ts b/src/providers/nextAuthAuthentication/service/service.ts
new file mode 100644
index 00000000..419cbe66
--- /dev/null
+++ b/src/providers/nextAuthAuthentication/service/service.ts
@@ -0,0 +1,14 @@
+import { signIn, SignInOptions, signOut, SignOutParams } from "next-auth/react";
+import { ProviderId } from "../../authentication/common/types";
+
+export const service = {
+ login: (providerId: ProviderId, options?: SignInOptions): void => {
+ signIn(providerId, options).catch((e) => console.error(e));
+ },
+ logout: (options?: SignOutParams): void => {
+ signOut({
+ callbackUrl: options?.callbackUrl,
+ redirect: options?.redirect || false,
+ }).catch((e) => console.error(e));
+ },
+};
diff --git a/src/providers/nextAuthAuthentication/types.ts b/src/providers/nextAuthAuthentication/types.ts
new file mode 100644
index 00000000..02239de5
--- /dev/null
+++ b/src/providers/nextAuthAuthentication/types.ts
@@ -0,0 +1,9 @@
+import { Session } from "next-auth";
+import { ReactNode } from "react";
+
+export interface NextAuthAuthenticationProviderProps {
+ children: ReactNode | ReactNode[];
+ refetchInterval?: number;
+ session?: Session | null;
+ timeout?: number;
+}
diff --git a/src/routes/constants.ts b/src/routes/constants.ts
new file mode 100644
index 00000000..5527a210
--- /dev/null
+++ b/src/routes/constants.ts
@@ -0,0 +1,3 @@
+export const ROUTE = {
+ LOGIN: "/login",
+};
diff --git a/src/styles/common/mui/button.ts b/src/styles/common/mui/button.ts
new file mode 100644
index 00000000..58595080
--- /dev/null
+++ b/src/styles/common/mui/button.ts
@@ -0,0 +1,20 @@
+import { ButtonProps } from "@mui/material";
+
+export const COLOR: Record = {
+ ERROR: "error",
+ INFO: "info",
+ INHERIT: "inherit",
+ PRIMARY: "primary",
+ SECONDARY: "secondary",
+ SUCCESS: "success",
+ WARNING: "warning",
+};
+
+export const VARIANT: Record = {
+ ACTIVE_NAV: "activeNav",
+ BACK_NAV: "backNav",
+ CONTAINED: "contained",
+ NAV: "nav",
+ OUTLINED: "outlined",
+ TEXT: "text",
+};
diff --git a/src/styles/common/mui/paper.ts b/src/styles/common/mui/paper.ts
new file mode 100644
index 00000000..1eaaeac6
--- /dev/null
+++ b/src/styles/common/mui/paper.ts
@@ -0,0 +1,11 @@
+import { PaperProps } from "@mui/material";
+
+export const VARIANT: Record = {
+ ELEVATION: "elevation",
+ FOOTER: "footer",
+ MENU: "menu",
+ OUTLINED: "outlined",
+ PANEL: "panel",
+ SEARCH_BAR: "searchbar",
+ TABLE: "table",
+};
diff --git a/src/styles/common/mui/popover.ts b/src/styles/common/mui/popover.ts
new file mode 100644
index 00000000..6f1364cf
--- /dev/null
+++ b/src/styles/common/mui/popover.ts
@@ -0,0 +1,19 @@
+import { PopoverOrigin } from "@mui/material";
+
+export const POPOVER_ORIGIN_HORIZONTAL: Record<
+ string,
+ PopoverOrigin["horizontal"]
+> = {
+ CENTER: "center",
+ LEFT: "left",
+ RIGHT: "right",
+};
+
+export const POPOVER_ORIGIN_VERTICAL: Record<
+ string,
+ PopoverOrigin["vertical"]
+> = {
+ BOTTOM: "bottom",
+ CENTER: "center",
+ TOP: "top",
+};
diff --git a/src/theme/common/entities.ts b/src/theme/common/entities.ts
new file mode 100644
index 00000000..f806b189
--- /dev/null
+++ b/src/theme/common/entities.ts
@@ -0,0 +1,7 @@
+export interface BaseComponentProps {
+ className?: string;
+}
+
+export interface TrackingComponentProps {
+ trackingId?: string;
+}
diff --git a/src/views/LoginView/loginView.tsx b/src/views/LoginView/loginView.tsx
index bcc8b6a1..434a8a5d 100644
--- a/src/views/LoginView/loginView.tsx
+++ b/src/views/LoginView/loginView.tsx
@@ -1,18 +1,24 @@
+import { ClientSafeProvider } from "next-auth/react";
import React from "react";
import { Login } from "../../components/Login/login";
-import { useAuthenticationConfig } from "../../hooks/useAuthenticationConfig";
+import { useAuthenticationConfig } from "../../hooks/authentication/config/useAuthenticationConfig";
-export const LoginView = (): JSX.Element => {
- const { googleGISAuthConfig, termsOfService, text, title, warning } =
- useAuthenticationConfig();
+export interface LoginViewProps {
+ providers?: ClientSafeProvider[];
+}
+export const LoginView = ({
+ providers,
+}: LoginViewProps): JSX.Element | null => {
+ const authConfig = useAuthenticationConfig();
+ if (!authConfig) return null;
return (
);
};
diff --git a/tests/authentication.test.ts b/tests/authentication.test.ts
deleted file mode 100644
index 358f9d63..00000000
--- a/tests/authentication.test.ts
+++ /dev/null
@@ -1,142 +0,0 @@
-import { jest } from "@jest/globals";
-import { LOGIN_STATUS_NOT_STARTED } from "../src/hooks/useAuthentication/common/constants";
-import {
- LoginStatus,
- REQUEST_STATUS,
-} from "../src/hooks/useAuthentication/common/entities";
-import { GoogleResponse } from "../src/hooks/useAuthentication/useFetchGoogleProfile";
-import { TerraResponse } from "../src/hooks/useAuthentication/useFetchTerraProfile";
-import { TerraTermsOfServiceResponse } from "../src/hooks/useAuthentication/useFetchTerraTermsOfService";
-
-jest.unstable_mockModule("react-idle-timer", () => ({
- useIdleTimer: jest.fn(),
-}));
-
-const { shouldReleaseToken } = await import("../src/providers/authentication");
-
-describe("authentication", () => {
- // Boolean constants.
- const IS_NOT_SUCCESS = false;
- const IS_NOT_SUPPORTED = false;
- const IS_SUCCESS = true;
- const IS_SUPPORTED = true;
- // Response objects.
- const GOOGLE_RESPONSE = {} as GoogleResponse;
- const TERRA_RESPONSE = {} as TerraResponse;
- const TERRA_TOS_RESPONSE = {} as TerraTermsOfServiceResponse;
- // Login statuses - not started, not supported.
- const LOGIN_STATUS_NOT_STARTED_NOT_SUPPORTED_TERRA: LoginStatus =
- {
- isSuccess: IS_NOT_SUCCESS,
- isSupported: IS_NOT_SUPPORTED,
- requestStatus: REQUEST_STATUS.NOT_STARTED,
- response: TERRA_RESPONSE,
- };
- const LOGIN_STATUS_NOT_STARTED_NOT_SUPPORTED_TERRA_TOS: LoginStatus =
- {
- isSuccess: IS_NOT_SUCCESS,
- isSupported: IS_NOT_SUPPORTED,
- requestStatus: REQUEST_STATUS.NOT_STARTED,
- response: TERRA_TOS_RESPONSE,
- };
- // Login statuses - not started, supported.
- const LOGIN_STATUS_NOT_STARTED_SUPPORTED_TERRA =
- LOGIN_STATUS_NOT_STARTED as LoginStatus;
- const LOGIN_STATUS_NOT_STARTED_SUPPORTED_TERRA_TOS =
- LOGIN_STATUS_NOT_STARTED as LoginStatus;
- const LOGIN_STATUS_NOT_STARTED_SUPPORTED_USER_PROFILE =
- LOGIN_STATUS_NOT_STARTED as LoginStatus;
- // Login statuses - completed, not successful.
- const LOGIN_STATUS_COMPLETED_NOT_SUCCESS_TERRA_TOS: LoginStatus =
- {
- isSuccess: IS_NOT_SUCCESS,
- isSupported: IS_SUPPORTED,
- requestStatus: REQUEST_STATUS.COMPLETED,
- response: TERRA_TOS_RESPONSE,
- };
- const LOGIN_STATUS_COMPLETED_NOT_SUCCESS_USER_PROFILE: LoginStatus =
- {
- isSuccess: IS_NOT_SUCCESS,
- isSupported: IS_SUPPORTED,
- requestStatus: REQUEST_STATUS.COMPLETED,
- response: GOOGLE_RESPONSE,
- };
- // Login statuses - completed, successful.
- const LOGIN_STATUS_COMPLETED_SUCCESS_TERRA: LoginStatus = {
- isSuccess: IS_SUCCESS,
- isSupported: IS_SUPPORTED,
- requestStatus: REQUEST_STATUS.COMPLETED,
- response: TERRA_RESPONSE,
- };
- const LOGIN_STATUS_COMPLETED_SUCCESS_TERRA_TOS: LoginStatus =
- {
- isSuccess: IS_SUCCESS,
- isSupported: IS_SUPPORTED,
- requestStatus: REQUEST_STATUS.COMPLETED,
- response: TERRA_TOS_RESPONSE,
- };
- const LOGIN_STATUS_COMPLETED_SUCCESS_USER_PROFILE: LoginStatus =
- {
- isSuccess: IS_SUCCESS,
- isSupported: IS_SUPPORTED,
- requestStatus: REQUEST_STATUS.COMPLETED,
- response: GOOGLE_RESPONSE,
- };
-
- describe("Should Release Token", () => {
- describe("shouldReleaseToken", () => {
- describe("Terra endpoint is configured", () => {
- test("login not started", () => {
- const releaseToken = shouldReleaseToken(
- LOGIN_STATUS_NOT_STARTED_SUPPORTED_USER_PROFILE,
- LOGIN_STATUS_NOT_STARTED_SUPPORTED_TERRA,
- LOGIN_STATUS_NOT_STARTED_SUPPORTED_TERRA_TOS
- );
- expect(releaseToken).toBeFalsy();
- });
- test("login completed and Terra terms of service is not successful", () => {
- const releaseToken = shouldReleaseToken(
- LOGIN_STATUS_COMPLETED_SUCCESS_USER_PROFILE,
- LOGIN_STATUS_COMPLETED_SUCCESS_TERRA,
- LOGIN_STATUS_COMPLETED_NOT_SUCCESS_TERRA_TOS
- );
- expect(releaseToken).toBeFalsy();
- });
- test("login completed and Terra terms of service is successful", () => {
- const releaseToken = shouldReleaseToken(
- LOGIN_STATUS_COMPLETED_SUCCESS_USER_PROFILE,
- LOGIN_STATUS_COMPLETED_SUCCESS_TERRA,
- LOGIN_STATUS_COMPLETED_SUCCESS_TERRA_TOS
- );
- expect(releaseToken).toBeTruthy();
- });
- });
- describe("Terra endpoint is not configured", () => {
- test("login not started", () => {
- const releaseToken = shouldReleaseToken(
- LOGIN_STATUS_NOT_STARTED_SUPPORTED_USER_PROFILE,
- LOGIN_STATUS_NOT_STARTED_NOT_SUPPORTED_TERRA,
- LOGIN_STATUS_NOT_STARTED_NOT_SUPPORTED_TERRA_TOS
- );
- expect(releaseToken).toBeFalsy();
- });
- test("user profile is not successful", () => {
- const releaseToken = shouldReleaseToken(
- LOGIN_STATUS_COMPLETED_NOT_SUCCESS_USER_PROFILE,
- LOGIN_STATUS_NOT_STARTED_NOT_SUPPORTED_TERRA,
- LOGIN_STATUS_NOT_STARTED_NOT_SUPPORTED_TERRA_TOS
- );
- expect(releaseToken).toBeFalsy();
- });
- test("user profile is successful", () => {
- const releaseToken = shouldReleaseToken(
- LOGIN_STATUS_COMPLETED_SUCCESS_USER_PROFILE,
- LOGIN_STATUS_NOT_STARTED_NOT_SUPPORTED_TERRA,
- LOGIN_STATUS_NOT_STARTED_NOT_SUPPORTED_TERRA_TOS
- );
- expect(releaseToken).toBeTruthy();
- });
- });
- });
- });
-});
diff --git a/tests/getProfileStatus.test.ts b/tests/getProfileStatus.test.ts
new file mode 100644
index 00000000..3f08a651
--- /dev/null
+++ b/tests/getProfileStatus.test.ts
@@ -0,0 +1,290 @@
+import {
+ LoginStatus,
+ REQUEST_STATUS,
+} from "../src/providers/authentication/terra/hooks/common/entities";
+import { TerraNIHResponse } from "../src/providers/authentication/terra/hooks/useFetchTerraNIHProfile";
+import { TerraResponse } from "../src/providers/authentication/terra/hooks/useFetchTerraProfile";
+import { TerraTermsOfServiceResponse } from "../src/providers/authentication/terra/hooks/useFetchTerraTermsOfService";
+import { TERRA_PROFILE_STATUS } from "../src/providers/authentication/terra/types";
+import { getProfileStatus } from "../src/providers/authentication/terra/utils";
+
+const LOGIN_STATUS_NIH_COMPLETED: LoginStatus = {
+ isSuccess: true,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.COMPLETED,
+ response: undefined,
+};
+
+const LOGIN_STATUS_TERRA_COMPLETED: LoginStatus = {
+ isSuccess: true,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.COMPLETED,
+ response: undefined,
+};
+
+const LOGIN_STATUS_TOS_COMPLETED: LoginStatus = {
+ isSuccess: true,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.COMPLETED,
+ response: undefined,
+};
+
+const LOGIN_STATUS_TOS_COMPLETED_UNSUCCESSFUL: LoginStatus =
+ {
+ isSuccess: false,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.COMPLETED,
+ response: undefined,
+ };
+
+const LOGIN_STATUS_NIH_NOT_STARTED: LoginStatus = {
+ isSuccess: false,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.NOT_STARTED,
+ response: undefined,
+};
+
+const LOGIN_STATUS_TERRA_NOT_STARTED: LoginStatus = {
+ isSuccess: false,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.NOT_STARTED,
+ response: undefined,
+};
+
+const LOGIN_STATUS_TOS_NOT_STARTED: LoginStatus = {
+ isSuccess: false,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.NOT_STARTED,
+ response: undefined,
+};
+
+const LOGIN_STATUS_NIH_PENDING: LoginStatus = {
+ isSuccess: false,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.PENDING,
+ response: undefined,
+};
+
+const LOGIN_STATUS_TERRA_PENDING: LoginStatus = {
+ isSuccess: false,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.PENDING,
+ response: undefined,
+};
+
+const LOGIN_STATUS_TOS_PENDING: LoginStatus = {
+ isSuccess: false,
+ isSupported: true,
+ requestStatus: REQUEST_STATUS.PENDING,
+ response: undefined,
+};
+
+const LOGIN_STATUS_NIH_UNSUPPORTED: LoginStatus = {
+ isSuccess: false,
+ isSupported: false,
+ requestStatus: REQUEST_STATUS.NOT_STARTED,
+ response: undefined,
+};
+
+const LOGIN_STATUS_TERRA_UNSUPPORTED: LoginStatus = {
+ isSuccess: false,
+ isSupported: false,
+ requestStatus: REQUEST_STATUS.NOT_STARTED,
+ response: undefined,
+};
+
+const LOGIN_STATUS_TOS_UNSUPPORTED: LoginStatus = {
+ isSuccess: false,
+ isSupported: false,
+ requestStatus: REQUEST_STATUS.NOT_STARTED,
+ response: undefined,
+};
+
+describe("getProfileStatus", () => {
+ test("not authenticated, services not started", () => {
+ expect(
+ getProfileStatus(
+ false,
+ LOGIN_STATUS_NIH_NOT_STARTED,
+ LOGIN_STATUS_TERRA_NOT_STARTED,
+ LOGIN_STATUS_TOS_NOT_STARTED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("not authenticated, services unsupported", () => {
+ expect(
+ getProfileStatus(
+ false,
+ LOGIN_STATUS_NIH_UNSUPPORTED,
+ LOGIN_STATUS_TERRA_UNSUPPORTED,
+ LOGIN_STATUS_TOS_UNSUPPORTED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("not authenticated, services completed", () => {
+ expect(
+ getProfileStatus(
+ false,
+ LOGIN_STATUS_NIH_COMPLETED,
+ LOGIN_STATUS_TERRA_COMPLETED,
+ LOGIN_STATUS_TOS_COMPLETED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("authenticated, services not started", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_NOT_STARTED,
+ LOGIN_STATUS_TERRA_NOT_STARTED,
+ LOGIN_STATUS_TOS_NOT_STARTED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("authenticated, services pending", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_PENDING,
+ LOGIN_STATUS_TERRA_PENDING,
+ LOGIN_STATUS_TOS_PENDING
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("authenticated, nih pending, other services completed", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_PENDING,
+ LOGIN_STATUS_TERRA_COMPLETED,
+ LOGIN_STATUS_TOS_COMPLETED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("authenticated, terra pending, other services completed", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_COMPLETED,
+ LOGIN_STATUS_TERRA_PENDING,
+ LOGIN_STATUS_TOS_COMPLETED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("authenticated, tos pending, other services completed", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_COMPLETED,
+ LOGIN_STATUS_TERRA_COMPLETED,
+ LOGIN_STATUS_TOS_PENDING
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("authenticated, nih completed, terra pending, tos not started", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_COMPLETED,
+ LOGIN_STATUS_TERRA_PENDING,
+ LOGIN_STATUS_TOS_NOT_STARTED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("authenticated, nih not started, terra completed, tos pending", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_NOT_STARTED,
+ LOGIN_STATUS_TERRA_COMPLETED,
+ LOGIN_STATUS_TOS_PENDING
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("authenticated, nih pending, terra not started, tos completed", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_PENDING,
+ LOGIN_STATUS_TERRA_NOT_STARTED,
+ LOGIN_STATUS_TOS_COMPLETED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.PENDING);
+ });
+
+ test("authenticated, services unsupported", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_UNSUPPORTED,
+ LOGIN_STATUS_TERRA_UNSUPPORTED,
+ LOGIN_STATUS_TOS_UNSUPPORTED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.AUTHENTICATED);
+ });
+
+ test("authenticated, services completed, tos unsuccessful", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_COMPLETED,
+ LOGIN_STATUS_TERRA_COMPLETED,
+ LOGIN_STATUS_TOS_COMPLETED_UNSUCCESSFUL
+ )
+ ).toBe(TERRA_PROFILE_STATUS.UNAUTHENTICATED);
+ });
+
+ test("authenticated, nih unsupported, terra completed, tos completed unsuccessfully", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_UNSUPPORTED,
+ LOGIN_STATUS_TERRA_COMPLETED,
+ LOGIN_STATUS_TOS_COMPLETED_UNSUCCESSFUL
+ )
+ ).toBe(TERRA_PROFILE_STATUS.UNAUTHENTICATED);
+ });
+
+ test("authenticated, nih unsupported, other services completed", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_UNSUPPORTED,
+ LOGIN_STATUS_TERRA_COMPLETED,
+ LOGIN_STATUS_TOS_COMPLETED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.AUTHENTICATED);
+ });
+
+ test("authenticated, terra completed, other services unsupported", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_UNSUPPORTED,
+ LOGIN_STATUS_TERRA_COMPLETED,
+ LOGIN_STATUS_TOS_UNSUPPORTED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.UNAUTHENTICATED);
+ });
+
+ test("authenticated, services completed", () => {
+ expect(
+ getProfileStatus(
+ true,
+ LOGIN_STATUS_NIH_COMPLETED,
+ LOGIN_STATUS_TERRA_COMPLETED,
+ LOGIN_STATUS_TOS_COMPLETED
+ )
+ ).toBe(TERRA_PROFILE_STATUS.AUTHENTICATED);
+ });
+});
diff --git a/tests/terraProfileProvider.test.tsx b/tests/terraProfileProvider.test.tsx
new file mode 100644
index 00000000..10a3ad13
--- /dev/null
+++ b/tests/terraProfileProvider.test.tsx
@@ -0,0 +1,121 @@
+import { jest } from "@jest/globals";
+import { render } from "@testing-library/react";
+import React from "react";
+import { DEFAULT_AUTHENTICATION_STATE } from "../src/providers/authentication/authentication/constants";
+import { authenticationComplete } from "../src/providers/authentication/authentication/dispatch";
+import { DEFAULT_CREDENTIALS_STATE } from "../src/providers/authentication/credentials/constants";
+import { updateCredentials } from "../src/providers/authentication/credentials/dispatch";
+
+const TOKEN = "test-token";
+
+const PROFILE_PENDING = {
+ isComplete: false,
+ isProfileActive: false,
+};
+
+const PROFILE_SETTLED_ACTIVE = {
+ isComplete: true,
+ isProfileActive: true,
+};
+
+const PROFILE_SETTLED_INACTIVE = {
+ isComplete: true,
+ isProfileActive: false,
+};
+
+jest.unstable_mockModule(
+ "../src/providers/authentication/authentication/hook",
+ () => ({
+ useAuthentication: jest.fn(),
+ })
+);
+jest.unstable_mockModule(
+ "../src/providers/authentication/credentials/hook",
+ () => ({
+ useCredentials: jest.fn(),
+ })
+);
+jest.unstable_mockModule(
+ "../src/providers/authentication/terra/hooks/useFetchProfiles",
+ () => ({
+ useFetchProfiles: jest.fn(),
+ })
+);
+
+const { useAuthentication } = await import(
+ "../src/providers/authentication/authentication/hook"
+);
+const { useCredentials } = await import(
+ "../src/providers/authentication/credentials/hook"
+);
+const { useFetchProfiles } = await import(
+ "../src/providers/authentication/terra/hooks/useFetchProfiles"
+);
+const { TerraProfileProvider } = await import(
+ "../src/providers/authentication/terra/provider"
+);
+
+const MOCK_AUTHENTICATION_DISPATCH = jest.fn();
+const MOCK_CREDENTIALS_DISPATCH = jest.fn();
+const MOCK_USE_AUTHENTICATION = useAuthentication as jest.MockedFunction<
+ typeof useAuthentication
+>;
+const MOCK_USE_CREDENTIALS = useCredentials as jest.MockedFunction<
+ typeof useCredentials
+>;
+const MOCK_USE_FETCH_PROFILES = useFetchProfiles as jest.MockedFunction<
+ () => Partial>
+>;
+
+describe("TerraProfileProvider", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ MOCK_USE_AUTHENTICATION.mockReturnValue({
+ authenticationDispatch: MOCK_AUTHENTICATION_DISPATCH,
+ authenticationState: DEFAULT_AUTHENTICATION_STATE,
+ });
+ MOCK_USE_CREDENTIALS.mockReturnValue({
+ credentialsDispatch: MOCK_CREDENTIALS_DISPATCH,
+ credentialsState: DEFAULT_CREDENTIALS_STATE,
+ });
+ MOCK_USE_FETCH_PROFILES.mockReturnValue(PROFILE_PENDING);
+ });
+
+ it("does not dispatch actions when terra profile is incomplete", () => {
+ render(
+
+ Child Component
+
+ );
+ expect(MOCK_AUTHENTICATION_DISPATCH).not.toHaveBeenCalled();
+ expect(MOCK_CREDENTIALS_DISPATCH).not.toHaveBeenCalled();
+ });
+
+ it("calls authenticationComplete dispatch when terra profile is SETTLED and INACTIVE", () => {
+ MOCK_USE_FETCH_PROFILES.mockReturnValue(PROFILE_SETTLED_INACTIVE);
+ render(
+
+ Child Component
+
+ );
+ expect(MOCK_AUTHENTICATION_DISPATCH).toHaveBeenCalledWith(
+ authenticationComplete()
+ );
+ expect(MOCK_CREDENTIALS_DISPATCH).not.toHaveBeenCalled();
+ });
+
+ it("dispatches authenticationComplete and updateCredentials when terra profile is SETTLED and ACTIVE", () => {
+ MOCK_USE_FETCH_PROFILES.mockReturnValue(PROFILE_SETTLED_ACTIVE);
+ render(
+
+ Child Component
+
+ );
+ expect(MOCK_AUTHENTICATION_DISPATCH).toHaveBeenCalledWith(
+ authenticationComplete()
+ );
+ expect(MOCK_CREDENTIALS_DISPATCH).toHaveBeenCalledWith(
+ updateCredentials(TOKEN)
+ );
+ });
+});
diff --git a/tests/transformRoute.test.ts b/tests/transformRoute.test.ts
new file mode 100644
index 00000000..36f61dd0
--- /dev/null
+++ b/tests/transformRoute.test.ts
@@ -0,0 +1,21 @@
+import { transformRoute } from "../src/hooks/authentication/session/useSessionActive";
+
+describe("transformRoute", () => {
+ it("should return the first non-login route without the inactivity param", () => {
+ const routes = ["/login", "/route1?inactivityTimeout=true", "/route2"];
+ const result = transformRoute(routes);
+ expect(result).toBe("/route1");
+ });
+
+ it("should remove the inactivity param from the route", () => {
+ const routes = ["/route1?inactivityTimeout=true"];
+ const result = transformRoute(routes);
+ expect(result).toBe("/route1");
+ });
+
+ it("should return undefined if all routes are login routes", () => {
+ const routes = ["/login"];
+ const result = transformRoute(routes);
+ expect(result).toBeUndefined();
+ });
+});
diff --git a/tests/useAuthenticationStatus.test.ts b/tests/useAuthenticationStatus.test.ts
deleted file mode 100644
index 74df4366..00000000
--- a/tests/useAuthenticationStatus.test.ts
+++ /dev/null
@@ -1,155 +0,0 @@
-import {
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_NOT_SUPPORTED,
-} from "../src/hooks/useAuthentication/common/constants";
-import {
- AUTHENTICATION_STATUS,
- LoginResponse,
- LoginStatus,
- REQUEST_STATUS,
-} from "../src/hooks/useAuthentication/common/entities";
-import { getAuthenticationStatus } from "../src/hooks/useAuthentication/useAuthenticationStatus";
-
-const TEST_LOGIN_IS_COMPLETE = "login is complete";
-const TEST_LOGIN_NOT_STARTED = "login not started";
-const TEST_USER_PROFILE_AND_TERRA_AND_TOS_IS_COMPLETE =
- "user profile, terra and terms of service is complete";
-const TEST_USER_PROFILE_AND_TERRA_IS_COMPLETE =
- "user profile and terra is complete";
-const TEST_USER_PROFILE_IS_COMPLETE = "user profile is complete";
-
-describe("useAuthenticationStatus", () => {
- // Login statuses - completed, successful.
- const LOGIN_STATUS_COMPLETED_SUCCESS: LoginStatus = {
- isSuccess: true,
- isSupported: true,
- requestStatus: REQUEST_STATUS.COMPLETED,
- response: {} as LoginResponse,
- };
-
- describe("Calculate Authentication Status", () => {
- describe("getAuthenticationStatus", () => {
- describe("endpoints are configured", () => {
- test(TEST_LOGIN_NOT_STARTED, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_NOT_STARTED,
- ]);
- expect(authenticationStatus).toEqual(
- AUTHENTICATION_STATUS.INCOMPLETE
- );
- });
- test(TEST_USER_PROFILE_IS_COMPLETE, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- ]);
- expect(authenticationStatus).toEqual(
- AUTHENTICATION_STATUS.INCOMPLETE
- );
- });
- test(TEST_USER_PROFILE_AND_TERRA_IS_COMPLETE, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- ]);
- expect(authenticationStatus).toEqual(
- AUTHENTICATION_STATUS.INCOMPLETE
- );
- });
- test(TEST_USER_PROFILE_AND_TERRA_AND_TOS_IS_COMPLETE, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- ]);
- expect(authenticationStatus).toEqual(
- AUTHENTICATION_STATUS.INCOMPLETE
- );
- });
- test(TEST_LOGIN_IS_COMPLETE, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_COMPLETED_SUCCESS,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- ]);
- expect(authenticationStatus).toEqual(AUTHENTICATION_STATUS.COMPLETED);
- });
- });
- describe("NIH endpoint is not configured", () => {
- test(TEST_LOGIN_NOT_STARTED, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_NOT_STARTED,
- ]);
- expect(authenticationStatus).toEqual(
- AUTHENTICATION_STATUS.INCOMPLETE
- );
- });
- test(TEST_USER_PROFILE_IS_COMPLETE, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- ]);
- expect(authenticationStatus).toEqual(
- AUTHENTICATION_STATUS.INCOMPLETE
- );
- });
- test(TEST_USER_PROFILE_AND_TERRA_IS_COMPLETE, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- LOGIN_STATUS_NOT_STARTED,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- ]);
- expect(authenticationStatus).toEqual(
- AUTHENTICATION_STATUS.INCOMPLETE
- );
- });
- test(TEST_LOGIN_IS_COMPLETE, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- ]);
- expect(authenticationStatus).toEqual(AUTHENTICATION_STATUS.COMPLETED);
- });
- });
- describe("Terra endpoint is not configured", () => {
- test(TEST_LOGIN_NOT_STARTED, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_NOT_STARTED,
- ]);
- expect(authenticationStatus).toEqual(
- AUTHENTICATION_STATUS.INCOMPLETE
- );
- });
- test(TEST_LOGIN_IS_COMPLETE, () => {
- const authenticationStatus = getAuthenticationStatus([
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_NOT_SUPPORTED,
- LOGIN_STATUS_COMPLETED_SUCCESS,
- ]);
- expect(authenticationStatus).toEqual(AUTHENTICATION_STATUS.COMPLETED);
- });
- });
- });
- });
-});
diff --git a/tests/useRouteHistory.test.ts b/tests/useRouteHistory.test.ts
new file mode 100644
index 00000000..ad81f177
--- /dev/null
+++ b/tests/useRouteHistory.test.ts
@@ -0,0 +1,97 @@
+import { jest } from "@jest/globals";
+import { act, renderHook } from "@testing-library/react";
+import Router, { NextRouter } from "next/router";
+
+const ROOT_PATH = "/";
+const ROUTES = ["/route1", "/route2", "/route3", "/route4"];
+
+jest.unstable_mockModule("next/router", () => {
+ return {
+ ...jest.requireActual("next/router"),
+ useRouter: jest.fn(),
+ };
+});
+jest.unstable_mockModule("../src/hooks/useRouteRoot", () => ({
+ useRouteRoot: jest.fn(),
+}));
+
+const { useRouter } = await import("next/router");
+const { useRouteRoot } = await import("../src/hooks/useRouteRoot");
+const { useRouteHistory } = await import("../src/hooks/useRouteHistory");
+
+const MOCK_USE_ROUTER = useRouter as jest.MockedFunction<
+ () => Partial
+>;
+const MOCK_USE_ROUTE_ROOT = useRouteRoot as jest.MockedFunction<
+ typeof useRouteRoot
+>;
+
+describe("useRouteHistory", () => {
+ beforeEach(() => {
+ MOCK_USE_ROUTE_ROOT.mockReset();
+ MOCK_USE_ROUTER.mockReset();
+ MOCK_USE_ROUTER.mockReturnValue({
+ asPath: ROUTES[0],
+ });
+ MOCK_USE_ROUTE_ROOT.mockReturnValue(ROOT_PATH);
+ });
+
+ test("returns the root path when no previous route exists", () => {
+ const { result } = renderHook(() => useRouteHistory());
+ expect(result.current.callbackUrl()).toBe(ROOT_PATH);
+ });
+
+ test("updates history on route change", () => {
+ const { result } = renderHook(() => useRouteHistory());
+ act(() => {
+ Router.events.emit("routeChangeComplete", ROUTES[1]);
+ Router.events.emit("routeChangeComplete", ROUTES[2]);
+ });
+ expect(result.current.callbackUrl()).toBe(ROUTES[1]);
+ });
+
+ test("does not add duplicate routes to history", () => {
+ const { result } = renderHook(() => useRouteHistory());
+ act(() => {
+ Router.events.emit("routeChangeComplete", ROUTES[2]);
+ Router.events.emit("routeChangeComplete", ROUTES[2]);
+ });
+ expect(result.current.callbackUrl()).toBe(ROUTES[0]);
+ });
+
+ test("limits history length to maxHistory", () => {
+ const { result } = renderHook(() => useRouteHistory(2));
+ act(() => {
+ Router.events.emit("routeChangeComplete", ROUTES[1]);
+ Router.events.emit("routeChangeComplete", ROUTES[2]);
+ Router.events.emit("routeChangeComplete", ROUTES[3]);
+ });
+ // Use `callbackUrl` with a transform function to capture the full history.
+ let history;
+ result.current.callbackUrl((routes) => {
+ history = routes;
+ return ROOT_PATH;
+ });
+ // After emitting the routes, the history stack is [ROUTES[3], ROUTES[2].
+ expect(history).toHaveLength(2);
+ expect(history).toEqual([ROUTES[3], ROUTES[2]]);
+ });
+
+ test("uses transform function if provided", () => {
+ const { result } = renderHook(() => useRouteHistory(4));
+ act(() => {
+ Router.events.emit("routeChangeComplete", ROUTES[1]);
+ Router.events.emit("routeChangeComplete", ROUTES[2]);
+ Router.events.emit("routeChangeComplete", ROUTES[3]);
+ });
+ // After emitting the routes, the history stack is [ROUTES[3], ROUTES[2], ROUTES[1], ROUTES[0]].
+ const transformFn = (routes: string[]): string => routes[2];
+ expect(result.current.callbackUrl(transformFn)).toBe(ROUTES[1]);
+ });
+
+ test("returns root path when transform function is provided but history stack lacks sufficient entries", () => {
+ const { result } = renderHook(() => useRouteHistory());
+ const transformFn = (routes: string[]): string => routes[2];
+ expect(result.current.callbackUrl(transformFn)).toBe(ROOT_PATH);
+ });
+});
diff --git a/tests/useSessionActive.test.ts b/tests/useSessionActive.test.ts
new file mode 100644
index 00000000..606bc190
--- /dev/null
+++ b/tests/useSessionActive.test.ts
@@ -0,0 +1,74 @@
+import { jest } from "@jest/globals";
+import { renderHook } from "@testing-library/react";
+import { TransformRouteFn } from "../src/hooks/useRouteHistory";
+import {
+ AUTH_STATUS,
+ AuthState,
+} from "../src/providers/authentication/auth/types";
+
+const AUTH_STATE_AUTHENTICATED_SETTLED: AuthState = {
+ isAuthenticated: true,
+ status: AUTH_STATUS.SETTLED,
+};
+
+const AUTH_STATE_PENDING: AuthState = {
+ isAuthenticated: false,
+ status: AUTH_STATUS.PENDING,
+};
+
+const AUTH_STATE_UNAUTHENTICATED_SETTLED: AuthState = {
+ isAuthenticated: false,
+ status: AUTH_STATUS.SETTLED,
+};
+
+const ROOT_PATH = "/";
+const ROUTES = ["/login", "/route1", "/route2"];
+
+jest.unstable_mockModule("next/router", () => {
+ return {
+ ...jest.requireActual("next/router"),
+ default: {
+ push: jest.fn(),
+ },
+ };
+});
+jest.unstable_mockModule("../src/hooks/useRouteHistory", () => ({
+ useRouteHistory: jest.fn(),
+}));
+
+const Router = (await import("next/router")).default;
+const { useRouteHistory } = await import("../src/hooks/useRouteHistory");
+const { useSessionActive } = await import(
+ "../src/hooks/authentication/session/useSessionActive"
+);
+
+const MOCK_USE_ROUTE_HISTORY = useRouteHistory as jest.MockedFunction<
+ typeof useRouteHistory
+>;
+
+describe("useSessionActive", () => {
+ beforeEach(() => {
+ MOCK_USE_ROUTE_HISTORY.mockReset();
+ MOCK_USE_ROUTE_HISTORY.mockReturnValue({
+ callbackUrl: jest.fn(
+ (transformFn?: TransformRouteFn | undefined) =>
+ transformFn?.(ROUTES) ?? ROOT_PATH
+ ),
+ });
+ });
+
+ test("does not redirect if auth status is PENDING", () => {
+ renderHook(() => useSessionActive(AUTH_STATE_PENDING));
+ expect(Router.push).not.toHaveBeenCalled();
+ });
+
+ test("redirects if auth status is SETTLED", () => {
+ renderHook(() => useSessionActive(AUTH_STATE_UNAUTHENTICATED_SETTLED));
+ expect(Router.push).toHaveBeenCalled();
+ });
+
+ test("redirects to callback URL if auth status is SETTLED", () => {
+ renderHook(() => useSessionActive(AUTH_STATE_AUTHENTICATED_SETTLED));
+ expect(Router.push).toHaveBeenCalledWith(ROUTES[1]);
+ });
+});
diff --git a/tests/useSessionAuth.test.ts b/tests/useSessionAuth.test.ts
new file mode 100644
index 00000000..bae48ca9
--- /dev/null
+++ b/tests/useSessionAuth.test.ts
@@ -0,0 +1,91 @@
+import { renderHook } from "@testing-library/react";
+import { useAuthReducer } from "../src/hooks/authentication/auth/useAuthReducer";
+import { useAuthenticationReducer } from "../src/hooks/authentication/authentication/useAuthenticationReducer";
+import { useSessionAuth } from "../src/hooks/authentication/session/useSessionAuth";
+import {
+ AUTH_STATUS,
+ AuthState,
+} from "../src/providers/authentication/auth/types";
+import {
+ AUTHENTICATION_STATUS,
+ AuthenticationState,
+} from "../src/providers/authentication/authentication/types";
+
+describe("useSessionAuth", () => {
+ test("auth status SETTLED with no profile", async () => {
+ testAuthenticationState(
+ {
+ profile: undefined,
+ status: AUTHENTICATION_STATUS.SETTLED,
+ },
+ {
+ isAuthenticated: false,
+ status: AUTH_STATUS.SETTLED,
+ }
+ );
+ });
+
+ test("auth status PENDING with no profile", async () => {
+ testAuthenticationState(
+ {
+ profile: undefined,
+ status: AUTHENTICATION_STATUS.PENDING,
+ },
+ {
+ isAuthenticated: false,
+ status: AUTH_STATUS.PENDING,
+ }
+ );
+ });
+
+ test("auth status PENDING with profile", async () => {
+ testAuthenticationState(
+ {
+ profile: {
+ email: "test@example.com",
+ name: "Test",
+ },
+ status: AUTHENTICATION_STATUS.PENDING,
+ },
+ {
+ isAuthenticated: false,
+ status: AUTH_STATUS.PENDING,
+ }
+ );
+ });
+
+ test("auth status SETTLED with profile", async () => {
+ testAuthenticationState(
+ {
+ profile: {
+ email: "test@example.com",
+ name: "Test",
+ },
+ status: AUTHENTICATION_STATUS.SETTLED,
+ },
+ {
+ isAuthenticated: true,
+ status: AUTH_STATUS.SETTLED,
+ }
+ );
+ });
+});
+
+function testAuthenticationState(
+ authenticationState: AuthenticationState,
+ expectedAuthState: AuthState
+): void {
+ const { result: authenticationReducerResult } = renderHook(() =>
+ useAuthenticationReducer(authenticationState)
+ );
+ const { result: authReducerResult } = renderHook(() => useAuthReducer());
+
+ renderHook(() =>
+ useSessionAuth({
+ authReducer: authReducerResult.current,
+ authenticationReducer: authenticationReducerResult.current,
+ })
+ );
+
+ expect(authReducerResult.current.authState).toMatchObject(expectedAuthState);
+}