From 9037f96e23318e3509e0fe03d0e4940ba80a40a2 Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 31 Oct 2025 10:04:14 +0100 Subject: [PATCH 01/11] Added new search param for client_id --- packages/common/src/apps/searchParams.ts | 7 +++++++ packages/common/test/apps/searchParams.test.ts | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/packages/common/src/apps/searchParams.ts b/packages/common/src/apps/searchParams.ts index d32f34043..9f51009b2 100644 --- a/packages/common/src/apps/searchParams.ts +++ b/packages/common/src/apps/searchParams.ts @@ -49,6 +49,10 @@ export enum MonkSearchParam { * @see SteeringWheelPosition */ STEERING_WHEEL = 's', + /** + * Search parameter used to specify the Auth0 Client ID. + */ + CLIENT_ID = 'c', } /** @@ -60,6 +64,7 @@ export type MonkSearchParamsGetter = { (param: MonkSearchParam.VEHICLE_TYPE): VehicleType | null; (param: MonkSearchParam.STEERING_WHEEL): SteeringWheelPosition | null; (param: MonkSearchParam.LANGUAGE): MonkLanguage | null; + (param: MonkSearchParam.CLIENT_ID): string | null; }; function validateParamValue( @@ -108,6 +113,8 @@ export function useMonkSearchParams({ availableVehicleTypes }: UseMonkSearchPara return validateParamValue(value, SteeringWheelPosition); case MonkSearchParam.LANGUAGE: return validateParamValue(value, monkLanguages); + case MonkSearchParam.CLIENT_ID: + return value; default: return null; } diff --git a/packages/common/test/apps/searchParams.test.ts b/packages/common/test/apps/searchParams.test.ts index 0624512f3..65a574c5e 100644 --- a/packages/common/test/apps/searchParams.test.ts +++ b/packages/common/test/apps/searchParams.test.ts @@ -162,5 +162,23 @@ describe('MonkSearchParams utils', () => { unmount(); }); + + it('should return a null client ID if it is not found in the search params', () => { + const { result, unmount } = renderHook(useMonkSearchParams); + + expect(result.current.get(MonkSearchParam.CLIENT_ID)).toBeNull(); + + unmount(); + }); + + it('should return the client ID if it is found in the search params', () => { + const clientId = 'test-id-test'; + mockSearchParams({ [MonkSearchParam.CLIENT_ID]: clientId }); + const { result, unmount } = renderHook(useMonkSearchParams); + + expect(result.current.get(MonkSearchParam.CLIENT_ID)).toEqual(clientId); + + unmount(); + }); }); }); From 68e7c23785bd2a9b0a9ece36f4607424573b4747 Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 31 Oct 2025 10:04:51 +0100 Subject: [PATCH 02/11] Added authProvider component --- packages/network/README.md | 70 +++++++++++- packages/network/src/auth/authProvider.tsx | 52 +++++++++ .../network/src/auth/authProvider.types.ts | 28 +++++ packages/network/src/auth/index.ts | 2 + packages/network/src/auth/token.ts | 28 +++++ .../network/test/auth/authProvider.test.tsx | 102 ++++++++++++++++++ packages/network/test/auth/token.test.ts | 85 ++++++++++++++- packages/types/src/config.ts | 4 + 8 files changed, 369 insertions(+), 2 deletions(-) create mode 100644 packages/network/src/auth/authProvider.tsx create mode 100644 packages/network/src/auth/authProvider.types.ts create mode 100644 packages/network/test/auth/authProvider.test.tsx diff --git a/packages/network/README.md b/packages/network/README.md index a0f2de6cc..2b5532cbd 100644 --- a/packages/network/README.md +++ b/packages/network/README.md @@ -349,6 +349,39 @@ function MyAuthComponent() { } ``` +## AuthProvider + +### Description +This component is a Authentication provider that selects the appropriate Auth0 configuration based on URL parameters (MonkSearchParam.CLIENT_ID). + +### Example + +```tsx +import { AuthProvider } from '@monkvision/network'; + +const configs = [ + { + clientId: 'CID_EU', + domain: 'eu.auth0.com', + authorizationParams: { redirect_uri: 'https://eu.monk.ai' }, + }, + { + clientId: 'CID_US', + domain: 'us.auth0.com', + authorizationParams: { redirect_uri: 'https://us.monk.ai' }, + }, +]; + +function App() { + return ( + + ... + + ); +} +``` + + ## JWT Utils ### Token decoding You can decode Monk JWT token issued by Auth0 using the `decodeMonkJwt` util function provided by this package : @@ -374,7 +407,6 @@ console.log(isUserAuthorized(value, requiredPermissions)); // value can either be an auth token as a string or a decoded JWT payload ``` - ### isTokenExpired This utility function checks if an authorization token is expired or not. You can either pass an auth token to be decoded or the JWT payload directly. @@ -385,3 +417,39 @@ import { isTokenExpired } from '@monkvision/network'; console.log(isTokenExpired(value)); // value can either be an auth token as a string or a decoded JWT payload ``` + +### isTokenValid +This utility function checks if the stored auth token is valid for the given Auth0 Client ID. + +```typescript +import { isTokenValid } from '@monkvision/network'; + +console.log(isTokenValid(clientId)); +``` + +### getApiConfigOrThrow +This utility function retrieves the appropriate AuthConfig based on the URL search params (MonkSearchParam.CLIENT_ID). +If no matching configuration is found, it falls back to the first configuration in the provided list. + +```typescript +import { getApiConfigOrThrow } from '@monkvision/network'; +import { MonkSearchParam } from '@monkvision/common'; + +const configs = [ + { + clientId: 'CID_EU', + domain: 'eu.auth0.com', + authorizationParams: { redirect_uri: 'https://eu.monk.ai' }, + }, + { + clientId: 'CID_US', + domain: 'us.auth0.com', + authorizationParams: { redirect_uri: 'https://us.monk.ai' }, + }, +]; + +// Suppose the URL includes ?c=CID_US +const config = getApiConfigOrThrow(configs); +console.log(config); +// → returns the "us" configuration +``` diff --git a/packages/network/src/auth/authProvider.tsx b/packages/network/src/auth/authProvider.tsx new file mode 100644 index 000000000..6441d2417 --- /dev/null +++ b/packages/network/src/auth/authProvider.tsx @@ -0,0 +1,52 @@ +import { PropsWithChildren, useEffect } from 'react'; +import { Auth0Provider, useAuth0 } from '@auth0/auth0-react'; +import { STORAGE_KEY_AUTH_TOKEN } from '@monkvision/common'; +import { getApiConfigOrThrow, isTokenValid } from './token'; +import { AuthConfig } from './authProvider.types'; + +function AuthValidator({ clientId }: { clientId: string }) { + const { logout } = useAuth0(); + + useEffect(() => { + const storedToken = localStorage.getItem(STORAGE_KEY_AUTH_TOKEN); + + if (!isTokenValid(clientId) && storedToken) { + localStorage.removeItem(STORAGE_KEY_AUTH_TOKEN); + logout({ logoutParams: { returnTo: window.location.href } }); + } + }, [clientId, logout]); + + return null; +} + +/** + * Props accepted by the AuthProvider component. + */ +export interface AuthProviderProps { + /** + * List of Auth0 configurations to choose from based on URL parameters. + */ + configs: AuthConfig[]; +} + +/** + * Authentication provider that selects the appropriate Auth0 configuration based on URL parameters. + */ +export function AuthProvider({ configs, children }: PropsWithChildren) { + if (!configs?.length) { + return <>{children}; + } + const config = getApiConfigOrThrow(configs); + + return ( + + + {children} + + ); +} diff --git a/packages/network/src/auth/authProvider.types.ts b/packages/network/src/auth/authProvider.types.ts new file mode 100644 index 000000000..6fd58d607 --- /dev/null +++ b/packages/network/src/auth/authProvider.types.ts @@ -0,0 +1,28 @@ +import { Context } from 'react'; +import { Auth0ContextInterface, AuthorizationParams } from '@auth0/auth0-react'; + +/** + * Configuration for a specific Auth0 authentication region. + */ +export interface AuthConfig { + /** + * The Auth0 domain for this region. + */ + domain: string; + /** + * The Auth0 Client ID for this region. + */ + clientId: string; + /** + * The API domain for this region. + */ + apiDomain?: string; + /** + * The authorization parameters for this region. + */ + authorizationParams: AuthorizationParams; + /** + * The Auth0 context to use for this region. + */ + context?: Context; +} diff --git a/packages/network/src/auth/index.ts b/packages/network/src/auth/index.ts index 538b5c29a..9d4bbc9b4 100644 --- a/packages/network/src/auth/index.ts +++ b/packages/network/src/auth/index.ts @@ -1,2 +1,4 @@ export * from './token'; export * from './hooks'; +export * from './authProvider'; +export * from './authProvider.types'; diff --git a/packages/network/src/auth/token.ts b/packages/network/src/auth/token.ts index e8bb1b2b6..39d6c2c00 100644 --- a/packages/network/src/auth/token.ts +++ b/packages/network/src/auth/token.ts @@ -1,6 +1,8 @@ import { JwtPayload } from 'jsonwebtoken'; import { jwtDecode } from 'jwt-decode'; import { MonkApiPermission } from '@monkvision/types'; +import { STORAGE_KEY_AUTH_TOKEN, useMonkSearchParams, MonkSearchParam } from '@monkvision/common'; +import { AuthConfig } from './authProvider.types'; /** * The payload of the authentication token used with the Monk API. @@ -53,3 +55,29 @@ export function isTokenExpired(tokenOrPayload: MonkJwtPayload | string | null): typeof tokenOrPayload === 'object' ? tokenOrPayload : decodeMonkJwt(tokenOrPayload); return !payload.exp || Math.round(Date.now() / 1000) >= payload.exp; } + +/** + * Utility function that checks if the stored auth token is valid for the given Auth0 Client ID. + */ +export function isTokenValid(clientID: string): boolean { + const fetchedToken = localStorage.getItem(STORAGE_KEY_AUTH_TOKEN); + const fetchedTokenDecoded = fetchedToken ? decodeMonkJwt(fetchedToken).azp : null; + return fetchedTokenDecoded === clientID; +} + +/** + * Utility function that retrieves the appropriate AuthConfig based on the URL search params + * (ie. MonkSearchParam.CLIENT_ID). + */ +export function getApiConfigOrThrow(configs: AuthConfig[]): AuthConfig { + const { get } = useMonkSearchParams(); + + if (!configs.length) { + throw new Error('No authentication configurations provided'); + } + + const defaultClientId = configs[0]; + const clientId = get(MonkSearchParam.CLIENT_ID); + const authConfig = configs.find((config) => config.clientId === clientId); + return authConfig ?? defaultClientId; +} diff --git a/packages/network/test/auth/authProvider.test.tsx b/packages/network/test/auth/authProvider.test.tsx new file mode 100644 index 000000000..bb1131c62 --- /dev/null +++ b/packages/network/test/auth/authProvider.test.tsx @@ -0,0 +1,102 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { STORAGE_KEY_AUTH_TOKEN } from '@monkvision/common'; +import { AuthConfig } from '../../src/auth/authProvider.types'; +import { useAuth0 } from '@auth0/auth0-react'; + +jest.mock('../../src/auth/token', () => ({ + getApiConfigOrThrow: jest.fn(), + isTokenValid: jest.fn(), +})); + +import { AuthProvider } from '../../src/auth/authProvider'; +import { getApiConfigOrThrow, isTokenValid } from '../../src/auth/token'; + +function createConfigs(): AuthConfig[] { + return [ + { + clientId: 'client-A', + domain: 'a.auth0.com', + authorizationParams: { redirect_uri: 'https://a.example.com' }, + context: undefined, + }, + { + clientId: 'client-B', + domain: 'b.auth0.com', + authorizationParams: { redirect_uri: 'https://b.example.com' }, + context: undefined, + }, + ]; +} + +describe('AuthProvider component', () => { + beforeEach(() => { + jest.clearAllMocks(); + localStorage.clear(); + delete (global as any).__auth0ProviderLastProps; + }); + + it('should renders children and passes correct props to Auth0Provider', () => { + const childTestId = 'child-test'; + const configs = createConfigs(); + (getApiConfigOrThrow as jest.Mock).mockReturnValue(configs[0]); + + render( + +
+ , + ); + + expect(screen.getByTestId(childTestId)).toBeInTheDocument(); + const lastProps = (global as any).__auth0ProviderLastProps; + expect(lastProps).toMatchObject(configs[0]); + }); + + it('should calls logout and clears token when token is invalid', () => { + const childTestId = 'child-test'; + const configs = createConfigs(); + (getApiConfigOrThrow as jest.Mock).mockReturnValue(configs[1]); + (isTokenValid as jest.Mock).mockReturnValue(false); + + localStorage.setItem(STORAGE_KEY_AUTH_TOKEN, 'auth-token-test'); + + render( + +
+ , + ); + + const useAuth0Mock = (useAuth0 as jest.Mock).mock.results[0].value; + expect(localStorage.getItem(STORAGE_KEY_AUTH_TOKEN)).toBeNull(); + expect(useAuth0Mock.logout).toHaveBeenCalled(); + }); + + it('should not call logout when there is no token in localStorage', () => { + const childTestId = 'child-test'; + const configs = createConfigs(); + (getApiConfigOrThrow as jest.Mock).mockReturnValue(configs[1]); + (isTokenValid as jest.Mock).mockReturnValue(false); + + render( + +
+ , + ); + + const useAuth0Mock = (useAuth0 as jest.Mock).mock.results[0].value; + expect(useAuth0Mock.logout).not.toHaveBeenCalled(); + }); + + it('should update Auth0Provider props for different configs', () => { + const childTestId = 'child-test'; + const configs = createConfigs(); + (getApiConfigOrThrow as jest.Mock).mockReturnValue(configs[1]); + render( + +
+ , + ); + const lastProps = (global as any).__auth0ProviderLastProps; + expect(lastProps).toMatchObject(configs[1]); + }); +}); diff --git a/packages/network/test/auth/token.test.ts b/packages/network/test/auth/token.test.ts index 81abc0ac0..c301ab1b3 100644 --- a/packages/network/test/auth/token.test.ts +++ b/packages/network/test/auth/token.test.ts @@ -4,9 +4,26 @@ jest.mock('jwt-decode', () => ({ })), })); +jest.mock('@monkvision/common', () => { + const actual = jest.requireActual('@monkvision/common'); + return { + ...actual, + useMonkSearchParams: jest.fn(), + }; +}); + import { MonkApiPermission } from '@monkvision/types'; import { jwtDecode } from 'jwt-decode'; -import { decodeMonkJwt, isTokenExpired, isUserAuthorized, MonkJwtPayload } from '../../src'; +import { + decodeMonkJwt, + isTokenExpired, + isUserAuthorized, + MonkJwtPayload, + isTokenValid, + getApiConfigOrThrow, +} from '../../src'; +import { STORAGE_KEY_AUTH_TOKEN, useMonkSearchParams } from '@monkvision/common'; +import { AuthConfig } from '../../src/auth/authProvider.types'; describe('Network package JWT utils', () => { afterEach(() => { @@ -120,4 +137,70 @@ describe('Network package JWT utils', () => { ).toBe(false); }); }); + + describe('isTokenValid function', () => { + beforeEach(() => { + localStorage.clear(); + (jwtDecode as jest.Mock).mockReset(); + }); + + it('should return false when no token is stored', () => { + expect(isTokenValid('client-123')).toBe(false); + }); + + it('should return true when stored token azp matches client ID', () => { + localStorage.setItem(STORAGE_KEY_AUTH_TOKEN, 'encoded-token'); + (jwtDecode as jest.Mock).mockImplementationOnce(() => ({ azp: 'client-123' })); + expect(isTokenValid('client-123')).toBe(true); + expect(jwtDecode).toHaveBeenCalledWith('encoded-token'); + }); + + it('should return false when stored token azp differs from client ID', () => { + localStorage.setItem(STORAGE_KEY_AUTH_TOKEN, 'encoded-token'); + (jwtDecode as jest.Mock).mockImplementationOnce(() => ({ azp: 'client-XYZ' })); + expect(isTokenValid('client-123')).toBe(false); + }); + }); + + describe('getApiConfigOrThrow function', () => { + const mockUseSearchParams = useMonkSearchParams as unknown as jest.Mock; + + beforeEach(() => { + mockUseSearchParams.mockReset(); + }); + + const configs: AuthConfig[] = [ + { + clientId: 'client-A', + domain: 'a.auth0.com', + authorizationParams: { redirect_uri: 'https://a.example.com' }, + context: undefined, + }, + { + clientId: 'client-B', + domain: 'b.auth0.com', + authorizationParams: { redirect_uri: 'https://b.example.com' }, + context: undefined, + }, + ]; + + it('should throw when no authentication configurations are provided', () => { + mockUseSearchParams.mockReturnValue({ get: jest.fn().mockReturnValue(undefined) }); + expect(() => getApiConfigOrThrow([] as AuthConfig[])).toThrow( + 'No authentication configurations provided', + ); + }); + + it('should return first config when no matching clientId in params', () => { + mockUseSearchParams.mockReturnValue({ get: jest.fn().mockReturnValue(undefined) }); + const result = getApiConfigOrThrow(configs); + expect(result).toBe(configs[0]); + }); + + it('should return matching config when clientId is present in params', () => { + mockUseSearchParams.mockReturnValue({ get: jest.fn().mockReturnValue('client-B') }); + const result = getApiConfigOrThrow(configs); + expect(result).toBe(configs[1]); + }); + }); }); diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 5855b8d35..5d0f18a9d 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -365,4 +365,8 @@ export type LiveConfig = (PhotoCaptureAppConfig | VideoCaptureAppConfig) & { * The description of the configuration. */ description: string; + /** + * Optional API domain override. + */ + apiDomain?: string; }; From d9ce73cdbcb9277849e37b66e4955a328bd9d05f Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 31 Oct 2025 10:44:57 +0100 Subject: [PATCH 03/11] Added apiDomain prop to liveconfig --- .../LiveConfigAppProvider.tsx | 18 +++++++- .../components/LiveConfigAppProvider.test.tsx | 42 +++++++++++++++++-- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx index 5644b4bf2..02a78d556 100644 --- a/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx @@ -21,6 +21,10 @@ export interface LiveConfigAppProviderProps extends Omit { loading.onSuccess(); - setConfig(result); + const finalApiDomain = result.apiDomain ?? apiDomain; + + if (!finalApiDomain) { + const error = new Error( + `Missing apiDomain: neither prop apiDomain nor result.apiDomain is defined for live config ${id}.`, + ); + handleError(error); + loading.onError(); + return; + } + + setConfig({ ...result, apiDomain: finalApiDomain }); }, onReject: (err) => { handleError(err); diff --git a/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx b/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx index e2eb5c1fa..cf6c6be66 100644 --- a/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx +++ b/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx @@ -24,7 +24,7 @@ describe('LiveConfigAppProvider component', () => { }); it('should fetch the live config and pass it to the MonkAppStateProvider component', async () => { - const config = { hello: 'world' }; + const config = { hello: 'world', apiDomain: 'test-domain' }; (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve(config)); const id = 'test-id-test'; const { unmount } = render(); @@ -36,10 +36,42 @@ describe('LiveConfigAppProvider component', () => { unmount(); }); + it('should fetch the live config, ensure apiDomain is set, and pass it to MonkAppStateProvider', async () => { + const config = { hello: 'world' }; + const liveConfigAppProviderPropsMock = { id: 'test-id-test', apiDomain: 'test-domain' }; + (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve(config)); + const { unmount } = render(); + + await waitFor(() => { + expectPropsOnChildMock(MonkAppStateProvider, { + config: { ...config, apiDomain: liveConfigAppProviderPropsMock.apiDomain }, + }); + }); + + unmount(); + }); + + it('should display an error message with a retry button in case of no apiDomain found', async () => { + const config = { hello: 'world' }; + (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve(config)); + const id = 'test-id-test'; + const { unmount } = render(); + + expect(MonkAppStateProvider).not.toHaveBeenCalled(); + await waitFor(() => { + expect(screen.getByTestId('error-msg')).not.toBeNull(); + expectPropsOnChildMock(Button, { children: 'Retry', onClick: expect.any(Function) }); + }); + + unmount(); + }); + it('should pass down the props and children to the MonkAppStateProvider component', async () => { const onFetchAuthToken = jest.fn(); const onFetchLanguage = jest.fn(); const children = 'test-children'; + const config = { hello: 'world', apiDomain: 'test-domain' }; + (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve(config)); const { unmount } = render( { expect(screen.queryByTestId(spinnerTestId)).not.toBeNull(); expect(MonkAppStateProvider).not.toHaveBeenCalled(); await act(async () => { - promise.resolve({}); + promise.resolve({ apiDomain: 'test-domain' }); await promise; }); expect(screen.queryByTestId(spinnerTestId)).toBeNull(); @@ -93,7 +125,9 @@ describe('LiveConfigAppProvider component', () => { expectPropsOnChildMock(Button, { children: 'Retry', onClick: expect.any(Function) }); }); const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; - (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve({})); + (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ apiDomain: 'test-domain' }), + ); act(() => { onClick(); }); @@ -107,7 +141,7 @@ describe('LiveConfigAppProvider component', () => { }); it('should not fetch the live config and return the local config if it is used', async () => { - const localConfig = { hello: 'world' } as unknown as LiveConfig; + const localConfig = { hello: 'world', apiDomain: 'test-domain' } as unknown as LiveConfig; const id = 'test-id-test'; const { unmount } = render(); From 716cf32e21dfcfc13abcc07189947dd7a39cec5e Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 31 Oct 2025 10:46:13 +0100 Subject: [PATCH 04/11] Added test utils for authProvider component --- .../test-utils/src/__mocks__/@auth0/auth0-react.ts | 8 -------- .../src/__mocks__/@auth0/auth0-react.tsx | 14 ++++++++++++++ .../src/__mocks__/@monkvision/common.tsx | 1 + .../src/__mocks__/@monkvision/network.ts | 2 ++ 4 files changed, 17 insertions(+), 8 deletions(-) delete mode 100644 configs/test-utils/src/__mocks__/@auth0/auth0-react.ts create mode 100644 configs/test-utils/src/__mocks__/@auth0/auth0-react.tsx diff --git a/configs/test-utils/src/__mocks__/@auth0/auth0-react.ts b/configs/test-utils/src/__mocks__/@auth0/auth0-react.ts deleted file mode 100644 index 3cae079dd..000000000 --- a/configs/test-utils/src/__mocks__/@auth0/auth0-react.ts +++ /dev/null @@ -1,8 +0,0 @@ -export = { - /* Actual exports */ - /* Mocks */ - useAuth0: jest.fn(() => ({ - getAccessTokenWithPopup: jest.fn(() => Promise.resolve('')), - logout: jest.fn(), - })), -}; diff --git a/configs/test-utils/src/__mocks__/@auth0/auth0-react.tsx b/configs/test-utils/src/__mocks__/@auth0/auth0-react.tsx new file mode 100644 index 000000000..438164121 --- /dev/null +++ b/configs/test-utils/src/__mocks__/@auth0/auth0-react.tsx @@ -0,0 +1,14 @@ +const mockAuth0Provider = ({ children, ...props }: any) => { + (global as any).__auth0ProviderLastProps = props; + return <>{children}; +}; + +export = { + /* Actual exports */ + /* Mocks */ + useAuth0: jest.fn(() => ({ + getAccessTokenWithPopup: jest.fn(() => Promise.resolve('')), + logout: jest.fn(), + })), + Auth0Provider: mockAuth0Provider, +}; diff --git a/configs/test-utils/src/__mocks__/@monkvision/common.tsx b/configs/test-utils/src/__mocks__/@monkvision/common.tsx index c647ef190..0b50f1e03 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/common.tsx +++ b/configs/test-utils/src/__mocks__/@monkvision/common.tsx @@ -148,4 +148,5 @@ export = { requestCompassPermission: jest.fn(() => Promise.resolve()), })), useSafeTimeout: jest.fn(() => jest.fn()), + useMonkSearchParams: jest.fn(() => ({ get: jest.fn(() => null) })), }; diff --git a/configs/test-utils/src/__mocks__/@monkvision/network.ts b/configs/test-utils/src/__mocks__/@monkvision/network.ts index bbb35ecec..c9ae26f68 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/network.ts +++ b/configs/test-utils/src/__mocks__/@monkvision/network.ts @@ -31,6 +31,8 @@ export = { decodeMonkJwt: jest.fn((str) => str), isUserAuthorized: jest.fn(() => true), isTokenExpired: jest.fn(() => false), + isTokenValid: jest.fn(() => true), + getApiConfigOrThrow: jest.fn(), useAuth: jest.fn(() => ({ authToken: null, login: jest.fn(() => Promise.resolve('')), From 0b0c9a6dddb420af84a55e090d038c08fba42540 Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 31 Oct 2025 10:47:21 +0100 Subject: [PATCH 05/11] Added US cluster to demo app --- apps/demo-app/.env-cmdrc.json | 29 +++++++++++++++++++++++----- apps/demo-app/src/auth.ts | 24 +++++++++++++++++++++++ apps/demo-app/src/components/App.tsx | 3 +++ apps/demo-app/src/index.tsx | 16 ++++----------- 4 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 apps/demo-app/src/auth.ts diff --git a/apps/demo-app/.env-cmdrc.json b/apps/demo-app/.env-cmdrc.json index 3dd97fe90..80716eb9e 100644 --- a/apps/demo-app/.env-cmdrc.json +++ b/apps/demo-app/.env-cmdrc.json @@ -6,9 +6,13 @@ "REACT_APP_ENVIRONMENT": "local", "REACT_APP_LIVE_CONFIG_ID": "demo-app-development", "REACT_APP_USE_LOCAL_CONFIG": "true", - "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", + "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", + "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_SENTRY_DEBUG": "true", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" @@ -16,9 +20,13 @@ "development": { "REACT_APP_ENVIRONMENT": "development", "REACT_APP_LIVE_CONFIG_ID": "demo-app-development", - "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", + "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", + "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" }, @@ -26,26 +34,37 @@ "REACT_APP_ENVIRONMENT": "staging", "REACT_APP_LIVE_CONFIG_ID": "demo-app-staging", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", - "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", + "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" }, "preview": { "REACT_APP_ENVIRONMENT": "preview", "REACT_APP_LIVE_CONFIG_ID": "demo-app-preview", - "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", + "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", + "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" }, "backend-staging-qa": { "REACT_APP_ENVIRONMENT": "backend-staging-qa", "REACT_APP_LIVE_CONFIG_ID": "demo-app-backend-staging-qa", - "REACT_APP_AUTH_DOMAIN": "idp.staging.monk.ai", "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", + "REACT_APP_AUTH_DOMAIN": "idp.staging.monk.ai", + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "DAeZWqeeOfgItYBcQzFeFwSrlvmUdN7L", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", + "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.staging.monk.ai" } diff --git a/apps/demo-app/src/auth.ts b/apps/demo-app/src/auth.ts new file mode 100644 index 000000000..47e4650f1 --- /dev/null +++ b/apps/demo-app/src/auth.ts @@ -0,0 +1,24 @@ +import { AuthorizationParams } from '@auth0/auth0-react'; +import { getEnvOrThrow } from '@monkvision/common'; +import { AuthConfig } from '@monkvision/network'; + +export const AUTHORIZATION_PARAMS: AuthorizationParams = { + redirect_uri: window.location.origin, + audience: getEnvOrThrow('REACT_APP_AUTH_AUDIENCE'), + prompt: 'login', +}; + +export const authConfigs: AuthConfig[] = [ + { + domain: getEnvOrThrow('REACT_APP_AUTH_DOMAIN'), + clientId: getEnvOrThrow('REACT_APP_AUTH_CLIENT_ID'), + apiDomain: getEnvOrThrow('REACT_APP_API_DOMAIN'), + authorizationParams: AUTHORIZATION_PARAMS, + }, + { + domain: getEnvOrThrow('REACT_APP_AUTH_DOMAIN_US'), + clientId: getEnvOrThrow('REACT_APP_AUTH_CLIENT_ID_US'), + apiDomain: getEnvOrThrow('REACT_APP_API_DOMAIN_US'), + authorizationParams: AUTHORIZATION_PARAMS, + }, +]; diff --git a/apps/demo-app/src/components/App.tsx b/apps/demo-app/src/components/App.tsx index b86bf83c1..f9c8def05 100644 --- a/apps/demo-app/src/components/App.tsx +++ b/apps/demo-app/src/components/App.tsx @@ -3,9 +3,11 @@ import { getEnvOrThrow, MonkProvider } from '@monkvision/common'; import { useTranslation } from 'react-i18next'; import { LiveConfigAppProvider } from '@monkvision/common-ui-web'; import { LiveConfig } from '@monkvision/types'; +import { getApiConfigOrThrow } from '@monkvision/network'; import { Page } from '../pages'; import * as config from '../local-config.json'; import { AppContainer } from './AppContainer'; +import { authConfigs } from '../auth'; const localConfig = process.env['REACT_APP_USE_LOCAL_CONFIG'] === 'true' @@ -20,6 +22,7 @@ export function App() { navigate(Page.CREATE_INSPECTION)} onFetchLanguage={(lang) => i18n.changeLanguage(lang)} lang={i18n.language} diff --git a/apps/demo-app/src/index.tsx b/apps/demo-app/src/index.tsx index 9a5832da1..a141b9663 100644 --- a/apps/demo-app/src/index.tsx +++ b/apps/demo-app/src/index.tsx @@ -1,28 +1,20 @@ import ReactDOM from 'react-dom'; import { MonitoringProvider } from '@monkvision/monitoring'; import { AnalyticsProvider } from '@monkvision/analytics'; -import { Auth0Provider } from '@auth0/auth0-react'; -import { getEnvOrThrow } from '@monkvision/common'; +import { AuthProvider } from '@monkvision/network'; import { sentryMonitoringAdapter } from './sentry'; import { posthogAnalyticsAdapter } from './posthog'; import { AppRouter } from './components'; +import { authConfigs } from './auth'; import './index.css'; import './i18n'; ReactDOM.render( - + - + , document.getElementById('root'), From b67756ae6bc38fadab7af33ee80763a3840326eb Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 7 Nov 2025 13:59:17 +0100 Subject: [PATCH 06/11] Changed getAuthConfig function to check url param in this order: token > client_id Hooks removed from pure function --- packages/network/README.md | 10 +-- packages/network/src/auth/authProvider.tsx | 7 +- .../network/src/auth/authProvider.types.ts | 18 +++-- packages/network/src/auth/token.ts | 29 +++++---- .../network/test/auth/authProvider.test.tsx | 12 ++-- packages/network/test/auth/token.test.ts | 65 +++++++++++-------- 6 files changed, 81 insertions(+), 60 deletions(-) diff --git a/packages/network/README.md b/packages/network/README.md index 2b5532cbd..b29ec0216 100644 --- a/packages/network/README.md +++ b/packages/network/README.md @@ -427,12 +427,12 @@ import { isTokenValid } from '@monkvision/network'; console.log(isTokenValid(clientId)); ``` -### getApiConfigOrThrow -This utility function retrieves the appropriate AuthConfig based on the URL search params (MonkSearchParam.CLIENT_ID). -If no matching configuration is found, it falls back to the first configuration in the provided list. +### getAuthConfig +This utility function that retrieves the appropriate AuthConfig based on the URL search params. +Priority: TOKEN parameter (decoded azp claim) > CLIENT_ID parameter > first config (default) ```typescript -import { getApiConfigOrThrow } from '@monkvision/network'; +import { getAuthConfig } from '@monkvision/network'; import { MonkSearchParam } from '@monkvision/common'; const configs = [ @@ -449,7 +449,7 @@ const configs = [ ]; // Suppose the URL includes ?c=CID_US -const config = getApiConfigOrThrow(configs); +const config = getAuthConfig(configs); console.log(config); // → returns the "us" configuration ``` diff --git a/packages/network/src/auth/authProvider.tsx b/packages/network/src/auth/authProvider.tsx index 6441d2417..108d4e7eb 100644 --- a/packages/network/src/auth/authProvider.tsx +++ b/packages/network/src/auth/authProvider.tsx @@ -1,7 +1,7 @@ import { PropsWithChildren, useEffect } from 'react'; import { Auth0Provider, useAuth0 } from '@auth0/auth0-react'; import { STORAGE_KEY_AUTH_TOKEN } from '@monkvision/common'; -import { getApiConfigOrThrow, isTokenValid } from './token'; +import { getAuthConfig, isTokenValid } from './token'; import { AuthConfig } from './authProvider.types'; function AuthValidator({ clientId }: { clientId: string }) { @@ -33,10 +33,11 @@ export interface AuthProviderProps { * Authentication provider that selects the appropriate Auth0 configuration based on URL parameters. */ export function AuthProvider({ configs, children }: PropsWithChildren) { - if (!configs?.length) { + const config = getAuthConfig(configs); + + if (!config) { return <>{children}; } - const config = getApiConfigOrThrow(configs); return ( ; } diff --git a/packages/network/src/auth/token.ts b/packages/network/src/auth/token.ts index 39d6c2c00..3e5a86e31 100644 --- a/packages/network/src/auth/token.ts +++ b/packages/network/src/auth/token.ts @@ -1,7 +1,7 @@ import { JwtPayload } from 'jsonwebtoken'; import { jwtDecode } from 'jwt-decode'; import { MonkApiPermission } from '@monkvision/types'; -import { STORAGE_KEY_AUTH_TOKEN, useMonkSearchParams, MonkSearchParam } from '@monkvision/common'; +import { STORAGE_KEY_AUTH_TOKEN, MonkSearchParam, zlibDecompress } from '@monkvision/common'; import { AuthConfig } from './authProvider.types'; /** @@ -61,23 +61,26 @@ export function isTokenExpired(tokenOrPayload: MonkJwtPayload | string | null): */ export function isTokenValid(clientID: string): boolean { const fetchedToken = localStorage.getItem(STORAGE_KEY_AUTH_TOKEN); - const fetchedTokenDecoded = fetchedToken ? decodeMonkJwt(fetchedToken).azp : null; - return fetchedTokenDecoded === clientID; + const fetchedClientId = fetchedToken ? decodeMonkJwt(fetchedToken).azp : null; + return fetchedClientId === clientID; } /** - * Utility function that retrieves the appropriate AuthConfig based on the URL search params - * (ie. MonkSearchParam.CLIENT_ID). + * Utility function that retrieves the appropriate AuthConfig based on the URL search params. + * Priority: TOKEN parameter (decoded azp claim) > CLIENT_ID parameter > first config (default) */ -export function getApiConfigOrThrow(configs: AuthConfig[]): AuthConfig { - const { get } = useMonkSearchParams(); - +export function getAuthConfig(configs: AuthConfig[]): AuthConfig | undefined { if (!configs.length) { - throw new Error('No authentication configurations provided'); + return undefined; } - const defaultClientId = configs[0]; - const clientId = get(MonkSearchParam.CLIENT_ID); - const authConfig = configs.find((config) => config.clientId === clientId); - return authConfig ?? defaultClientId; + const { searchParams } = new URL(window.location.href); + const tokenParam = searchParams.get(MonkSearchParam.TOKEN); + const clientIdParam = searchParams.get(MonkSearchParam.CLIENT_ID); + + const tokenClientId = tokenParam ? decodeMonkJwt(zlibDecompress(tokenParam)).azp : null; + const targetClientId = tokenClientId || clientIdParam; + + const matchingConfig = configs.find((config) => config.clientId === targetClientId); + return matchingConfig ?? configs[0]; } diff --git a/packages/network/test/auth/authProvider.test.tsx b/packages/network/test/auth/authProvider.test.tsx index bb1131c62..0b019960a 100644 --- a/packages/network/test/auth/authProvider.test.tsx +++ b/packages/network/test/auth/authProvider.test.tsx @@ -5,12 +5,12 @@ import { AuthConfig } from '../../src/auth/authProvider.types'; import { useAuth0 } from '@auth0/auth0-react'; jest.mock('../../src/auth/token', () => ({ - getApiConfigOrThrow: jest.fn(), + getAuthConfig: jest.fn(), isTokenValid: jest.fn(), })); import { AuthProvider } from '../../src/auth/authProvider'; -import { getApiConfigOrThrow, isTokenValid } from '../../src/auth/token'; +import { getAuthConfig, isTokenValid } from '../../src/auth/token'; function createConfigs(): AuthConfig[] { return [ @@ -39,7 +39,7 @@ describe('AuthProvider component', () => { it('should renders children and passes correct props to Auth0Provider', () => { const childTestId = 'child-test'; const configs = createConfigs(); - (getApiConfigOrThrow as jest.Mock).mockReturnValue(configs[0]); + (getAuthConfig as jest.Mock).mockReturnValue(configs[0]); render( @@ -55,7 +55,7 @@ describe('AuthProvider component', () => { it('should calls logout and clears token when token is invalid', () => { const childTestId = 'child-test'; const configs = createConfigs(); - (getApiConfigOrThrow as jest.Mock).mockReturnValue(configs[1]); + (getAuthConfig as jest.Mock).mockReturnValue(configs[1]); (isTokenValid as jest.Mock).mockReturnValue(false); localStorage.setItem(STORAGE_KEY_AUTH_TOKEN, 'auth-token-test'); @@ -74,7 +74,7 @@ describe('AuthProvider component', () => { it('should not call logout when there is no token in localStorage', () => { const childTestId = 'child-test'; const configs = createConfigs(); - (getApiConfigOrThrow as jest.Mock).mockReturnValue(configs[1]); + (getAuthConfig as jest.Mock).mockReturnValue(configs[1]); (isTokenValid as jest.Mock).mockReturnValue(false); render( @@ -90,7 +90,7 @@ describe('AuthProvider component', () => { it('should update Auth0Provider props for different configs', () => { const childTestId = 'child-test'; const configs = createConfigs(); - (getApiConfigOrThrow as jest.Mock).mockReturnValue(configs[1]); + (getAuthConfig as jest.Mock).mockReturnValue(configs[1]); render(
diff --git a/packages/network/test/auth/token.test.ts b/packages/network/test/auth/token.test.ts index c301ab1b3..d8b526ff5 100644 --- a/packages/network/test/auth/token.test.ts +++ b/packages/network/test/auth/token.test.ts @@ -4,25 +4,17 @@ jest.mock('jwt-decode', () => ({ })), })); -jest.mock('@monkvision/common', () => { - const actual = jest.requireActual('@monkvision/common'); - return { - ...actual, - useMonkSearchParams: jest.fn(), - }; -}); - -import { MonkApiPermission } from '@monkvision/types'; import { jwtDecode } from 'jwt-decode'; +import { MonkApiPermission } from '@monkvision/types'; +import { MonkSearchParam, STORAGE_KEY_AUTH_TOKEN, zlibDecompress } from '@monkvision/common'; import { decodeMonkJwt, isTokenExpired, isUserAuthorized, MonkJwtPayload, isTokenValid, - getApiConfigOrThrow, + getAuthConfig, } from '../../src'; -import { STORAGE_KEY_AUTH_TOKEN, useMonkSearchParams } from '@monkvision/common'; import { AuthConfig } from '../../src/auth/authProvider.types'; describe('Network package JWT utils', () => { @@ -162,11 +154,11 @@ describe('Network package JWT utils', () => { }); }); - describe('getApiConfigOrThrow function', () => { - const mockUseSearchParams = useMonkSearchParams as unknown as jest.Mock; - + describe('getAuthConfig function', () => { beforeEach(() => { - mockUseSearchParams.mockReset(); + jest.resetAllMocks(); + delete (window as any).location; + (window as any).location = { href: 'https://test.app' }; }); const configs: AuthConfig[] = [ @@ -184,23 +176,44 @@ describe('Network package JWT utils', () => { }, ]; - it('should throw when no authentication configurations are provided', () => { - mockUseSearchParams.mockReturnValue({ get: jest.fn().mockReturnValue(undefined) }); - expect(() => getApiConfigOrThrow([] as AuthConfig[])).toThrow( - 'No authentication configurations provided', - ); + it('should return undefined when no configs are provided', () => { + const result = getAuthConfig([]); + expect(result).toBeUndefined(); }); - it('should return first config when no matching clientId in params', () => { - mockUseSearchParams.mockReturnValue({ get: jest.fn().mockReturnValue(undefined) }); - const result = getApiConfigOrThrow(configs); + it('should return first config when no params are present', () => { + const result = getAuthConfig(configs); expect(result).toBe(configs[0]); }); - it('should return matching config when clientId is present in params', () => { - mockUseSearchParams.mockReturnValue({ get: jest.fn().mockReturnValue('client-B') }); - const result = getApiConfigOrThrow(configs); + it('should return matching config when CLIENT_ID param is present', () => { + ( + window as any + ).location.href = `https://test.app?${MonkSearchParam.CLIENT_ID}=${configs[1].clientId}`; + + const result = getAuthConfig(configs); expect(result).toBe(configs[1]); }); + + it('should return matching config from TOKEN param (decoded azp)', () => { + const fakeToken = 'compressed-token'; + const fakeTokenDecompressed = 'decoded-token'; + const fakeDecodedTokenClientId = { azp: configs[0].clientId }; + + (window as any).location.href = `https://test.app?${MonkSearchParam.TOKEN}=${fakeToken}`; + (zlibDecompress as jest.Mock).mockImplementationOnce(() => fakeTokenDecompressed); + (jwtDecode as jest.Mock).mockImplementationOnce(() => fakeDecodedTokenClientId); + + const result = getAuthConfig(configs); + expect(zlibDecompress).toHaveBeenCalledWith(fakeToken); + expect(jwtDecode).toHaveBeenCalledWith(fakeTokenDecompressed); + expect(result).toBe(configs[0]); + }); + + it('falls back to first config when no match found', () => { + (window as any).location.href = `https://test.app?${MonkSearchParam.CLIENT_ID}=nonexistent`; + const result = getAuthConfig(configs); + expect(result).toBe(configs[0]); + }); }); }); From 7bb839c4540e3a9f8870a299c62440091f28f648 Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 7 Nov 2025 14:40:10 +0100 Subject: [PATCH 07/11] Made thumbnail optional in liveconfig fetched and instead passed as props --- .../LiveConfigAppProvider.tsx | 34 +++++++++++++++---- .../components/LiveConfigAppProvider.test.tsx | 34 +++++++++++++++---- 2 files changed, 55 insertions(+), 13 deletions(-) diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx index 02a78d556..511f825e3 100644 --- a/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx @@ -13,6 +13,24 @@ import { styles } from './LiveConfigAppProvider.styles'; import { Spinner } from '../Spinner'; import { Button } from '../Button'; +function validateRequiredDomains( + apiDomain: string | undefined, + thumbnailDomain: string | undefined, + configId: string, +): Error | null { + if (!apiDomain) { + return new Error( + `Missing required apiDomain: provide it as a LiveConfigAppProvider prop or define it in the JSON live config file: "${configId}".`, + ); + } + if (!thumbnailDomain) { + return new Error( + `Missing required thumbnailDomain: provide it as a LiveConfigAppProvider prop or define it in the JSON live config file: "${configId}".`, + ); + } + return null; +} + /** * Props accepted by the LiveConfigAppProvider component. */ @@ -25,6 +43,10 @@ export interface LiveConfigAppProviderProps extends Omit { loading.onSuccess(); const finalApiDomain = result.apiDomain ?? apiDomain; + const finalThumbnailDomain = result.thumbnailDomain ?? thumbnailDomain; - if (!finalApiDomain) { - const error = new Error( - `Missing apiDomain: neither prop apiDomain nor result.apiDomain is defined for live config ${id}.`, - ); - handleError(error); + const missingDomain = validateRequiredDomains(finalApiDomain, finalThumbnailDomain, id); + if (missingDomain) { + handleError(missingDomain); loading.onError(); return; } - setConfig({ ...result, apiDomain: finalApiDomain }); + setConfig({ ...result, apiDomain: finalApiDomain, thumbnailDomain: finalThumbnailDomain }); }, onReject: (err) => { handleError(err); diff --git a/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx b/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx index cf6c6be66..1d7ef8246 100644 --- a/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx +++ b/packages/common-ui-web/test/components/LiveConfigAppProvider.test.tsx @@ -24,7 +24,11 @@ describe('LiveConfigAppProvider component', () => { }); it('should fetch the live config and pass it to the MonkAppStateProvider component', async () => { - const config = { hello: 'world', apiDomain: 'test-domain' }; + const config = { + hello: 'world', + apiDomain: 'test-domain', + thumbnailDomain: 'test-thumbmail-domain', + }; (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve(config)); const id = 'test-id-test'; const { unmount } = render(); @@ -38,13 +42,21 @@ describe('LiveConfigAppProvider component', () => { it('should fetch the live config, ensure apiDomain is set, and pass it to MonkAppStateProvider', async () => { const config = { hello: 'world' }; - const liveConfigAppProviderPropsMock = { id: 'test-id-test', apiDomain: 'test-domain' }; + const liveConfigAppProviderPropsMock = { + id: 'test-id-test', + apiDomain: 'test-domain', + thumbnailDomain: 'test-thumbmail-domain', + }; (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve(config)); const { unmount } = render(); await waitFor(() => { expectPropsOnChildMock(MonkAppStateProvider, { - config: { ...config, apiDomain: liveConfigAppProviderPropsMock.apiDomain }, + config: { + ...config, + apiDomain: liveConfigAppProviderPropsMock.apiDomain, + thumbnailDomain: liveConfigAppProviderPropsMock.thumbnailDomain, + }, }); }); @@ -70,7 +82,11 @@ describe('LiveConfigAppProvider component', () => { const onFetchAuthToken = jest.fn(); const onFetchLanguage = jest.fn(); const children = 'test-children'; - const config = { hello: 'world', apiDomain: 'test-domain' }; + const config = { + hello: 'world', + apiDomain: 'test-domain', + thumbnailDomain: 'test-thumbmail-domain', + }; (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => Promise.resolve(config)); const { unmount } = render( { expect(screen.queryByTestId(spinnerTestId)).not.toBeNull(); expect(MonkAppStateProvider).not.toHaveBeenCalled(); await act(async () => { - promise.resolve({ apiDomain: 'test-domain' }); + promise.resolve({ apiDomain: 'test-domain', thumbnailDomain: 'test-thumbmail-domain' }); await promise; }); expect(screen.queryByTestId(spinnerTestId)).toBeNull(); @@ -126,7 +142,7 @@ describe('LiveConfigAppProvider component', () => { }); const { onClick } = (Button as unknown as jest.Mock).mock.calls[0][0]; (MonkApi.getLiveConfig as jest.Mock).mockImplementationOnce(() => - Promise.resolve({ apiDomain: 'test-domain' }), + Promise.resolve({ apiDomain: 'test-domain', thumbnailDomain: 'test-thumbmail-domain' }), ); act(() => { onClick(); @@ -141,7 +157,11 @@ describe('LiveConfigAppProvider component', () => { }); it('should not fetch the live config and return the local config if it is used', async () => { - const localConfig = { hello: 'world', apiDomain: 'test-domain' } as unknown as LiveConfig; + const localConfig = { + hello: 'world', + apiDomain: 'test-domain', + thumbnailDomain: 'test-thumbmail-domain', + } as unknown as LiveConfig; const id = 'test-id-test'; const { unmount } = render(); From df5b84f701ba8bd3122f2ed28489d5594669c49a Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 7 Nov 2025 14:40:52 +0100 Subject: [PATCH 08/11] Made apiDomain and thumbnailDomain optional in configValidator --- documentation/src/utils/schemas.ts | 56 +++++++++++++++++++----------- 1 file changed, 35 insertions(+), 21 deletions(-) diff --git a/documentation/src/utils/schemas.ts b/documentation/src/utils/schemas.ts index a7fcbba89..08c22c22e 100644 --- a/documentation/src/utils/schemas.ts +++ b/documentation/src/utils/schemas.ts @@ -9,6 +9,7 @@ import { MileageUnit, MonkApiPermission, PhotoCaptureSightGuidelinesOption, + PhotoCaptureSightTutorialOption, PhotoCaptureTutorialOption, SteeringWheelPosition, TaskName, @@ -269,44 +270,57 @@ export const CreateInspectionDiscriminatedUnionSchema = z.discriminatedUnion( const domainsByEnv = { staging: { - api: 'api.staging.monk.ai/v1', - thumbnail: 'api.staging.monk.ai/image_resize', + api: ['api.staging.monk.ai/v1', 'monk-us-core-api.gateway.dev.acvauctions.com/v1'], + thumbnail: [ + 'api.staging.monk.ai/image_resize', + 'monk-us-image.gateway.dev.acvauctions.com/image_resize', + ], }, preview: { - api: 'api.preview.monk.ai/v1', - thumbnail: 'api.preview.monk.ai/image_resize', + api: ['api.preview.monk.ai/v1', 'monk-us-core-api.gateway.staging.acvauctions.com/v1'], + thumbnail: [ + 'api.preview.monk.ai/image_resize', + 'monk-us-image.gateway.staging.acvauctions.com/image_resize', + ], }, production: { - api: 'api.monk.ai/v1', - thumbnail: 'api.monk.ai/image_resize', + api: ['api.monk.ai/v1', 'monk-us-core-api.gateway.acvauctions.com/v1'], + thumbnail: ['api.monk.ai/image_resize', 'monk-us-image.gateway.acvauctions.com/image_resize'], }, }; -const apiDomains = Object.values(domainsByEnv).map((env) => env.api) as [string, ...string[]]; -const thumbnailDomains = Object.values(domainsByEnv).map((env) => env.thumbnail) as [ +const apiDomains = [...new Set(Object.values(domainsByEnv).flatMap((env) => env.api))] as [ string, ...string[], ]; +const thumbnailDomains = [ + ...new Set(Object.values(domainsByEnv).flatMap((env) => env.thumbnail)), +] as [string, ...string[]]; export const DomainsSchema = z .object({ - apiDomain: z.enum(apiDomains), - thumbnailDomain: z.enum(thumbnailDomains), + apiDomain: z.enum(apiDomains).optional(), + thumbnailDomain: z.enum(thumbnailDomains).optional(), }) .refine( (data) => { - const apiEnv = Object.values(domainsByEnv).find((env) => env.api === data.apiDomain); - const thumbnailEnv = Object.values(domainsByEnv).find( - (env) => env.thumbnail === data.thumbnailDomain, + const { apiDomain, thumbnailDomain } = data; + + if (!apiDomain || !thumbnailDomain) { + return true; + } + + const apiEnv = Object.values(domainsByEnv).find((env) => env.api.includes(apiDomain)); + const thumbnailEnv = Object.values(domainsByEnv).find((env) => + env.thumbnail.includes(thumbnailDomain), ); - return !!apiEnv && apiEnv === thumbnailEnv; + + return Boolean(apiEnv && thumbnailEnv && apiEnv === thumbnailEnv); }, - (data) => ({ - message: `The selected thumbnailDomain must correspond to the selected apiDomain. Please use the corresponding thumbnailDomain: ${ - thumbnailDomains[apiDomains.indexOf(data.apiDomain)] - }`, + { + message: 'The selected thumbnailDomain must correspond to the selected apiDomain.', path: ['thumbnailDomain'], - }), + }, ); export const LiveConfigSchema = z @@ -329,8 +343,8 @@ export const LiveConfigSchema = z sightGuidelines: z.array(SightGuidelineSchema).optional(), enableTutorial: z.nativeEnum(PhotoCaptureTutorialOption).optional(), allowSkipTutorial: z.boolean().optional(), - enableSightTutorial: z.boolean().optional(), - sightTutorial: z.array(SightGuidelineSchema).optional(), + enableSightTutorial: z.nativeEnum(PhotoCaptureSightTutorialOption).optional(), + sightTutorial: z.array(SightTutorialSchema).optional(), defaultVehicleType: z.nativeEnum(VehicleType), allowManualLogin: z.boolean(), allowVehicleTypeSelection: z.boolean(), From 3916b8724746cdd3d8ff0e6c6fad10d00b961b63 Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 7 Nov 2025 14:41:09 +0100 Subject: [PATCH 09/11] Changed mock global --- configs/test-utils/src/__mocks__/@monkvision/common.tsx | 2 ++ configs/test-utils/src/__mocks__/@monkvision/network.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/configs/test-utils/src/__mocks__/@monkvision/common.tsx b/configs/test-utils/src/__mocks__/@monkvision/common.tsx index 0b50f1e03..407b451a0 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/common.tsx +++ b/configs/test-utils/src/__mocks__/@monkvision/common.tsx @@ -39,6 +39,7 @@ const { email, getVehicleModel, getAspectRatio, + MonkSearchParam, } = jest.requireActual('@monkvision/common'); export = { @@ -71,6 +72,7 @@ export = { email, getVehicleModel, getAspectRatio, + MonkSearchParam, /* Mocks */ useMonkTheme: jest.fn(() => createTheme()), diff --git a/configs/test-utils/src/__mocks__/@monkvision/network.ts b/configs/test-utils/src/__mocks__/@monkvision/network.ts index c9ae26f68..9d720e5af 100644 --- a/configs/test-utils/src/__mocks__/@monkvision/network.ts +++ b/configs/test-utils/src/__mocks__/@monkvision/network.ts @@ -32,7 +32,7 @@ export = { isUserAuthorized: jest.fn(() => true), isTokenExpired: jest.fn(() => false), isTokenValid: jest.fn(() => true), - getApiConfigOrThrow: jest.fn(), + getAuthConfig: jest.fn(), useAuth: jest.fn(() => ({ authToken: null, login: jest.fn(() => Promise.resolve('')), From 86a6bf47f3b89b8d0621de10ff9ed707c1baecda Mon Sep 17 00:00:00 2001 From: David Ly Date: Fri, 7 Nov 2025 14:41:33 +0100 Subject: [PATCH 10/11] Added thumbnail to env and demo app --- apps/demo-app/.env-cmdrc.json | 46 ++++++++++++++++++++++------ apps/demo-app/src/auth.ts | 2 ++ apps/demo-app/src/components/App.tsx | 5 +-- apps/demo-app/src/local-config.json | 2 -- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/apps/demo-app/.env-cmdrc.json b/apps/demo-app/.env-cmdrc.json index 80716eb9e..631131185 100644 --- a/apps/demo-app/.env-cmdrc.json +++ b/apps/demo-app/.env-cmdrc.json @@ -6,13 +6,18 @@ "REACT_APP_ENVIRONMENT": "local", "REACT_APP_LIVE_CONFIG_ID": "demo-app-development", "REACT_APP_USE_LOCAL_CONFIG": "true", + "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", - "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", - "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_THUMBNAIL_DOMAIN": "api.preview.monk.ai/image_resize", + + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", + "REACT_APP_THUMBNAIL_DOMAIN_US": "monk-us-image.gateway.staging.acvauctions.com/image_resize", + "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_SENTRY_DEBUG": "true", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" @@ -20,51 +25,72 @@ "development": { "REACT_APP_ENVIRONMENT": "development", "REACT_APP_LIVE_CONFIG_ID": "demo-app-development", + "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", - "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", - "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_THUMBNAIL_DOMAIN": "api.preview.monk.ai/image_resize", + + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", + "REACT_APP_THUMBNAIL_DOMAIN_US": "monk-us-image.gateway.staging.acvauctions.com/image_resize", + "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" }, "staging": { "REACT_APP_ENVIRONMENT": "staging", "REACT_APP_LIVE_CONFIG_ID": "demo-app-staging", + + "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", - "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", - "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_THUMBNAIL_DOMAIN": "api.preview.monk.ai/image_resize", + + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", + "REACT_APP_THUMBNAIL_DOMAIN_US": "monk-us-image.gateway.staging.acvauctions.com/image_resize", + "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" }, "preview": { "REACT_APP_ENVIRONMENT": "preview", "REACT_APP_LIVE_CONFIG_ID": "demo-app-preview", + "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_DOMAIN": "idp.preview.monk.ai", - "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "O7geYcPM6zEJrHw0WvQVzSIzw4WzrAtH", - "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_THUMBNAIL_DOMAIN": "api.preview.monk.ai/image_resize", + + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", + "REACT_APP_THUMBNAIL_DOMAIN_US": "monk-us-image.gateway.staging.acvauctions.com/image_resize", + "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.preview.monk.ai" }, "backend-staging-qa": { "REACT_APP_ENVIRONMENT": "backend-staging-qa", "REACT_APP_LIVE_CONFIG_ID": "demo-app-backend-staging-qa", + "REACT_APP_AUTH_AUDIENCE": "https://api.monk.ai/v1/", "REACT_APP_AUTH_DOMAIN": "idp.staging.monk.ai", - "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", "REACT_APP_AUTH_CLIENT_ID": "DAeZWqeeOfgItYBcQzFeFwSrlvmUdN7L", - "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN": "api.preview.monk.ai/v1", + "REACT_APP_THUMBNAIL_DOMAIN": "api.staging.dev.monk.ai/image_resize", + + "REACT_APP_AUTH_DOMAIN_US": "acv-staging.us.auth0.com", + "REACT_APP_AUTH_CLIENT_ID_US": "0tIBBTLd4uP52jtF2PcXawWYxW12mUfZ", "REACT_APP_API_DOMAIN_US": "monk-us-core-api.gateway.staging.acvauctions.com/v1", + "REACT_APP_THUMBNAIL_DOMAIN_US": "monk-us-image.gateway.staging.acvauctions.com/image_resize", + "REACT_APP_SENTRY_DSN": "https://74f50bfe6f11de7aefd54acfa5dfed96@o4505669501648896.ingest.us.sentry.io/4506863461662720", "REACT_APP_INSPECTION_REPORT_URL": "https://demo-capture.staging.monk.ai" } diff --git a/apps/demo-app/src/auth.ts b/apps/demo-app/src/auth.ts index 47e4650f1..8253a18f0 100644 --- a/apps/demo-app/src/auth.ts +++ b/apps/demo-app/src/auth.ts @@ -13,12 +13,14 @@ export const authConfigs: AuthConfig[] = [ domain: getEnvOrThrow('REACT_APP_AUTH_DOMAIN'), clientId: getEnvOrThrow('REACT_APP_AUTH_CLIENT_ID'), apiDomain: getEnvOrThrow('REACT_APP_API_DOMAIN'), + thumbnailDomain: getEnvOrThrow('REACT_APP_THUMBNAIL_DOMAIN'), authorizationParams: AUTHORIZATION_PARAMS, }, { domain: getEnvOrThrow('REACT_APP_AUTH_DOMAIN_US'), clientId: getEnvOrThrow('REACT_APP_AUTH_CLIENT_ID_US'), apiDomain: getEnvOrThrow('REACT_APP_API_DOMAIN_US'), + thumbnailDomain: getEnvOrThrow('REACT_APP_THUMBNAIL_DOMAIN_US'), authorizationParams: AUTHORIZATION_PARAMS, }, ]; diff --git a/apps/demo-app/src/components/App.tsx b/apps/demo-app/src/components/App.tsx index f9c8def05..0719ff6d7 100644 --- a/apps/demo-app/src/components/App.tsx +++ b/apps/demo-app/src/components/App.tsx @@ -3,7 +3,7 @@ import { getEnvOrThrow, MonkProvider } from '@monkvision/common'; import { useTranslation } from 'react-i18next'; import { LiveConfigAppProvider } from '@monkvision/common-ui-web'; import { LiveConfig } from '@monkvision/types'; -import { getApiConfigOrThrow } from '@monkvision/network'; +import { getAuthConfig } from '@monkvision/network'; import { Page } from '../pages'; import * as config from '../local-config.json'; import { AppContainer } from './AppContainer'; @@ -22,7 +22,8 @@ export function App() { navigate(Page.CREATE_INSPECTION)} onFetchLanguage={(lang) => i18n.changeLanguage(lang)} lang={i18n.language} diff --git a/apps/demo-app/src/local-config.json b/apps/demo-app/src/local-config.json index 280228747..d31e33cf1 100644 --- a/apps/demo-app/src/local-config.json +++ b/apps/demo-app/src/local-config.json @@ -12,8 +12,6 @@ "createInspectionOptions": { "tasks": ["damage_detection", "wheel_analysis"] }, - "apiDomain": "api.preview.monk.ai/v1", - "thumbnailDomain": "api.preview.monk.ai/image_resize", "enableTutorial": "first_time_only", "enableSightTutorial": "modern", "startTasksOnComplete": true, From d8d2b8eceed7f3ae93e0723b8b8263c3875b95c0 Mon Sep 17 00:00:00 2001 From: David Ly Date: Mon, 10 Nov 2025 15:42:45 +0100 Subject: [PATCH 11/11] Fixed wording --- .../LiveConfigAppProvider/LiveConfigAppProvider.tsx | 10 +++++++--- packages/network/test/auth/authProvider.test.tsx | 4 ++-- packages/types/src/config.ts | 4 ++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx index 511f825e3..f6d88b523 100644 --- a/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx +++ b/packages/common-ui-web/src/components/LiveConfigAppProvider/LiveConfigAppProvider.tsx @@ -97,9 +97,13 @@ export function LiveConfigAppProvider({ const finalApiDomain = result.apiDomain ?? apiDomain; const finalThumbnailDomain = result.thumbnailDomain ?? thumbnailDomain; - const missingDomain = validateRequiredDomains(finalApiDomain, finalThumbnailDomain, id); - if (missingDomain) { - handleError(missingDomain); + const missingDomainError = validateRequiredDomains( + finalApiDomain, + finalThumbnailDomain, + id, + ); + if (missingDomainError) { + handleError(missingDomainError); loading.onError(); return; } diff --git a/packages/network/test/auth/authProvider.test.tsx b/packages/network/test/auth/authProvider.test.tsx index 0b019960a..caaa62db8 100644 --- a/packages/network/test/auth/authProvider.test.tsx +++ b/packages/network/test/auth/authProvider.test.tsx @@ -36,7 +36,7 @@ describe('AuthProvider component', () => { delete (global as any).__auth0ProviderLastProps; }); - it('should renders children and passes correct props to Auth0Provider', () => { + it('should render children and pass correct props to Auth0Provider', () => { const childTestId = 'child-test'; const configs = createConfigs(); (getAuthConfig as jest.Mock).mockReturnValue(configs[0]); @@ -52,7 +52,7 @@ describe('AuthProvider component', () => { expect(lastProps).toMatchObject(configs[0]); }); - it('should calls logout and clears token when token is invalid', () => { + it('should call logout and clear token when token is invalid', () => { const childTestId = 'child-test'; const configs = createConfigs(); (getAuthConfig as jest.Mock).mockReturnValue(configs[1]); diff --git a/packages/types/src/config.ts b/packages/types/src/config.ts index 5d0f18a9d..58c187139 100644 --- a/packages/types/src/config.ts +++ b/packages/types/src/config.ts @@ -369,4 +369,8 @@ export type LiveConfig = (PhotoCaptureAppConfig | VideoCaptureAppConfig) & { * Optional API domain override. */ apiDomain?: string; + /** + * Optional Thumbnail domain override. + */ + thumbnailDomain?: string; };