diff --git a/src/oneid/oneid-ecs-core/src/main/webui/package.json b/src/oneid/oneid-ecs-core/src/main/webui/package.json
index f56268a4..73ab4934 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/package.json
+++ b/src/oneid/oneid-ecs-core/src/main/webui/package.json
@@ -10,6 +10,7 @@
"@mui/lab": "^5.0.0-alpha.170",
"@mui/material": "^5.15.20",
"@pagopa/mui-italia": "^1.4.2",
+ "@tanstack/react-query": "^5.62.2",
"@types/node": "^20.14.5",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/App.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/App.test.tsx
index 936edc42..af2ae1d5 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/App.test.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/App.test.tsx
@@ -19,7 +19,7 @@ vi.mock('./services/analyticsService', () => ({
}));
// Mock the components
-vi.mock('./pages/login/Login', () => ({
+vi.mock('./pages/login', () => ({
default: () =>
Mocked Login Component
,
}));
vi.mock('./pages/logout/Logout', () => ({
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/App.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/App.tsx
index b0655bc5..acbb2834 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/App.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/App.tsx
@@ -1,4 +1,3 @@
-import Login from './pages/login/Login';
import {
ROUTE_LOGIN,
ROUTE_LOGIN_ERROR,
@@ -7,6 +6,7 @@ import {
import { redirectToLogin } from './utils/utils';
import Logout from './pages/logout/Logout';
import { LoginError } from './pages/loginError/LoginError';
+import Login from './pages/login';
const onLogout = () => ;
const onLoginError = () => ;
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.test.tsx
new file mode 100644
index 00000000..c6f26b72
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.test.tsx
@@ -0,0 +1,110 @@
+import { describe, it, expect, vi, Mock } from 'vitest';
+import { renderHook, waitFor } from '@testing-library/react';
+import { useLoginData } from './useLoginData';
+import { ENV } from '../utils/env';
+import { fetchBannerContent, getIdpList, getClientData } from '../services/api';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+// Mock API functions
+vi.mock('../services/api', () => ({
+ fetchBannerContent: vi.fn(),
+ getIdpList: vi.fn(),
+ getClientData: vi.fn(),
+}));
+
+// Mock ENV
+vi.mock('../utils/env', () => ({
+ ENV: {
+ JSON_URL: {
+ ALERT: 'mock-alert-url',
+ IDP_LIST: 'mock-idp-list-url',
+ CLIENT_BASE_URL: 'mock-client-base-url',
+ },
+ },
+}));
+
+describe('useLoginData', () => {
+ const wrapper = ({ children }: { children: React.ReactNode }) => {
+ const queryClient = new QueryClient();
+ return (
+ {children}
+ );
+ };
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('fetches banner content successfully', async () => {
+ const mockBannerContent = [
+ { title: 'Test Banner', description: 'Test Description' },
+ ];
+ (fetchBannerContent as Mock).mockResolvedValue(mockBannerContent);
+
+ const { result } = renderHook(useLoginData, { wrapper });
+
+ await waitFor(() =>
+ expect(result.current.bannerQuery.isSuccess).toBe(true)
+ );
+ expect(fetchBannerContent).toHaveBeenCalledWith(ENV.JSON_URL.ALERT);
+ expect(result.current.bannerQuery.data).toEqual(mockBannerContent);
+ });
+
+ it('fetches identity providers list successfully', async () => {
+ const mockIdpList = {
+ providers: [{ name: 'Test IDP', url: 'test-idp-url' }],
+ };
+ (getIdpList as Mock).mockResolvedValue(mockIdpList);
+
+ const { result } = renderHook(useLoginData, { wrapper });
+
+ await waitFor(() => expect(result.current.idpQuery.isSuccess).toBe(true));
+ expect(getIdpList).toHaveBeenCalledWith(ENV.JSON_URL.IDP_LIST);
+ expect(result.current.idpQuery.data).toEqual(mockIdpList);
+ });
+
+ it('fetches client data successfully', async () => {
+ const mockClientData = {
+ clientId: 'test-client-id',
+ clientSecret: 'test-client-secret',
+ };
+ (getClientData as Mock).mockResolvedValue(mockClientData);
+
+ const { result } = renderHook(useLoginData, { wrapper });
+
+ await waitFor(() =>
+ expect(result.current.clientQuery.isSuccess).toBe(true)
+ );
+ expect(getClientData).toHaveBeenCalledWith(ENV.JSON_URL.CLIENT_BASE_URL);
+ expect(result.current.clientQuery.data).toEqual(mockClientData);
+ });
+
+ it('handles errors correctly', async () => {
+ (fetchBannerContent as Mock).mockRejectedValue(
+ new Error('Banner content error')
+ );
+ (getIdpList as Mock).mockRejectedValue(new Error('IDP list error'));
+ (getClientData as Mock).mockRejectedValue(new Error('Client data error'));
+
+ const { result } = renderHook(useLoginData, { wrapper });
+
+ await waitFor(() => expect(result.current.bannerQuery.isError).toBe(true), {
+ timeout: 10000,
+ });
+ expect(result.current.bannerQuery.error).toEqual(
+ new Error('Banner content error')
+ );
+
+ await waitFor(() => expect(result.current.idpQuery.isError).toBe(true), {
+ timeout: 10000,
+ });
+ expect(result.current.idpQuery.error).toEqual(new Error('IDP list error'));
+
+ await waitFor(() => expect(result.current.clientQuery.isError).toBe(true), {
+ timeout: 10000,
+ });
+ expect(result.current.clientQuery.error).toEqual(
+ new Error('Client data error')
+ );
+ });
+});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.tsx
new file mode 100644
index 00000000..b4541a52
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/hooks/useLoginData.tsx
@@ -0,0 +1,38 @@
+import { useQuery } from '@tanstack/react-query';
+import { ENV } from '../utils/env';
+import { IdentityProviders } from '../utils/IDPS';
+import {
+ type BannerContent,
+ type Client,
+ fetchBannerContent,
+ getIdpList,
+ getClientData,
+} from '../services/api';
+
+const staleTime = 5 * 60 * 1000;
+const retry = 2;
+
+export const useLoginData = () => {
+ const bannerQuery = useQuery, Error>({
+ queryKey: ['bannerContent'],
+ queryFn: () => fetchBannerContent(ENV.JSON_URL.ALERT),
+ staleTime,
+ retry,
+ });
+
+ const idpQuery = useQuery({
+ queryKey: ['idpList'],
+ queryFn: () => getIdpList(ENV.JSON_URL.IDP_LIST),
+ staleTime,
+ retry,
+ });
+
+ const clientQuery = useQuery({
+ queryKey: ['clientData'],
+ queryFn: () => getClientData(ENV.JSON_URL.CLIENT_BASE_URL),
+ staleTime,
+ retry,
+ });
+
+ return { bannerQuery, idpQuery, clientQuery };
+};
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/index.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/index.tsx
index 991d5e97..c1ff6ec8 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/index.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/index.tsx
@@ -5,16 +5,20 @@ import { theme } from '@pagopa/mui-italia';
import App from './App';
import './locale';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const rootEl = document.getElementById('root');
if (rootEl) {
const root = createRoot(rootEl);
+ const queryClient = new QueryClient();
root.render(
-
+
+
+
);
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.test.tsx
index d78f5434..4c1b3f80 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.test.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.test.tsx
@@ -1,138 +1,139 @@
-/* eslint-disable functional/immutable-data */
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
-import { afterAll, beforeAll, expect, Mock, test, vi } from 'vitest';
-
+import { Mock, vi } from 'vitest';
+import Login, { LinkWrapper } from '../login';
+import { useLoginData } from '../../hooks/useLoginData';
import { ENV } from '../../utils/env';
-import { i18nTestSetup } from '../../__tests__/i18nTestSetup';
-import Login from './Login';
-
-// Mock fetch
-global.fetch = vi.fn();
-
-i18nTestSetup({
- loginPage: {
- privacyAndCondition: {
- text: 'terms: {{termsLink}} privacy: {{privacyLink}}',
- },
- },
-});
-
-const oldWindowLocation = global.window.location;
-
-beforeAll(() => {
- // Mock window location
- Object.defineProperty(window, 'location', { value: { assign: vi.fn() } });
-});
-
-afterAll(() => {
- Object.defineProperty(window, 'location', { value: oldWindowLocation });
-});
-
-// Clear mocks after each test
-afterEach(() => {
- vi.clearAllMocks();
+import { trackEvent } from '../../services/analyticsService';
+import { ThemeProvider } from '@emotion/react';
+import { createTheme } from '@mui/material';
+
+vi.mock('../../hooks/useLoginData');
+vi.mock('../../services/analyticsService', () => ({
+ trackEvent: vi.fn(),
+}));
+vi.mock('@mui/material', async () => {
+ const actual = await vi.importActual('@mui/material');
+ return {
+ ...actual,
+ useTheme: () => ({
+ spacing: (value: number) => value * 8,
+ breakpoints: { down: () => '@media (max-width: 960px)' },
+ }),
+ };
});
-test('Renders Login component', () => {
- render();
- expect(screen.getByText('loginPage.title')).toBeInTheDocument();
-});
+describe('LinkWrapper', () => {
+ const mockOnClick = vi.fn();
-const mockWarning = 'This is a warning!';
+ const renderWithTheme = (component: React.ReactNode) => {
+ const theme = createTheme();
+ return render({component});
+ };
-test('Fetches and displays banner alerts', async () => {
- // Mock the fetch response
- const mockBannerResponse = [
- { enable: true, severity: 'warning', description: mockWarning },
- ];
- (fetch as Mock).mockResolvedValueOnce({
- json: vi.fn().mockResolvedValueOnce(mockBannerResponse),
+ beforeEach(() => {
+ vi.resetAllMocks();
});
- render();
+ it('renders children correctly', () => {
+ renderWithTheme(Test Link);
- await waitFor(() => {
- expect(screen.getByText(mockWarning)).toBeInTheDocument();
+ expect(screen.getByText('Test Link')).toBeInTheDocument();
});
-});
-test('Handles fetch error for alert message', async () => {
- (fetch as Mock).mockRejectedValueOnce(new Error('Fetch failed'));
+ it('calls onClick handler when clicked', () => {
+ renderWithTheme(Click Me);
+ const link = screen.getByText('Click Me');
- render();
+ fireEvent.click(link);
- // Optionally check if an error message or warning is displayed
- await waitFor(() => {
- expect(screen.queryByText(mockWarning)).not.toBeInTheDocument();
- });
-});
-
-test('Fetches IDP list on mount', async () => {
- const mockIDPListResponse = {
- identityProviders: [{ entityID: 'test-idp' }],
- richiediSpid: 'https://example.com/spid',
- };
- (fetch as Mock).mockResolvedValueOnce({
- json: vi.fn().mockResolvedValueOnce(mockIDPListResponse),
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
});
- render();
+ it('applies correct styles', () => {
+ renderWithTheme(
+ Styled Link
+ );
+ const link = screen.getByText('Styled Link');
- await waitFor(() => {
- expect(fetch).toHaveBeenCalledWith(ENV.JSON_URL.IDP_LIST);
+ expect(link).toHaveStyle({
+ cursor: 'pointer',
+ });
});
});
-test('Handles invalid client ID gracefully', async () => {
- window.history.pushState({}, '', `?client_id=invalidId`);
-
- render();
+describe('', () => {
+ const mockBannerQuery = {
+ isSuccess: true,
+ data: [{ enable: true, severity: 'warning', description: 'Test banner' }],
+ };
+ const mockClientQuery = {
+ isFetched: true,
+ data: {
+ friendlyName: 'Test Client',
+ logoUri: 'https://example.com/logo.png',
+ },
+ };
+ const mockIdpQuery = {
+ isLoading: false,
+ data: [
+ {
+ entityID: 'idp1',
+ name: 'IDP 1',
+ imageUrl: 'https://example.com/idp1.png',
+ },
+ ],
+ };
- await waitFor(() => {
- expect(screen.queryByAltText('Test Client')).not.toBeInTheDocument();
+ beforeEach(() => {
+ (useLoginData as Mock).mockReturnValue({
+ bannerQuery: mockBannerQuery,
+ clientQuery: mockClientQuery,
+ idpQuery: mockIdpQuery,
+ });
});
-});
-
-test('Clicking SPID button opens modal', () => {
- render();
- const buttonSpid = document.getElementById('spidButton');
- fireEvent.click(buttonSpid as HTMLElement);
-
- expect(screen.getByRole('dialog')).toBeInTheDocument(); // Check if modal opens
-});
-test('Clicking CIE button redirects correctly', () => {
- render();
- const buttonCIE = screen.getByRole('button', {
- name: 'loginPage.loginBox.cieLogin',
+ it('renders titles and descriptions', () => {
+ render();
+ expect(screen.getByText('loginPage.title')).toBeInTheDocument();
+ expect(screen.getByText('loginPage.description')).toBeInTheDocument();
});
- fireEvent.click(buttonCIE);
- expect(global.window.location.assign).toHaveBeenCalledWith(
- `${ENV.URL_API.AUTHORIZE}?idp=${ENV.SPID_CIE_ENTITY_ID}`
- );
-});
-
-test('Clicking terms and conditions link redirects correctly', () => {
- render();
-
- const termsConditionLink = screen.getByText(
- 'loginPage.privacyAndCondition.terms'
- );
- fireEvent.click(termsConditionLink);
-
- expect(global.window.location.assign).toHaveBeenCalledWith(
- ENV.URL_FOOTER.TERMS_AND_CONDITIONS
- );
-});
+ it('displays the client logo', () => {
+ render();
+ const logo = screen.getByAltText('Test Client');
+ expect(logo).toBeInTheDocument();
+ expect(logo).toHaveAttribute('src', 'https://example.com/logo.png');
+ });
-test('Clicking privacy link redirects correctly', () => {
- render();
+ it('shows a banner when bannerQuery is successful', () => {
+ render();
+ expect(screen.getByText('Test banner')).toBeInTheDocument();
+ });
- const privacyLink = screen.getByText('loginPage.privacyAndCondition.privacy');
- fireEvent.click(privacyLink);
+ it('opens the SpidModal on SPID button click', () => {
+ render();
+ const spidButton = screen.getByRole('button', { name: /SPID/i });
+ fireEvent.click(spidButton);
+ expect(
+ screen.getByRole('dialog', { name: 'spidSelect.modalTitle' })
+ ).toBeInTheDocument();
+ });
- expect(global.window.location.assign).toHaveBeenCalledWith(
- ENV.URL_FOOTER.PRIVACY_DISCLAIMER
- );
+ it('navigates to CIE login on CIE button click', async () => {
+ render();
+ const cieButton = screen.getByRole('button', { name: /CIE/i });
+ fireEvent.click(cieButton);
+
+ await waitFor(() => {
+ expect(trackEvent).toHaveBeenCalledWith(
+ 'LOGIN_IDP_SELECTED',
+ {
+ SPID_IDP_NAME: 'CIE',
+ SPID_IDP_ID: ENV.SPID_CIE_ENTITY_ID,
+ FORWARD_PARAMETERS: expect.any(String),
+ },
+ expect.any(Function)
+ );
+ });
+ });
});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.tsx
deleted file mode 100644
index d2dbc6e7..00000000
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.tsx
+++ /dev/null
@@ -1,119 +0,0 @@
-import { Button, Dialog, Grid, Icon, Typography } from '@mui/material';
-import { useTranslation } from 'react-i18next';
-
-import { IdentityProvider, IdentityProviders } from '../../utils/IDPS';
-import { trackEvent } from '../../services/analyticsService';
-import { forwardSearchParams } from '../../utils/utils';
-import { ENV } from '../../utils/env';
-import { ImageWithFallback } from '../../components/ImageFallback';
-import { IDP_PLACEHOLDER_IMG } from '../../utils/constants';
-
-type Props = {
- openSpidModal: boolean;
- setOpenSpidModal: (openDialog: boolean) => void;
- idpList: IdentityProviders;
-};
-
-export const getSPID = (IDP: IdentityProvider) => {
- const params = forwardSearchParams(IDP.entityID);
- const redirectUrl = `${ENV.URL_API.AUTHORIZE}?${params}`;
- trackEvent(
- 'LOGIN_IDP_SELECTED',
- {
- SPID_IDP_NAME: IDP.name,
- SPID_IDP_ID: IDP.entityID,
- FORWARD_PARAMETERS: params,
- },
- () => window.location.assign(redirectUrl)
- );
-};
-
-const IdpListSelection = ({
- identityProviders,
-}: {
- identityProviders: Array;
-}) =>
- identityProviders?.map((IDP, i) => (
-
-
-
- ));
-
-const SpidModal = ({ openSpidModal, setOpenSpidModal, idpList }: Props) => {
- const { t } = useTranslation();
-
- return (
-
- );
-};
-
-export default SpidModal;
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidSelect.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidSelect.tsx
deleted file mode 100644
index a8dcf157..00000000
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidSelect.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-import { Fragment } from 'react';
-import Icon from '@mui/material/Icon';
-import Grid from '@mui/material/Grid';
-import Typography from '@mui/material/Typography';
-import Button from '@mui/material/Button';
-import { useTranslation } from 'react-i18next';
-
-import { IdentityProvider, IdentityProviders } from '../../utils/IDPS';
-import SpidBig from '../../assets/spid_big.svg';
-import { ENV } from '../../utils/env';
-import { IDP_PLACEHOLDER_IMG } from '../../utils/constants';
-import { trackEvent } from '../../services/analyticsService';
-import { forwardSearchParams } from '../../utils/utils';
-import { ImageWithFallback } from '../../components/ImageFallback';
-
-type Props = {
- onBack: () => void;
- idpList: IdentityProviders;
-};
-
-export const getSPID = (IDP: IdentityProvider) => {
- const params = forwardSearchParams(IDP.entityID);
- const redirectUrl = `${ENV.URL_API.AUTHORIZE}?${params}`;
- trackEvent(
- 'LOGIN_IDP_SELECTED',
- {
- SPID_IDP_NAME: IDP.name,
- SPID_IDP_ID: IDP.entityID,
- FORWARD_PARAMETERS: params,
- },
- () => window.location.assign(redirectUrl)
- );
-};
-
-export const SpidList = ({ idpList }: { idpList: IdentityProviders }) =>
- idpList.identityProviders.map((IDP, i) => (
-
-
-
- ));
-
-const SpidSelect = ({ onBack, idpList }: Props) => {
- const { t } = useTranslation();
-
- return (
-
-
-
-
-
-
-
-
-
-
- {t('spidSelect.title')}
-
-
-
-
- {idpList?.identityProviders?.length ? (
-
- ) : (
-
- {t('spidSelect.placeholder')}
-
- )}
-
-
-
-
-
-
-
-
- );
-};
-
-export default SpidSelect;
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton/CieButton.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton/CieButton.test.tsx
new file mode 100644
index 00000000..26208260
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton/CieButton.test.tsx
@@ -0,0 +1,36 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, it, vi } from 'vitest';
+import { CieButtonProps, CieButton } from '../CieButton';
+import { i18nTestSetup } from '../../../../__tests__/i18nTestSetup';
+
+describe('CieButton', () => {
+ const BUTTON_TEXT = 'CIE Login';
+ const onClickMock = vi.fn();
+
+ const renderComponent = (props: Partial = {}) => {
+ i18nTestSetup({ 'loginPage.loginBox.cieLogin': BUTTON_TEXT });
+ render();
+ };
+
+ it('renders the button with the correct text', () => {
+ renderComponent();
+ expect(
+ screen.getByRole('button', { name: BUTTON_TEXT })
+ ).toBeInTheDocument();
+ });
+
+ it('displays the CIE icon', () => {
+ renderComponent();
+ const icon = screen.getByAltText('CIE Icon');
+ expect(icon).toBeInTheDocument();
+ expect(icon).toHaveAttribute('src', expect.stringContaining('CIEIcon'));
+ });
+
+ it('calls the onClick handler when clicked', async () => {
+ renderComponent();
+ const button = screen.getByRole('button', { name: BUTTON_TEXT });
+ await userEvent.click(button);
+ expect(onClickMock).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton/index.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton/index.tsx
new file mode 100644
index 00000000..fab51ecf
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/CieButton/index.tsx
@@ -0,0 +1,33 @@
+import Icon from '@mui/material/Icon';
+import { useTranslation } from 'react-i18next';
+import Button from '@mui/material/Button';
+import CIEIcon from '../../../../assets/CIEIcon.svg';
+
+export type CieButtonProps = {
+ onClick: () => void;
+};
+
+export const CieIconWrapper = () => (
+
+
+
+);
+
+export const CieButton = ({ onClick }: CieButtonProps) => {
+ const { t } = useTranslation();
+
+ return (
+ }
+ onClick={onClick}
+ >
+ {t('loginPage.loginBox.cieLogin')}
+
+ );
+};
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton/SpidButton.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton/SpidButton.test.tsx
new file mode 100644
index 00000000..43ca6bf7
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton/SpidButton.test.tsx
@@ -0,0 +1,48 @@
+import { render, screen, fireEvent } from '@testing-library/react';
+import { vi } from 'vitest';
+import { SpidButton, SpidButtonProps } from '../SpidButton';
+import { i18nTestSetup } from '../../../../__tests__/i18nTestSetup';
+
+const TEST_ID = 'spidButton';
+
+describe('SpidButton Component', () => {
+ const onClickMock = vi.fn();
+
+ beforeAll(() => {
+ i18nTestSetup({
+ 'loginPage.loginBox.spidLogin': 'Login with SPID',
+ });
+ });
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ const renderComponent = (props: SpidButtonProps) =>
+ render();
+
+ it('renders the button with the correct text', () => {
+ renderComponent({ onClick: onClickMock });
+
+ const button = screen.getByTestId(TEST_ID);
+ expect(button).toBeInTheDocument();
+ expect(button).toHaveTextContent('Login with SPID');
+ });
+
+ it('triggers the onClick function when clicked', () => {
+ renderComponent({ onClick: onClickMock });
+
+ const button = screen.getByTestId(TEST_ID);
+ fireEvent.click(button);
+
+ expect(onClickMock).toHaveBeenCalledOnce();
+ });
+
+ it('displays the SPID icon', () => {
+ renderComponent({ onClick: onClickMock });
+
+ const icon = screen.getByAltText('SPID Icon');
+ expect(icon).toBeInTheDocument();
+ expect(icon).toHaveAttribute('src', expect.stringContaining('SpidIcon'));
+ });
+});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton/index.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton/index.tsx
new file mode 100644
index 00000000..bd527deb
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidButton/index.tsx
@@ -0,0 +1,46 @@
+import LoadingButton from '@mui/lab/LoadingButton';
+import Icon from '@mui/material/Icon';
+import Typography from '@mui/material/Typography';
+import { theme } from '@pagopa/mui-italia/dist/theme';
+import { useTranslation } from 'react-i18next';
+import SpidIcon from '../../../../assets/SpidIcon.svg';
+
+export const SpidIconWrapper = () => (
+
+
+
+);
+
+export type SpidButtonProps = {
+ onClick: () => void;
+};
+
+export const SpidButton = ({ onClick }: SpidButtonProps) => {
+ const { t } = useTranslation();
+
+ return (
+ }
+ sx={{
+ borderRadius: '4px',
+ width: '100%',
+ marginBottom: '5px',
+ }}
+ variant="contained"
+ >
+
+ {t('loginPage.loginBox.spidLogin')}
+
+
+ );
+};
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidModal/SpidModal.test.tsx
similarity index 83%
rename from src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.test.tsx
rename to src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidModal/SpidModal.test.tsx
index 4e182917..4e9e500b 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidModal.test.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidModal/SpidModal.test.tsx
@@ -1,12 +1,12 @@
import { render, screen, fireEvent } from '@testing-library/react';
-import { IdentityProviders } from '../../utils/IDPS';
-import { trackEvent } from '../../services/analyticsService';
-import { forwardSearchParams } from '../../utils/utils';
-import SpidModal from './SpidModal';
+import { IdentityProviders } from '../../../../utils/IDPS';
+import { trackEvent } from '../../../../services/analyticsService';
+import { forwardSearchParams } from '../../../../utils/utils';
+import SpidModal from '.././SpidModal';
-vi.mock('../../services/analyticsService');
-vi.mock('../../utils/utils', () => ({
+vi.mock('../../../../services/analyticsService');
+vi.mock('../../../../utils/utils', () => ({
forwardSearchParams: vi.fn(() => 'testParams'),
}));
@@ -25,6 +25,7 @@ describe('SpidModal', () => {
it('renders the modal with identity providers', () => {
render(
void;
+ idpList?: IdentityProviders;
+ loading?: boolean;
+};
+
+export const NoProviders = () => {
+ const { t } = useTranslation();
+
+ return (
+
+ {t('spidSelect.placeholder')}
+
+ );
+};
+
+export const ContentSelection = ({
+ idpList,
+}: {
+ idpList?: IdentityProviders;
+}) => {
+ const noSpidProvidersFound = !idpList?.identityProviders?.length;
+
+ return noSpidProvidersFound ? (
+
+ ) : (
+
+ );
+};
+
+const SpidModal = ({
+ openSpidModal,
+ setOpenSpidModal,
+ idpList,
+ loading,
+}: Props) => {
+ const { t } = useTranslation();
+
+ return (
+
+ );
+};
+
+export default SpidModal;
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidSelect.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSelect/SpidSelect.test.tsx
similarity index 84%
rename from src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidSelect.test.tsx
rename to src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSelect/SpidSelect.test.tsx
index fd57ce2f..3259c8e4 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/SpidSelect.test.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSelect/SpidSelect.test.tsx
@@ -1,16 +1,15 @@
+/* eslint-disable functional/immutable-data */
import { render, screen, fireEvent } from '@testing-library/react';
import { vi } from 'vitest';
-import { ENV } from '../../utils/env';
-import SpidSelect from './SpidSelect';
+import { ENV } from '../../../../utils/env';
+import SpidSelect from '../SpidSelect';
const oldWindowLocation = global.window.location;
beforeAll(() => {
- // eslint-disable-next-line functional/immutable-data
Object.defineProperty(window, 'location', { value: { assign: vi.fn() } });
});
afterAll(() => {
- // eslint-disable-next-line functional/immutable-data
Object.defineProperty(window, 'location', { value: oldWindowLocation });
});
const idpList = {
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSelect/index.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSelect/index.tsx
new file mode 100644
index 00000000..fe135a95
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSelect/index.tsx
@@ -0,0 +1,76 @@
+import { Fragment } from 'react';
+import Grid from '@mui/material/Grid';
+import Typography from '@mui/material/Typography';
+import Button from '@mui/material/Button';
+import { useTranslation } from 'react-i18next';
+import { IdentityProviders } from '../../../../utils/IDPS';
+import SpidBig from '../../../../assets/spid_big.svg';
+import { SpidSkeleton } from '../SpidSkeleton';
+import { ContentSelection } from '../SpidModal';
+
+type SpidSelectProps = {
+ onBack: () => void;
+ idpList?: IdentityProviders;
+ loading?: boolean;
+};
+
+const SpidSelect = ({ onBack, idpList, loading }: SpidSelectProps) => {
+ const { t } = useTranslation();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ {t('spidSelect.title')}
+
+
+ {loading ? : }
+
+
+
+
+
+
+ );
+};
+
+export default SpidSelect;
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSelection.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSelection.tsx
new file mode 100644
index 00000000..7dbe7733
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSelection.tsx
@@ -0,0 +1,69 @@
+import { Button, Grid, Icon } from '@mui/material';
+import { ImageWithFallback } from '../../../components/ImageFallback';
+import { IDP_PLACEHOLDER_IMG } from '../../../utils/constants';
+import { IdentityProvider } from '../../../utils/IDPS';
+import { trackEvent } from '../../../services/analyticsService';
+import { ENV } from '../../../utils/env';
+import { forwardSearchParams } from '../../../utils/utils';
+
+export const getSPID = (IDP: IdentityProvider) => {
+ const params = forwardSearchParams(IDP.entityID);
+ const redirectUrl = `${ENV.URL_API.AUTHORIZE}?${params}`;
+ trackEvent(
+ 'LOGIN_IDP_SELECTED',
+ {
+ SPID_IDP_NAME: IDP.name,
+ SPID_IDP_ID: IDP.entityID,
+ FORWARD_PARAMETERS: params,
+ },
+ () => window.location.assign(redirectUrl)
+ );
+};
+
+export const SpidSelection = ({
+ identityProviders,
+}: {
+ identityProviders: Array;
+}) => (
+
+
+ {identityProviders?.map((IDP, i) => (
+
+
+
+ ))}
+
+
+);
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSkeleton.test.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSkeleton.test.tsx
new file mode 100644
index 00000000..f06c3034
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSkeleton.test.tsx
@@ -0,0 +1,40 @@
+import { describe, it, expect } from 'vitest';
+import { render, screen } from '@testing-library/react';
+import { SpidSkeleton } from './SpidSkeleton';
+
+describe('SpidSkeleton', () => {
+ it('renders the SpidSkeleton component with the correct structure', () => {
+ render();
+
+ // Check that the component has the correct role and aria-label
+ const skeletonContainer = screen.getByRole('status', { name: 'loading' });
+ expect(skeletonContainer).toBeInTheDocument();
+
+ // Check that there are two primary Stack containers
+ const stackContainers =
+ skeletonContainer.querySelectorAll('.MuiStack-root');
+ expect(stackContainers).toHaveLength(2);
+
+ // Verify the presence of six Skeleton components (3 in each stack)
+ const skeletons = screen.getAllByRole('presentation');
+ expect(skeletons).toHaveLength(6);
+
+ // Check that each Skeleton component has the correct attributes
+ skeletons.forEach((skeleton) => {
+ expect(skeleton).toHaveAttribute('aria-busy', 'true');
+ });
+ });
+
+ it('has the correct styles for each Skeleton component', () => {
+ render();
+ const skeletons = screen.getAllByRole('presentation');
+
+ skeletons.forEach((skeleton) => {
+ expect(skeleton).toHaveStyle({
+ borderRadius: '4px',
+ height: '48px',
+ width: '148px',
+ });
+ });
+ });
+});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSkeleton.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSkeleton.tsx
new file mode 100644
index 00000000..e422efd0
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/components/SpidSkeleton.tsx
@@ -0,0 +1,35 @@
+import { Skeleton, Stack } from '@mui/material';
+
+export const SpidSkeleton = () => {
+ const ImgSkeleton = () => (
+
+ );
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.tsx b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/index.tsx
similarity index 54%
rename from src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.tsx
rename to src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/index.tsx
index 2ba45d8d..9e69343b 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/Login.tsx
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/pages/login/index.tsx
@@ -1,51 +1,23 @@
-import { useEffect, useState } from 'react';
-import Button from '@mui/material/Button';
+import { useState } from 'react';
import Link from '@mui/material/Link';
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
-import Icon from '@mui/material/Icon';
import { Alert } from '@mui/material';
import Typography from '@mui/material/Typography';
import { Trans, useTranslation } from 'react-i18next';
import { theme } from '@pagopa/mui-italia';
import Layout from '../../components/Layout';
-import SpidIcon from '../../assets/SpidIcon.svg';
-import CIEIcon from '../../assets/CIEIcon.svg';
import { ENV } from '../../utils/env';
import { IDP_PLACEHOLDER_IMG } from '../../utils/constants';
import { trackEvent } from '../../services/analyticsService';
import { forwardSearchParams } from '../../utils/utils';
-import type { IdentityProvider, IdentityProviders } from '../../utils/IDPS';
import { ImageWithFallback } from '../../components/ImageFallback';
-import SpidSelect from './SpidSelect';
-import SpidModal from './SpidModal';
-
-type BannerContent = {
- enable: boolean;
- severity: 'warning' | 'error' | 'info' | 'success';
- description: string;
-};
-
-type Client = {
- clientID: string;
- friendlyName: string;
- logoUri: string;
- policyUri: string;
- tosUri: string;
-};
-
-export const SpidIconWrapper = () => (
-
-
-
-);
-
-export const CieIconWrapper = () => (
-
-
-
-);
+import SpidModal from './components/SpidModal';
+import { useLoginData } from '../../hooks/useLoginData';
+import { SpidButton } from './components/SpidButton';
+import { CieButton } from './components/CieButton';
+import SpidSelect from './components/SpidSelect';
export const LinkWrapper = ({
onClick,
@@ -55,7 +27,6 @@ export const LinkWrapper = ({
children?: React.ReactNode;
}) => (
{
- const [showIDPS, setShowIDPS] = useState(false);
- const [bannerContent, setBannerContent] = useState>();
const [openSpidModal, setOpenSpidModal] = useState(false);
- const [idpList, setIdpList] = useState({
- identityProviders: [],
- richiediSpid: '',
- });
- const [clientData, setClientData] = useState();
-
- const mapToArray = (json: Record) => {
- const mapped = Object.values(json);
- setBannerContent(mapped as Array);
- };
-
- const alertMessage = async (loginBanner: string) => {
- try {
- const response = await fetch(loginBanner);
- const res = await response.json();
- mapToArray(res);
- } catch (error) {
- console.error(error);
- }
- };
-
- const getIdpList = async (idpListUrl: string) => {
- try {
- const response = await fetch(idpListUrl);
- const res: Array = await response.json();
- const assetsIDPUrl = ENV.URL_FE.ASSETS + '/idps';
- const rawIDPS = res
- .map((i) => ({
- ...i,
- imageUrl: `${assetsIDPUrl}/${btoa(i.entityID)}.png`,
- }))
- .sort(() => 0.5 - Math.random());
- const IDPS: {
- identityProviders: Array;
- richiediSpid: string;
- } = {
- identityProviders: rawIDPS,
- richiediSpid: 'https://www.spid.gov.it/cos-e-spid/come-attivare-spid/',
- };
- setIdpList(IDPS);
- } catch (error) {
- console.error(error);
- }
- };
-
- const getClientData = async (clientBaseListUrl: string) => {
- try {
- const query = new URLSearchParams(window.location.search);
- const clientID = query.get('client_id');
-
- if (clientID && clientID.match(/^[A-Za-z0-9_-]{43}$/)) {
- const clientListUrl = `${clientBaseListUrl}/${clientID}`;
- const response = await fetch(clientListUrl);
- const res: Client = await response.json();
- setClientData(res);
- } else {
- console.warn('no client_id supplied, or not valid 32bit Base64Url');
- }
- } catch (error) {
- console.error(error);
- }
- };
-
- useEffect(() => {
- void alertMessage(ENV.JSON_URL.ALERT);
- void getIdpList(ENV.JSON_URL.IDP_LIST);
- void getClientData(ENV.JSON_URL.CLIENT_BASE_URL);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ const [showIDPS, setShowIDPS] = useState(false);
+ const { bannerQuery, clientQuery, idpQuery } = useLoginData();
const { t } = useTranslation();
+ const columnsOccupiedByAlert = 5;
+
const goCIE = () => {
const params = forwardSearchParams(ENV.SPID_CIE_ENTITY_ID);
const redirectUrl = `${ENV.URL_API.AUTHORIZE}?${params}`;
@@ -169,23 +73,27 @@ const Login = () => {
const redirectPrivacyLink = () =>
trackEvent('LOGIN_PRIVACY', { SPID_IDP_NAME: 'LOGIN_PRIVACY' }, () =>
window.location.assign(
- clientData?.policyUri || ENV.URL_FOOTER.PRIVACY_DISCLAIMER
+ clientQuery.data?.policyUri || ENV.URL_FOOTER.PRIVACY_DISCLAIMER
)
);
const redirectToTOS = () =>
trackEvent('LOGIN_TOS', { SPID_IDP_NAME: 'LOGIN_TOS' }, () =>
window.location.assign(
- clientData?.tosUri || ENV.URL_FOOTER.TERMS_AND_CONDITIONS
+ clientQuery.data?.tosUri || ENV.URL_FOOTER.TERMS_AND_CONDITIONS
)
);
if (showIDPS) {
- return ;
+ return (
+
+ );
}
- const columnsOccupiedByAlert = 5;
-
return (
@@ -219,15 +127,15 @@ const Login = () => {
- {clientData?.logoUri && (
-
-
+
+
+ {clientQuery.isFetched && (
{
maxHeight: '100px',
objectFit: 'cover',
}}
- src={clientData?.logoUri}
- alt={clientData?.friendlyName}
+ src={clientQuery.data?.logoUri}
+ alt={clientQuery.data?.friendlyName || 'PagoPa Logo'}
placeholder={IDP_PLACEHOLDER_IMG}
/>
-
+ )}
- )}
+
{ENV.ENABLED_SPID_TEMPORARY_SELECT && (
@@ -262,8 +170,8 @@ const Login = () => {
)}
- {bannerContent &&
- bannerContent.map(
+ {bannerQuery.isSuccess &&
+ bannerQuery.data.map(
(bc, index) =>
bc.enable && (
@@ -301,45 +209,14 @@ const Login = () => {
-
+ setOpenSpidModal(true)} />
- }
- onClick={goCIE}
- >
- {t('loginPage.loginBox.cieLogin')}
-
+
@@ -361,8 +238,16 @@ const Login = () => {
privacyLink: `<1>${t('loginPage.privacyAndCondition.privacy')}1>`,
}}
components={[
- ,
- ,
+ ,
+ ,
]}
/>
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/services/api.test.ts b/src/oneid/oneid-ecs-core/src/main/webui/src/services/api.test.ts
new file mode 100644
index 00000000..c659d376
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/services/api.test.ts
@@ -0,0 +1,142 @@
+/* eslint-disable sonarjs/no-duplicate-string */
+/* eslint-disable functional/immutable-data */
+import { describe, it, expect, vi, beforeEach, Mock } from 'vitest';
+import { ENV } from '../utils/env';
+import { IdentityProvider } from '../utils/IDPS';
+import { getIdpList, getClientData, fetchBannerContent } from './api';
+
+vi.stubGlobal('fetch', vi.fn());
+
+describe('Utils functions', () => {
+ beforeEach(() => {
+ vi.spyOn(global.Math, 'random').mockReturnValue(0.5);
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ describe('getIdpList', () => {
+ const mockIDPList: Array> = [
+ { entityID: 'idp1', name: 'IDP 1', identifier: 'idp-identifier-1' },
+ { entityID: 'idp2', name: 'IDP 2', identifier: 'idp-identifier-2' },
+ ];
+
+ it('returns a enhanced IDP list', async () => {
+ (global.fetch as Mock).mockResolvedValueOnce({
+ ok: true,
+ json: async () => mockIDPList,
+ });
+
+ const result = await getIdpList('https://example.com/idp-list');
+ const assetsIDPUrl = ENV.URL_FE.ASSETS + '/idps';
+
+ expect(result.identityProviders).toHaveLength(2);
+ expect(result.identityProviders[0]).toHaveProperty(
+ 'imageUrl',
+ `${assetsIDPUrl}/${btoa(mockIDPList[0].entityID)}.png`
+ );
+ });
+
+ it('throws an error if the fetch fails', async () => {
+ (global.fetch as Mock).mockResolvedValueOnce({
+ ok: false,
+ statusText: 'Not Found',
+ });
+
+ await expect(getIdpList('https://example.com/idp-list')).rejects.toThrow(
+ 'Failed to fetch IDP list: Not Found'
+ );
+ });
+ });
+
+ describe('getClientData', () => {
+ const clientID = '0000000000000000000000000000000000000000000';
+
+ const mockClientData = {
+ clientID: clientID,
+ friendlyName: 'Test Client',
+ logoUri: 'https://example.com/logo.png',
+ policyUri: 'https://example.com/policy',
+ tosUri: 'https://example.com/tos',
+ };
+
+ beforeEach(() => {
+ vi.stubGlobal('window', {
+ location: { search: `?client_id=${clientID}` },
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('fetches client data successfully', async () => {
+ (global.fetch as Mock).mockResolvedValueOnce({
+ ok: true,
+ json: async () => mockClientData,
+ });
+
+ const result = await getClientData('https://example.com/clients');
+ expect(result).toEqual(mockClientData);
+ });
+
+ it('throws an error if client_id is invalid or missing', async () => {
+ vi.stubGlobal('window', {
+ location: { search: `?client_id=` },
+ });
+
+ await expect(
+ getClientData('https://example.com/clients')
+ ).rejects.toThrow('Invalid or missing client_id');
+ });
+
+ it('throws an error if the fetch fails', async () => {
+ (global.fetch as Mock).mockResolvedValueOnce({
+ ok: false,
+ statusText: 'Unauthorized',
+ });
+
+ await expect(
+ getClientData('https://example.com/clients')
+ ).rejects.toThrow('Failed to fetch client data: Unauthorized');
+ });
+ });
+
+ describe('fetchBannerContent', () => {
+ const mockBannerContent = {
+ banner1: {
+ enable: true,
+ severity: 'info',
+ description: 'This is a test banner',
+ },
+ banner2: {
+ enable: false,
+ severity: 'warning',
+ description: 'This is another test banner',
+ },
+ };
+
+ it('returns an array of banner content', async () => {
+ (global.fetch as Mock).mockResolvedValueOnce({
+ ok: true,
+ json: async () => mockBannerContent,
+ });
+
+ const result = await fetchBannerContent('https://example.com/banner');
+ expect(result).toHaveLength(2);
+ expect(result[0]).toHaveProperty('description', 'This is a test banner');
+ });
+
+ it('throws an error if the fetch fails', async () => {
+ (global.fetch as Mock).mockResolvedValueOnce({
+ ok: false,
+ statusText: 'Forbidden',
+ });
+
+ await expect(
+ fetchBannerContent('https://example.com/banner')
+ ).rejects.toThrow('Failed to fetch banner content: Forbidden');
+ });
+ });
+});
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/src/services/api.ts b/src/oneid/oneid-ecs-core/src/main/webui/src/services/api.ts
new file mode 100644
index 00000000..b85aeb44
--- /dev/null
+++ b/src/oneid/oneid-ecs-core/src/main/webui/src/services/api.ts
@@ -0,0 +1,73 @@
+import { ENV } from '../utils/env';
+import { IdentityProvider } from '../utils/IDPS';
+
+export type BannerContent = {
+ enable: boolean;
+ severity: 'warning' | 'error' | 'info' | 'success';
+ description: string;
+};
+
+export type Client = {
+ clientID: string;
+ friendlyName: string;
+ logoUri: string;
+ policyUri: string;
+ tosUri: string;
+};
+
+export type IDPList = {
+ identityProviders: Array;
+ richiediSpid: string;
+};
+
+export const getIdpList = async (idpListUrl: string) => {
+ const response = await fetch(idpListUrl);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch IDP list: ${response.statusText}`);
+ }
+
+ const res: Array = await response.json();
+ const assetsIDPUrl = ENV.URL_FE.ASSETS + '/idps';
+ const rawIDPS = res
+ .map((i) => ({
+ ...i,
+ imageUrl: `${assetsIDPUrl}/${btoa(i.entityID)}.png`,
+ }))
+ .sort(() => 0.5 - Math.random());
+
+ const out: IDPList = {
+ identityProviders: rawIDPS,
+ richiediSpid: 'https://www.spid.gov.it/cos-e-spid/come-attivare-spid/',
+ };
+
+ return out;
+};
+
+export const getClientData = async (clientBaseListUrl: string) => {
+ const query = new URLSearchParams(window.location.search);
+ const clientID = query.get('client_id');
+
+ if (!clientID || !clientID.match(/^[A-Za-z0-9_-]{43}$/)) {
+ throw new Error('Invalid or missing client_id');
+ }
+
+ const clientListUrl = `${clientBaseListUrl}/${clientID}`;
+ const response = await fetch(clientListUrl);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch client data: ${response.statusText}`);
+ }
+
+ return await response.json();
+};
+
+export const fetchBannerContent = async (
+ loginBannerUrl: string
+): Promise> => {
+ const response = await fetch(loginBannerUrl);
+ if (!response.ok) {
+ throw new Error(`Failed to fetch banner content: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ return Object.values(data) as Array;
+};
diff --git a/src/oneid/oneid-ecs-core/src/main/webui/yarn.lock b/src/oneid/oneid-ecs-core/src/main/webui/yarn.lock
index 6fd21050..54a24cbb 100644
--- a/src/oneid/oneid-ecs-core/src/main/webui/yarn.lock
+++ b/src/oneid/oneid-ecs-core/src/main/webui/yarn.lock
@@ -984,6 +984,18 @@
resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.23.0.tgz#54e3562ebd264ef5839f8091618310c40d43d8a9"
integrity sha512-lqCK5GQC8fNo0+JvTSxcG7YB1UKYp8yrNLhsArlvPWN+16ovSZgoehlVHg6X0sSWPUkpjRBR5TuR12ZugowZ4g==
+"@tanstack/query-core@5.62.2":
+ version "5.62.2"
+ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.2.tgz#4eef3201422f246788fb41d01662c2dea3136d9a"
+ integrity sha512-LcwVcC5qpsDpHcqlXUUL5o9SaOBwhNkGeV+B06s0GBoyBr8FqXPuXT29XzYXR36lchhnerp6XO+CWc84/vh7Zg==
+
+"@tanstack/react-query@^5.62.2":
+ version "5.62.2"
+ resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.2.tgz#fbcb8f991ddcf484ce7968fb58bb4790d6c98cd3"
+ integrity sha512-fkTpKKfwTJtVPKVR+ag7YqFgG/7TRVVPzduPAUF9zRCiiA8Wu305u+KJl8rCrh98Qce77vzIakvtUyzWLtaPGA==
+ dependencies:
+ "@tanstack/query-core" "5.62.2"
+
"@testing-library/dom@^10.2.0":
version "10.2.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-10.2.0.tgz#d3b22515bc0603a06f119c6ae6670669c3f2085f"