From 707e50e540c90abef2e811e13c8b663bd0dab626 Mon Sep 17 00:00:00 2001 From: Dimitris Karagiannis Date: Wed, 11 Oct 2023 19:34:15 +0300 Subject: [PATCH] feat: Update ocntext and useProducts hook --- src/authentication/context.test.tsx | 67 ++++++++++++------ src/authentication/context.tsx | 102 +++++++++++++++++++-------- src/hooks/useOrfiumProducts/index.ts | 23 ++++++ src/hooks/useOrfiumProducts/types.ts | 14 ++++ 4 files changed, 155 insertions(+), 51 deletions(-) create mode 100644 src/hooks/useOrfiumProducts/index.ts create mode 100644 src/hooks/useOrfiumProducts/types.ts diff --git a/src/authentication/context.test.tsx b/src/authentication/context.test.tsx index 9b9ded35..87886707 100644 --- a/src/authentication/context.test.tsx +++ b/src/authentication/context.test.tsx @@ -1,6 +1,6 @@ import { act, render, screen, waitFor } from '@testing-library/react'; import jwtDecode from 'jwt-decode'; -import React, { useEffect, useState } from 'react'; +import { useEffect, useState } from 'react'; // Auth0 custom error simulator. This extends a regular Error to match Auth0 Error. class CustomError extends Error { @@ -23,10 +23,11 @@ import { handleRedirectCallback as mockedHandleRedirectCallback, isAuthenticated, loginWithRedirect, - // @ts-ignore } from '../../__mocks__/@auth0/auth0-spa-js'; -import useOrganization from '../store/useOrganization'; -import useRequestToken from '../store/useRequestToken'; +import { orfiumIdBaseInstance } from '../request'; +import MockRequest from '../request/mock'; +import useOrganization from '../store/organizations'; +import useRequestToken from '../store/requestToken'; import { AuthenticationProvider, client, @@ -82,9 +83,28 @@ const TestingComponent = () => { }; describe('Context', () => { + const apiInstance = orfiumIdBaseInstance.instance; + const mock: MockRequest = new MockRequest(apiInstance); + beforeEach(() => { + mock.onGet('/products/').reply(200, [ + { + name: 'string', + organization_usage: 'string', + client_metadata: { + product_code: 'string', + }, + logo_url: 'string', + login_url: 'string', + }, + ]); + mockedGetTokenSilently.mockReset(); + }); + + afterEach(() => { // clear all mocks and mocked values jest.clearAllMocks(); + mock.reset(); mockedGetTokenSilently.mockReset(); getUser.mockReset(); loginWithRedirect.mockReset(); @@ -188,15 +208,16 @@ describe('Context', () => { // implement testing data setToken(testToken); setOrganizations(organizationList); - setSelectedOrganization(organizationList[0]); + setSelectedOrganization(organizationList[0].org_id); await logoutAuth(); const token = useRequestToken.getState().token; - const { organizations, selectedOrganization } = useOrganization.getState(); + const { organizations, organizationsList, selectedOrganization } = useOrganization.getState(); expect(token).toBe(undefined); - expect(organizations).toStrictEqual([]); - expect(selectedOrganization).toBe(undefined); + expect(organizations).toStrictEqual(null); + expect(organizationsList).toStrictEqual(null); + expect(selectedOrganization).toBe(null); }); }); @@ -212,21 +233,25 @@ describe('Context', () => { test('with cached results', async () => { const NEW_FAKE_EXPIRED_TOKEN = getNewFakeToken(); const setToken = useRequestToken.getState().setToken; + const setOrganizations = useOrganization.getState().setOrganizations; const setSelectedOrganization = useOrganization.getState().setSelectedOrganization; setToken(NEW_FAKE_EXPIRED_TOKEN); - setSelectedOrganization({ - org_id: 'org_WYZLEMyTm2xEbnbn', - display_name: 'test', - name: 'test', - can_administrate: true, - metadata: { - type: 'test', - product_codes: 'test', - }, - branding: { - logo_url: 'test', + setOrganizations([ + { + org_id: 'org_WYZLEMyTm2xEbnbn', + display_name: 'test', + name: 'test', + can_administrate: true, + metadata: { + type: 'test', + product_codes: 'test', + }, + branding: { + logo_url: 'test', + }, }, - }); + ]); + setSelectedOrganization('org_WYZLEMyTm2xEbnbn'); const { token, decodedToken } = await getTokenSilently(); @@ -372,7 +397,7 @@ describe('Context', () => { // implement testing data setOrganizations(organizationList); - setSelectedOrganization(organizationList[1]); + setSelectedOrganization(organizationList[1].org_id); Object.defineProperty(window, 'location', { value: new URL(`http://localhost:3000/?error=access_denied&error_description=whatever`), writable: true, diff --git a/src/authentication/context.tsx b/src/authentication/context.tsx index 45ae8fa2..f3279b2f 100644 --- a/src/authentication/context.tsx +++ b/src/authentication/context.tsx @@ -6,11 +6,11 @@ import { RedirectLoginOptions, } from '@auth0/auth0-spa-js'; import jwt_decode from 'jwt-decode'; -import React, { createContext, useEffect, useState } from 'react'; +import React, { createContext, useCallback, useEffect, useMemo, useState } from 'react'; import { useErrorHandler } from 'react-error-boundary'; - -import useOrganization from '../store/useOrganization'; -import useRequestToken from '../store/useRequestToken'; +import { useOrfiumProducts } from '../hooks/useOrfiumProducts'; +import useOrganization from '../store/organizations'; +import useRequestToken from '../store/requestToken'; import { config } from './config'; import { AuthenticationContextProps } from './types'; @@ -39,6 +39,10 @@ export const defaultContextValues: AuthenticationContextProps = { isAuthenticated: false, isLoading: false, user: undefined, + orfiumProducts: null, + organizations: [], + selectedOrganization: null, + switchOrganization: (__x) => {}, loginWithRedirect: () => Promise.resolve(), logout: () => Promise.resolve('logged out'), getAccessTokenSilently: () => Promise.resolve({ token: '', decodedToken: {} }), @@ -143,15 +147,27 @@ const AuthenticationProvider: React.FC = ({ children }) => { const [user, setUser] = useState>(); const [auth0Client, setAuth0Client] = useState(); const [isLoading, setIsLoading] = useState(true); + // const [products, setProducts] = useState(null); + // handleError is referentially stable, so it's safe to use as a dep in dep array + // https://github.com/bvaughn/react-error-boundary/blob/v3.1.4/src/index.tsx#L165C10-L165C18 const handleError = useErrorHandler(); const params = new URLSearchParams(window.location.search); const selectedOrganization = useOrganization((state) => state.selectedOrganization); const setSelectedOrganization = useOrganization((state) => state.setSelectedOrganization); - const organizations = useOrganization((state) => state.organizations); + const organizationsDict = useOrganization((state) => state.organizations); + const organizationsList = useOrganization((state) => state.organizationsList); const organization = params.get('organization') || selectedOrganization?.org_id; const invitation = params.get('invitation'); + const organizations = useMemo(() => { + if (organizationsDict && organizationsList) { + return organizationsList.map((x) => organizationsDict[x]); + } + + return []; + }, [organizationsDict, organizationsList]); + useEffect(() => { (async () => { try { @@ -195,33 +211,39 @@ const AuthenticationProvider: React.FC = ({ children }) => { })(); }, []); - const loginWithRedirect = async (o: RedirectLoginOptions) => { - try { - const client = await getAuth0Client(); - await client.loginWithRedirect(o); - } catch (error) { - return handleError(error); - } - }; + const loginWithRedirect = useCallback( + async (o: RedirectLoginOptions) => { + try { + const client = await getAuth0Client(); + await client.loginWithRedirect(o); + } catch (error) { + return handleError(error); + } + }, + [handleError] + ); - const getAccessTokenSilently = async (opts?: GetTokenSilentlyOptions) => { - try { - const result = await getTokenSilently(opts); + const getAccessTokenSilently = useCallback( + async (opts?: GetTokenSilentlyOptions) => { + try { + const result = await getTokenSilently(opts); - return result; - } catch (error: any) { - if (error?.error === 'login_required' || error?.error === 'consent_required') { - return loginWithRedirect({ - authorizationParams: { - organization: organization || undefined, - invitation: invitation || undefined, - }, - }); - } + return result; + } catch (error: any) { + if (error?.error === 'login_required' || error?.error === 'consent_required') { + return loginWithRedirect({ + authorizationParams: { + organization: organization || undefined, + invitation: invitation || undefined, + }, + }); + } - handleError(error); - } - }; + handleError(error); + } + }, + [handleError, invitation, loginWithRedirect, organization] + ); useEffect(() => { const searchParams = new URL(window.location.href).searchParams; @@ -230,7 +252,7 @@ const AuthenticationProvider: React.FC = ({ children }) => { if (error === 'access_denied') { const org = organizations[0]; - setSelectedOrganization(org); + setSelectedOrganization(org.org_id); loginWithRedirect({ authorizationParams: { organization: org?.org_id || undefined, @@ -248,6 +270,22 @@ const AuthenticationProvider: React.FC = ({ children }) => { } }, [auth0Client, isLoading, isAuthenticated]); + const switchOrganization = useCallback( + async function (orgID) { + const client = await getAuth0Client(); + await client.logout({ openUrl: false }); + await client.loginWithRedirect({ + authorizationParams: { + organization: orgID, + }, + }); + setSelectedOrganization(orgID); + }, + [setSelectedOrganization] + ); + + const orfiumProducts = useOrfiumProducts(selectedOrganization?.org_id); + return ( { loginWithRedirect, logout: logoutAuth, getAccessTokenSilently: (o?: GetTokenSilentlyOptions) => getAccessTokenSilently(o), + orfiumProducts, + organizations, + selectedOrganization, + switchOrganization, user, }} > diff --git a/src/hooks/useOrfiumProducts/index.ts b/src/hooks/useOrfiumProducts/index.ts new file mode 100644 index 00000000..7aceeec2 --- /dev/null +++ b/src/hooks/useOrfiumProducts/index.ts @@ -0,0 +1,23 @@ +// import { useQuery } from 'react-query'; +import { useEffect, useState } from 'react'; +import { orfiumIdBaseInstance } from '../../request'; +import { Product } from './types'; + +export function useOrfiumProducts(orgId: string | undefined) { + const [data, setData] = useState(null); + + useEffect(() => { + const { request, cancelTokenSource } = orfiumIdBaseInstance.createRequest({ + method: 'get', + url: '/products/', + }); + + request().then((resp) => { + setData(resp); + }); + + return cancelTokenSource.cancel; + }, [orgId]); + + return data; +} diff --git a/src/hooks/useOrfiumProducts/types.ts b/src/hooks/useOrfiumProducts/types.ts new file mode 100644 index 00000000..5e3e54d4 --- /dev/null +++ b/src/hooks/useOrfiumProducts/types.ts @@ -0,0 +1,14 @@ +export type ClientMetadata = { + product_code: string; +}; + +export type Product = { + client_id: string; + client_metadata: ClientMetadata; + grant_types: string | null; + icon_url: string; + login_url: string; + logo_url: string; + name: string; + organization_usage: string; +};