From 60b18e5fdd9c996e7ace553ea49c96dc4a0d5410 Mon Sep 17 00:00:00 2001 From: Francesco Bianchi <105631409+fbianchicodermine@users.noreply.github.com> Date: Fri, 21 Feb 2025 12:57:08 +0100 Subject: [PATCH 1/3] feat(pn-13849): view notification from retrieval Id (#1466) --- .../pn-commons/src/models/MixpanelEvents.ts | 4 +- .../utility/__test__/routes.utility.test.ts | 39 +++- packages/pn-commons/src/utility/index.ts | 3 +- .../pn-commons/src/utility/routes.utility.ts | 8 + .../pn-pa-webapp/src/__mocks__/Auth.mock.ts | 3 +- .../pn-pa-webapp/src/api/auth/Auth.api.ts | 2 +- packages/pn-pa-webapp/src/models/user.ts | 24 +++ .../pn-pa-webapp/src/redux/auth/actions.ts | 3 +- .../pn-pa-webapp/src/redux/auth/reducers.ts | 3 +- packages/pn-pa-webapp/src/redux/auth/types.ts | 25 --- .../src/hooks/useRapidAccessParam.ts | 10 + .../src/pages/login/Login.tsx | 12 +- .../src/pages/login/__test__/Login.test.tsx | 28 ++- .../src/pages/logout/Logout.tsx | 4 +- .../src/pages/logout/__test__/Logout.test.tsx | 8 +- .../src/pages/success/Success.tsx | 14 +- .../pages/success/__test__/Success.test.tsx | 25 ++- .../src/utility/__test__/storage.test.tsx | 21 +- .../src/utility/storage.ts | 35 ++- .../pn-personafisica-webapp/openapitools.json | 2 +- .../src/__mocks__/Auth.mock.ts | 16 +- .../src/api/auth/Auth.api.ts | 60 ++++-- .../src/api/auth/__test__/Auth.api.test.ts | 43 +++- .../src/hooks/useRapidAccessParam.ts | 10 + .../src/models/NotificationDetail.ts | 6 +- .../src/models/User.ts | 42 ++++ .../src/navigation/AARGuard.tsx | 102 --------- .../src/navigation/RapidAccessGuard.tsx | 126 +++++++++++ .../src/navigation/RouteGuard.tsx | 9 +- .../src/navigation/SessionGuard.tsx | 12 +- .../src/navigation/__test__/AARGuard.test.tsx | 167 -------------- .../__test__/RapidAccessGuard.test.tsx | 203 ++++++++++++++++++ .../__test__/navigation.utility.test.ts | 31 +-- .../src/navigation/navigation.utility.ts | 8 +- .../src/navigation/routes.const.ts | 2 - .../src/navigation/routes.tsx | 4 +- .../src/pages/NotificationDetail.page.tsx | 18 +- .../__test__/NotificationDetail.page.test.tsx | 26 ++- .../src/redux/auth/__test__/reducers.test.ts | 2 - .../src/redux/auth/actions.ts | 9 +- .../src/redux/auth/reducers.ts | 11 +- .../src/redux/auth/types.ts | 17 -- .../notification/__test__/reducers.test.ts | 18 ++ .../src/redux/notification/actions.ts | 27 ++- .../src/redux/notification/reducers.ts | 6 + .../SendNotificationDetailStrategy.ts | 10 +- .../MixpanelUtils/Strategies/TechStrategy.ts | 11 +- .../SendNotificationDetailStrategy.test.ts | 5 +- .../Strategies/__test__/TechStrategy.test.ts | 14 +- .../src/utility/notification.utility.ts | 15 +- .../pn-personagiuridica-webapp/src/App.tsx | 2 +- .../src/__mocks__/Auth.mock.ts | 2 +- .../src/__test__/App.test.tsx | 2 +- .../src/api/auth/Auth.api.ts | 2 +- .../Deleghe/DelegationsElements.tsx | 2 +- .../IntegrazioneApi/VirtualKeyContextMenu.tsx | 2 +- .../IntegrazioneApi/VirtualKeys.tsx | 2 +- .../IntegrazioneApi/VirtualKeysTable.tsx | 2 +- .../{redux/auth/types.ts => models/User.ts} | 2 +- .../src/navigation/routes.tsx | 2 +- .../src/pages/ApiIntegration.page.tsx | 2 +- .../src/pages/NotificationDetail.page.tsx | 2 +- .../src/pages/Notifiche.page.tsx | 2 +- .../src/redux/auth/__test__/reducers.test.ts | 2 +- .../src/redux/auth/actions.ts | 2 +- .../src/redux/auth/reducers.ts | 2 +- 66 files changed, 860 insertions(+), 475 deletions(-) delete mode 100644 packages/pn-pa-webapp/src/redux/auth/types.ts create mode 100644 packages/pn-personafisica-login/src/hooks/useRapidAccessParam.ts create mode 100644 packages/pn-personafisica-webapp/src/hooks/useRapidAccessParam.ts create mode 100644 packages/pn-personafisica-webapp/src/models/User.ts delete mode 100644 packages/pn-personafisica-webapp/src/navigation/AARGuard.tsx create mode 100644 packages/pn-personafisica-webapp/src/navigation/RapidAccessGuard.tsx delete mode 100644 packages/pn-personafisica-webapp/src/navigation/__test__/AARGuard.test.tsx create mode 100644 packages/pn-personafisica-webapp/src/navigation/__test__/RapidAccessGuard.test.tsx delete mode 100644 packages/pn-personafisica-webapp/src/redux/auth/types.ts rename packages/pn-personagiuridica-webapp/src/{redux/auth/types.ts => models/User.ts} (99%) diff --git a/packages/pn-commons/src/models/MixpanelEvents.ts b/packages/pn-commons/src/models/MixpanelEvents.ts index 46dad46d4c..5aa9493ffc 100644 --- a/packages/pn-commons/src/models/MixpanelEvents.ts +++ b/packages/pn-commons/src/models/MixpanelEvents.ts @@ -79,6 +79,8 @@ export type EventNotificationsListType = { cancelled_count: number; }; +export type EventNotificationSource = '3Papp' | 'QRcode' | 'LISTA_NOTIFICHE'; + export type EventNotificationDetailType = { notification_owner: boolean; notification_status: NotificationStatus; @@ -88,7 +90,7 @@ export type EventNotificationDetailType = { count_payment: number; contains_f24: 'yes' | 'no'; first_time_opening: boolean; - source: 'QRcode' | 'LISTA_NOTIFICHE'; + source: EventNotificationSource; }; export type EventMandateNotificationsListType = { diff --git a/packages/pn-commons/src/utility/__test__/routes.utility.test.ts b/packages/pn-commons/src/utility/__test__/routes.utility.test.ts index c7f5b14fb7..d5570ca582 100644 --- a/packages/pn-commons/src/utility/__test__/routes.utility.test.ts +++ b/packages/pn-commons/src/utility/__test__/routes.utility.test.ts @@ -1,4 +1,4 @@ -import { compileRoute } from '../routes.utility'; +import { AppRouteParams, compileRoute, getRapidAccessParam } from '../routes.utility'; describe('Routes utility', () => { it('Route with no params and no path', () => { @@ -76,4 +76,41 @@ describe('Routes utility', () => { }); expect(route).toEqual('/prefix/v1'); }); + + // Tests for getRapidAccessParam + it('getRapidAccessParam returns correct param when present', () => { + const params = new URLSearchParams({ aar: 'value' }); + const result = getRapidAccessParam(params); + expect(result).toEqual([AppRouteParams.AAR, 'value']); + }); + + it('getRapidAccessParam returns undefined when no params are present', () => { + const params = new URLSearchParams(); + const result = getRapidAccessParam(params); + expect(result).toBeUndefined(); + }); + + it('getRapidAccessParam returns correct param when multiple params are present', () => { + const params = new URLSearchParams({ aar: 'value', retrievalId: '123' }); + const result = getRapidAccessParam(params); + expect(result).toEqual([AppRouteParams.AAR, 'value']); + }); + + it('getRapidAccessParam returns correct param when only retrievalId is present', () => { + const params = new URLSearchParams({ retrievalId: '123' }); + const result = getRapidAccessParam(params); + expect(result).toEqual([AppRouteParams.RETRIEVAL_ID, '123']); + }); + + it('getRapidAccessParam returns undefined when param is empty', () => { + const params = new URLSearchParams({ aar: '' }); + const result = getRapidAccessParam(params); + expect(result).toBeUndefined(); + }); + + it('getRapidAccessParam returns undefined when parameters is unknown', () => { + const params = new URLSearchParams({ unknownParam: 'test-param' }); + const result = getRapidAccessParam(params); + expect(result).toBeUndefined(); + }); }); diff --git a/packages/pn-commons/src/utility/index.ts b/packages/pn-commons/src/utility/index.ts index 6b701066d5..ec3f3cf77a 100644 --- a/packages/pn-commons/src/utility/index.ts +++ b/packages/pn-commons/src/utility/index.ts @@ -61,7 +61,7 @@ import { setPaymentsInCache, } from './paymentCaching.utility'; import { parseError } from './redux.utility'; -import { AppRouteParams, compileRoute } from './routes.utility'; +import { AppRouteParams, compileRoute, getRapidAccessParam } from './routes.utility'; import { searchStringLimitReachedText, useSearchStringChangeInput } from './searchString.utility'; import { storageOpsBuilder } from './storage.utility'; import { dataRegex, formatFiscalCode, fromStringToBase64, sanitizeString } from './string.utility'; @@ -158,4 +158,5 @@ export { fromStringToBase64, IS_DEVELOP, APP_VERSION, + getRapidAccessParam, }; diff --git a/packages/pn-commons/src/utility/routes.utility.ts b/packages/pn-commons/src/utility/routes.utility.ts index 295f1ef291..92263542c8 100644 --- a/packages/pn-commons/src/utility/routes.utility.ts +++ b/packages/pn-commons/src/utility/routes.utility.ts @@ -71,4 +71,12 @@ export function compileRoute(route: Route) { export enum AppRouteParams { AAR = 'aar', + RETRIEVAL_ID = 'retrievalId', +} + +export function getRapidAccessParam(params: URLSearchParams): [AppRouteParams, string] | undefined { + const keys = Object.values(AppRouteParams); + const key = keys.find((k) => params.has(k)); + const param = key ? params.get(key) : undefined; + return key && param ? [key, param] : undefined; } diff --git a/packages/pn-pa-webapp/src/__mocks__/Auth.mock.ts b/packages/pn-pa-webapp/src/__mocks__/Auth.mock.ts index 9cf6f3973c..eabfb11d20 100644 --- a/packages/pn-pa-webapp/src/__mocks__/Auth.mock.ts +++ b/packages/pn-pa-webapp/src/__mocks__/Auth.mock.ts @@ -2,9 +2,8 @@ import MockAdapter from 'axios-mock-adapter'; import { authClient } from '../api/apiClients'; import { AUTH_TOKEN_EXCHANGE } from '../api/auth/auth.routes'; -import { PNRole, PartyRole } from '../models/user'; +import { PNRole, PartyRole, User } from '../models/user'; import { exchangeToken, logout } from '../redux/auth/actions'; -import { User } from '../redux/auth/types'; import { store } from '../redux/store'; export const mockLogin = async (body: User | string = userResponse): Promise => { diff --git a/packages/pn-pa-webapp/src/api/auth/Auth.api.ts b/packages/pn-pa-webapp/src/api/auth/Auth.api.ts index 6c8a940a8b..a5e60178ec 100644 --- a/packages/pn-pa-webapp/src/api/auth/Auth.api.ts +++ b/packages/pn-pa-webapp/src/api/auth/Auth.api.ts @@ -1,6 +1,6 @@ import { removeNullProperties } from '@pagopa-pn/pn-commons/src/utility/user.utility'; -import { User } from '../../redux/auth/types'; +import { User } from '../../models/user'; import { authClient } from '../apiClients'; import { AUTH_TOKEN_EXCHANGE } from './auth.routes'; diff --git a/packages/pn-pa-webapp/src/models/user.ts b/packages/pn-pa-webapp/src/models/user.ts index 893c402531..d375621f45 100644 --- a/packages/pn-pa-webapp/src/models/user.ts +++ b/packages/pn-pa-webapp/src/models/user.ts @@ -1,3 +1,5 @@ +import { BasicUser } from '@pagopa-pn/pn-commons'; + export interface Role { role: PNRole; partyRole: PartyRole; @@ -25,3 +27,25 @@ export enum PNRole { ADMIN = 'admin', // ref amministrativo OPERATOR = 'operator', // ref tecnico } + +export interface User extends BasicUser { + organization: Organization; + desired_exp: number; +} + +export interface Organization { + id: string; + roles: Array; + fiscal_code: string; // organization fiscal code + groups?: Array; + hasGroups?: boolean; + name: string; + subUnitCode?: string; + subUnitType?: string; + aooParent?: string; + ipaCode?: string; + rootParent?: { + id?: string; + description?: string; + }; +} diff --git a/packages/pn-pa-webapp/src/redux/auth/actions.ts b/packages/pn-pa-webapp/src/redux/auth/actions.ts index baee412dfc..1c9b38621f 100644 --- a/packages/pn-pa-webapp/src/redux/auth/actions.ts +++ b/packages/pn-pa-webapp/src/redux/auth/actions.ts @@ -14,9 +14,8 @@ import { BffTosPrivacyActionBody, UserConsentsApiFactory, } from '../../generated-client/tos-privacy'; -import { PNRole, PartyRole } from '../../models/user'; +import { PNRole, PartyRole, User } from '../../models/user'; import { RootState } from '../store'; -import { User } from './types'; export enum AUTH_ACTIONS { GET_TOS_PRIVACY_APPROVAL = 'getTosPrivacyApproval', diff --git a/packages/pn-pa-webapp/src/redux/auth/reducers.ts b/packages/pn-pa-webapp/src/redux/auth/reducers.ts index 9aa41da216..c49a6de041 100644 --- a/packages/pn-pa-webapp/src/redux/auth/reducers.ts +++ b/packages/pn-pa-webapp/src/redux/auth/reducers.ts @@ -11,7 +11,7 @@ import { import { PartyEntity, ProductEntity } from '@pagopa/mui-italia'; import { createSlice } from '@reduxjs/toolkit'; -import { PNRole, PartyRole, Role } from '../../models/user'; +import { Organization, PNRole, PartyRole, Role, User } from '../../models/user'; import { acceptTosPrivacy, exchangeToken, @@ -22,7 +22,6 @@ import { logout, setAdditionalLanguages, } from './actions'; -import { Organization, User } from './types'; const roleMatcher: yup.SchemaOf = yup.object({ role: yup.mixed().required(), diff --git a/packages/pn-pa-webapp/src/redux/auth/types.ts b/packages/pn-pa-webapp/src/redux/auth/types.ts deleted file mode 100644 index 72a7deebd7..0000000000 --- a/packages/pn-pa-webapp/src/redux/auth/types.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { BasicUser } from '@pagopa-pn/pn-commons'; - -import { Role } from '../../models/user'; - -export interface User extends BasicUser { - organization: Organization; - desired_exp: number; -} - -export interface Organization { - id: string; - roles: Array; - fiscal_code: string; // organization fiscal code - groups?: Array; - hasGroups?: boolean; - name: string; - subUnitCode?: string; - subUnitType?: string; - aooParent?: string; - ipaCode?: string; - rootParent?: { - id?: string; - description?: string; - }; -} diff --git a/packages/pn-personafisica-login/src/hooks/useRapidAccessParam.ts b/packages/pn-personafisica-login/src/hooks/useRapidAccessParam.ts new file mode 100644 index 0000000000..d4635a6955 --- /dev/null +++ b/packages/pn-personafisica-login/src/hooks/useRapidAccessParam.ts @@ -0,0 +1,10 @@ +import { useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import { getRapidAccessParam } from '@pagopa-pn/pn-commons'; + +export function useRapidAccessParam() { + const [params] = useSearchParams(); + + return useMemo(() => getRapidAccessParam(params), [params]); +} diff --git a/packages/pn-personafisica-login/src/pages/login/Login.tsx b/packages/pn-personafisica-login/src/pages/login/Login.tsx index cf44eed5e0..6afdd7e7f9 100644 --- a/packages/pn-personafisica-login/src/pages/login/Login.tsx +++ b/packages/pn-personafisica-login/src/pages/login/Login.tsx @@ -1,6 +1,5 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { useSearchParams } from 'react-router-dom'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; @@ -8,7 +7,6 @@ import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; import { styled } from '@mui/material/styles'; import { - AppRouteParams, Layout, PRIVACY_LINK_RELATIVE_PATH as PRIVACY_POLICY, useIsMobile, @@ -16,9 +14,10 @@ import { import { CieIcon, SpidIcon } from '@pagopa/mui-italia/dist/icons'; import { PFLoginEventsType } from '../../models/PFLoginEventsType'; +import { useRapidAccessParam } from '../../hooks/useRapidAccessParam'; import { getConfiguration } from '../../services/configuration.service'; import PFLoginEventStrategyFactory from '../../utility/MixpanelUtils/PFLoginEventStrategyFactory'; -import { storageAarOps } from '../../utility/storage'; +import { storageRapidAccessOps } from '../../utility/storage'; import SpidSelect from './SpidSelect'; const LoginButton = styled(Button)(() => ({ @@ -33,13 +32,12 @@ const Login = () => { const [showIDPS, setShowIDPS] = useState(false); const { t, i18n } = useTranslation(['login']); const isMobile = useIsMobile(); - const [params] = useSearchParams(); - const aar = params.get(AppRouteParams.AAR); + const rapidAccess = useRapidAccessParam(); const { URL_API_LOGIN, SPID_CIE_ENTITY_ID, PAGOPA_HELP_EMAIL, PF_URL } = getConfiguration(); const privacyPolicyUrl = `${PF_URL}${PRIVACY_POLICY}`; - if (aar !== null && aar !== '') { - storageAarOps.write(aar); + if (rapidAccess) { + storageRapidAccessOps.write(rapidAccess); } useEffect(() => { diff --git a/packages/pn-personafisica-login/src/pages/login/__test__/Login.test.tsx b/packages/pn-personafisica-login/src/pages/login/__test__/Login.test.tsx index 8e44c0c0c9..bd6b0637d8 100644 --- a/packages/pn-personafisica-login/src/pages/login/__test__/Login.test.tsx +++ b/packages/pn-personafisica-login/src/pages/login/__test__/Login.test.tsx @@ -6,18 +6,22 @@ import { getById, queryById, waitFor } from '@pagopa-pn/pn-commons/src/test-util import { fireEvent, render } from '../../../__test__/test-utils'; import { getConfiguration } from '../../../services/configuration.service'; -import { storageAarOps } from '../../../utility/storage'; +import { storageRapidAccessOps } from '../../../utility/storage'; import Login from '../Login'; const mockAssign = vi.fn(); -let mockSearchParams = true; +let mockSearchParamsAar = true; +let mockSearchParamsRetrievalId = false; // simulate url params function mockCreateMockedSearchParams() { const mockedSearchParams = new URLSearchParams(); - if (mockSearchParams) { + if (mockSearchParamsAar) { mockedSearchParams.set(AppRouteParams.AAR, 'fake-aar-token'); } + if (mockSearchParamsRetrievalId) { + mockedSearchParams.set(AppRouteParams.RETRIEVAL_ID, 'fake-retrieval_id'); + } return mockedSearchParams; } @@ -47,7 +51,7 @@ describe('test login page', () => { }); afterEach(() => { - storageAarOps.delete(); + storageRapidAccessOps.delete(); vi.clearAllMocks(); }); @@ -69,7 +73,7 @@ describe('test login page', () => { expect(cieButton).toBeInTheDocument(); const spidSelect = queryById(container, 'spidSelect'); expect(spidSelect).not.toBeInTheDocument(); - expect(storageAarOps.read()).toBe('fake-aar-token'); + expect(storageRapidAccessOps.read()).toEqual([AppRouteParams.AAR, 'fake-aar-token']); }); it('select spid login', async () => { @@ -100,12 +104,22 @@ describe('test login page', () => { }); it('not store data in session storage', () => { - mockSearchParams = false; + mockSearchParamsAar = false; + render( + + + + ); + expect(storageRapidAccessOps.read()).toBeUndefined(); + }); + + it('store retrievalId in session storage', () => { + mockSearchParamsRetrievalId = true; render( ); - expect(storageAarOps.read()).toBeUndefined(); + expect(storageRapidAccessOps.read()).toEqual([AppRouteParams.RETRIEVAL_ID, 'fake-retrieval_id']); }); }); diff --git a/packages/pn-personafisica-login/src/pages/logout/Logout.tsx b/packages/pn-personafisica-login/src/pages/logout/Logout.tsx index 6de03b4a34..6c749d2007 100644 --- a/packages/pn-personafisica-login/src/pages/logout/Logout.tsx +++ b/packages/pn-personafisica-login/src/pages/logout/Logout.tsx @@ -2,14 +2,14 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { ROUTE_LOGIN } from '../../navigation/routes.const'; -import { storageAarOps } from '../../utility/storage'; +import { storageRapidAccessOps } from '../../utility/storage'; const Logout = () => { const navigate = useNavigate(); const searchParams = window.location.search ?? ''; useEffect(() => { - storageAarOps.delete(); + storageRapidAccessOps.delete(); navigate(ROUTE_LOGIN + searchParams); }, []); diff --git a/packages/pn-personafisica-login/src/pages/logout/__test__/Logout.test.tsx b/packages/pn-personafisica-login/src/pages/logout/__test__/Logout.test.tsx index f0fc3983fb..b9bb368bc0 100644 --- a/packages/pn-personafisica-login/src/pages/logout/__test__/Logout.test.tsx +++ b/packages/pn-personafisica-login/src/pages/logout/__test__/Logout.test.tsx @@ -1,9 +1,11 @@ import { BrowserRouter } from 'react-router-dom'; import { vi } from 'vitest'; +import { AppRouteParams } from '@pagopa-pn/pn-commons'; + import { render } from '../../../__test__/test-utils'; import { ROUTE_LOGIN } from '../../../navigation/routes.const'; -import { storageAarOps } from '../../../utility/storage'; +import { storageRapidAccessOps } from '../../../utility/storage'; import Logout from '../Logout'; const mockNavigateFn = vi.fn(); @@ -19,13 +21,13 @@ describe('Logout page', () => { }); it('test logout', () => { - storageAarOps.write('aar-test'); + storageRapidAccessOps.write([AppRouteParams.AAR, 'aar-test']); render( ); - expect(storageAarOps.read()).toBeUndefined(); + expect(storageRapidAccessOps.read()).toBeUndefined(); expect(mockNavigateFn).toBeCalledTimes(1); expect(mockNavigateFn).toBeCalledWith(ROUTE_LOGIN); }); diff --git a/packages/pn-personafisica-login/src/pages/success/Success.tsx b/packages/pn-personafisica-login/src/pages/success/Success.tsx index 7c1755bb2e..de7107709a 100644 --- a/packages/pn-personafisica-login/src/pages/success/Success.tsx +++ b/packages/pn-personafisica-login/src/pages/success/Success.tsx @@ -1,18 +1,18 @@ import { useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { AppRouteParams, getLangCode, sanitizeString } from '@pagopa-pn/pn-commons'; +import { getLangCode, sanitizeString } from '@pagopa-pn/pn-commons'; import { PFLoginEventsType } from '../../models/PFLoginEventsType'; import { getConfiguration } from '../../services/configuration.service'; import PFLoginEventStrategyFactory from '../../utility/MixpanelUtils/PFLoginEventStrategyFactory'; -import { storageAarOps } from '../../utility/storage'; +import { storageRapidAccessOps } from '../../utility/storage'; const SuccessPage = () => { const { PF_URL } = getConfiguration(); const { i18n } = useTranslation(); - const aar = useMemo(() => storageAarOps.read(), []); + const rapidAccess = useMemo(() => storageRapidAccessOps.read(), []); const token = useMemo(() => window.location.hash, []); const calcRedirectUrl = useCallback(() => { @@ -20,10 +20,10 @@ const SuccessPage = () => { let redirectUrl = PF_URL ?? ''; // the includes check is needed to prevent xss attacks - if (redirectUrl && [PF_URL].includes(redirectUrl) && aar) { - storageAarOps.delete(); + if (redirectUrl && [PF_URL].includes(redirectUrl) && rapidAccess) { + storageRapidAccessOps.delete(); // eslint-disable-next-line functional/immutable-data - redirectUrl += `?${AppRouteParams.AAR}=${sanitizeString(aar)}`; + redirectUrl += `?${rapidAccess[0]}=${sanitizeString(rapidAccess[1])}`; } // the findIndex check is needed to prevent xss attacks @@ -32,7 +32,7 @@ const SuccessPage = () => { `${redirectUrl}${sanitizeString(token)}&lang=${sanitizeString(getLangCode(i18n.language))}` ); } - }, [aar, token, i18n.language]); + }, [rapidAccess, token, i18n.language]); useEffect(() => { calcRedirectUrl(); diff --git a/packages/pn-personafisica-login/src/pages/success/__test__/Success.test.tsx b/packages/pn-personafisica-login/src/pages/success/__test__/Success.test.tsx index 5df0c9424a..155dd2de33 100644 --- a/packages/pn-personafisica-login/src/pages/success/__test__/Success.test.tsx +++ b/packages/pn-personafisica-login/src/pages/success/__test__/Success.test.tsx @@ -1,9 +1,11 @@ import { BrowserRouter } from 'react-router-dom'; import { vi } from 'vitest'; +import { AppRouteParams } from '@pagopa-pn/pn-commons'; + import { render } from '../../../__test__/test-utils'; import { getConfiguration } from '../../../services/configuration.service'; -import { storageAarOps } from '../../../utility/storage'; +import { storageRapidAccessOps } from '../../../utility/storage'; import SuccessPage from '../Success'; const mockLocationAssign = vi.fn(); @@ -44,7 +46,7 @@ describe('test login page', () => { }); it('test redirect - aar', () => { - storageAarOps.write('aar-token'); + storageRapidAccessOps.write([AppRouteParams.AAR, 'aar-token']); render( @@ -57,8 +59,25 @@ describe('test login page', () => { ); }); + it('test redirect - retrievalId', () => { + storageRapidAccessOps.write([AppRouteParams.RETRIEVAL_ID, 'retrieval-id']); + render( + + + + ); + + expect(mockLocationAssign).toBeCalled(); + expect(mockLocationAssign).toBeCalledWith( + getConfiguration().PF_URL + '?retrievalId=retrieval-id#token=fake-token&lang=it' + ); + }); + it('test redirect - aar with xss attack', () => { - storageAarOps.write('aar-malicious-token'); + storageRapidAccessOps.write([ + AppRouteParams.AAR, + 'aar-malicious-token', + ]); render( diff --git a/packages/pn-personafisica-login/src/utility/__test__/storage.test.tsx b/packages/pn-personafisica-login/src/utility/__test__/storage.test.tsx index d4833c5232..1cf08669fb 100644 --- a/packages/pn-personafisica-login/src/utility/__test__/storage.test.tsx +++ b/packages/pn-personafisica-login/src/utility/__test__/storage.test.tsx @@ -1,13 +1,22 @@ import { AppRouteParams } from '@pagopa-pn/pn-commons'; -import { storageAarOps } from '../storage'; +import { storageRapidAccessOps } from '../storage'; describe('storage utility test', () => { - it('storageAarOps', () => { - storageAarOps.write('test'); - expect(sessionStorage.getItem(AppRouteParams.AAR)).toBe('test'); - expect(storageAarOps.read()).toBe('test'); - storageAarOps.delete(); + it('storage Aar', () => { + const aar: [AppRouteParams, string] = [AppRouteParams.AAR, 'test-aar']; + storageRapidAccessOps.write(aar); + expect(sessionStorage.getItem(AppRouteParams.AAR)).toBe('test-aar'); + expect(storageRapidAccessOps.read()).toEqual(aar); + storageRapidAccessOps.delete(); expect(sessionStorage.getItem(AppRouteParams.AAR)).toBeNull(); }); + it('storageRetrievalId test', () => { + const retrievalId: [AppRouteParams, string] = [AppRouteParams.RETRIEVAL_ID, 'test-retrieval-id']; + storageRapidAccessOps.write(retrievalId); + expect(sessionStorage.getItem(AppRouteParams.RETRIEVAL_ID)).toBe('test-retrieval-id'); + expect(storageRapidAccessOps.read()).toEqual(retrievalId); + storageRapidAccessOps.delete(); + expect(sessionStorage.getItem(AppRouteParams.RETRIEVAL_ID)).toBeNull(); + }); }); diff --git a/packages/pn-personafisica-login/src/utility/storage.ts b/packages/pn-personafisica-login/src/utility/storage.ts index 717ef3a44c..e67aad346d 100644 --- a/packages/pn-personafisica-login/src/utility/storage.ts +++ b/packages/pn-personafisica-login/src/utility/storage.ts @@ -1,3 +1,36 @@ import { AppRouteParams, storageOpsBuilder } from '@pagopa-pn/pn-commons'; -export const storageAarOps = storageOpsBuilder(AppRouteParams.AAR, 'string', false); +const storageAarOps = storageOpsBuilder(AppRouteParams.AAR, 'string', false); + +const storageRetrievalIdOps = storageOpsBuilder( + AppRouteParams.RETRIEVAL_ID, + 'string', + false +); + +export const storageRapidAccessOps = { + read: (): [AppRouteParams, string] | undefined => { + const aar = storageAarOps.read(); + if (aar) { + return [AppRouteParams.AAR, aar]; + } + const retrievalId = storageRetrievalIdOps.read(); + if (retrievalId) { + return [AppRouteParams.RETRIEVAL_ID, retrievalId]; + } + return undefined; + }, + write: ([key, value]: [AppRouteParams, string]) => { + if (key === AppRouteParams.AAR) { + storageAarOps.write(value); + storageRetrievalIdOps.delete(); + } else if (key === AppRouteParams.RETRIEVAL_ID) { + storageRetrievalIdOps.write(value); + storageAarOps.delete(); + } + }, + delete: () => { + storageAarOps.delete(); + storageRetrievalIdOps.delete(); + }, +}; diff --git a/packages/pn-personafisica-webapp/openapitools.json b/packages/pn-personafisica-webapp/openapitools.json index f975ab3ed8..9706144975 100644 --- a/packages/pn-personafisica-webapp/openapitools.json +++ b/packages/pn-personafisica-webapp/openapitools.json @@ -6,7 +6,7 @@ "generators": { "bff-notifications": { "generatorName": "typescript-axios", - "inputSpec": "https://raw.githubusercontent.com/pagopa/pn-bff/5355801397984dd3c3187e417d49e90c6c4076ae/docs/openapi/api-external-pn-bff-recipient-notifications.yaml", + "inputSpec": "https://raw.githubusercontent.com/pagopa/pn-bff/54ad754b908f0aa2772768791592135e4539a2b6/docs/openapi/api-external-pn-bff-recipient-notifications.yaml", "output": "./src/generated-client/notifications", "additionalProperties": { "supportsES6": true, diff --git a/packages/pn-personafisica-webapp/src/__mocks__/Auth.mock.ts b/packages/pn-personafisica-webapp/src/__mocks__/Auth.mock.ts index 70f5bedeea..74f0624983 100644 --- a/packages/pn-personafisica-webapp/src/__mocks__/Auth.mock.ts +++ b/packages/pn-personafisica-webapp/src/__mocks__/Auth.mock.ts @@ -2,14 +2,14 @@ import MockAdapter from 'axios-mock-adapter'; import { authClient } from '../api/apiClients'; import { AUTH_TOKEN_EXCHANGE } from '../api/auth/auth.routes'; +import { SourceChannel, User } from '../models/User'; import { exchangeToken, logout } from '../redux/auth/actions'; -import { User } from '../redux/auth/types'; import { store } from '../redux/store'; export const mockLogin = async (): Promise => { const mock = new MockAdapter(authClient); mock.onPost(AUTH_TOKEN_EXCHANGE()).reply(200, userResponse); - const action = store.dispatch(exchangeToken('mocked-token')); + const action = store.dispatch(exchangeToken({ spidToken: 'mocked-token' })); mock.reset(); mock.restore(); return action; @@ -23,7 +23,7 @@ export const mockAuthentication = () => { beforeAll(() => { mock = new MockAdapter(authClient); mock.onPost(AUTH_TOKEN_EXCHANGE()).reply(200, userResponse); - store.dispatch(exchangeToken('mocked-token')); + store.dispatch(exchangeToken({ spidToken: 'mocked-token' })); }); afterAll(() => { @@ -39,7 +39,6 @@ export const userResponse: User = { family_name: 'Rossi', fiscal_number: 'RSSMRA80A01H501U', email: 'info@agid.gov.it', - mobile_phone: '333333334', from_aa: false, uid: 'a6c1350d-1d69-4209-8bf8-31de58c79d6f', aud: 'portale.dev.pn.pagopa.it', @@ -49,3 +48,12 @@ export const userResponse: User = { iss: 'https://spid-hub-test.dev.pn.pagopa.it', jti: 'mockedJTI004', }; + +export const userResponseWithRetrievalId: User = { + ...userResponse, + source: { + channel: SourceChannel.TPP, + details: 'mock-tpp-id', + retrievalId: 'mock-retrieval-id', + }, +}; diff --git a/packages/pn-personafisica-webapp/src/api/auth/Auth.api.ts b/packages/pn-personafisica-webapp/src/api/auth/Auth.api.ts index 1211893b69..c1f3578b5b 100644 --- a/packages/pn-personafisica-webapp/src/api/auth/Auth.api.ts +++ b/packages/pn-personafisica-webapp/src/api/auth/Auth.api.ts @@ -1,24 +1,42 @@ -import { User } from "../../redux/auth/types"; -import { authClient } from "../apiClients"; -import { AUTH_TOKEN_EXCHANGE } from "./auth.routes"; +/* eslint-disable functional/immutable-data */ +import { + TokenExchangeBody, + TokenExchangeRequest, + User, + paramsToSourceType, +} from '../../models/User'; +import { authClient } from '../apiClients'; +import { AUTH_TOKEN_EXCHANGE } from './auth.routes'; export const AuthApi = { - exchangeToken: (spidToken: string): Promise => - authClient.post(AUTH_TOKEN_EXCHANGE(), {authorizationToken: spidToken}) - .then((response) => ({ - sessionToken: response.data.sessionToken, - email: response.data.email, - name: response.data.name, - family_name: response.data.family_name, - uid: response.data.uid, - fiscal_number: response.data.fiscal_number, - mobile_phone: response.data.mobile_phone, - from_aa: response.data.from_aa, - aud: response.data.aud, - level: response.data.level, - iat: response.data.iat, - exp: response.data.exp, - iss: response.data.iss, - jti: response.data.jti - })) + exchangeToken: async ({ spidToken, rapidAccess }: TokenExchangeRequest): Promise => { + const body: TokenExchangeBody = { authorizationToken: spidToken }; + if (rapidAccess) { + const [param, value] = rapidAccess; + body.source = { + type: paramsToSourceType[param], + id: value, + }; + } + const response = await authClient.post(AUTH_TOKEN_EXCHANGE(), body); + const user: User = { + sessionToken: response.data.sessionToken, + email: response.data.email, + name: response.data.name, + family_name: response.data.family_name, + uid: response.data.uid, + fiscal_number: response.data.fiscal_number, + from_aa: response.data.from_aa, + aud: response.data.aud, + level: response.data.level, + iat: response.data.iat, + exp: response.data.exp, + iss: response.data.iss, + jti: response.data.jti, + }; + if (response.data.source) { + user.source = response.data.source; + } + return user; + }, }; diff --git a/packages/pn-personafisica-webapp/src/api/auth/__test__/Auth.api.test.ts b/packages/pn-personafisica-webapp/src/api/auth/__test__/Auth.api.test.ts index 034a64614a..59ba25be95 100644 --- a/packages/pn-personafisica-webapp/src/api/auth/__test__/Auth.api.test.ts +++ b/packages/pn-personafisica-webapp/src/api/auth/__test__/Auth.api.test.ts @@ -1,18 +1,47 @@ import MockAdapter from 'axios-mock-adapter'; -import { userResponse } from '../../../__mocks__/Auth.mock'; +import { AppRouteParams } from '@pagopa-pn/pn-commons'; + +import { userResponse, userResponseWithRetrievalId } from '../../../__mocks__/Auth.mock'; import { authClient } from '../../apiClients'; import { AuthApi } from '../Auth.api'; import { AUTH_TOKEN_EXCHANGE } from '../auth.routes'; describe('Auth api tests', () => { - it('exchangeToken', async () => { - const token = 'mocked-token'; - const mock = new MockAdapter(authClient); - mock.onPost(AUTH_TOKEN_EXCHANGE(), { authorizationToken: token }).reply(200, userResponse); - const res = await AuthApi.exchangeToken(token); - expect(res).toStrictEqual(userResponse); + let mock: MockAdapter; + + beforeAll(() => { + mock = new MockAdapter(authClient); + }); + + afterEach(() => { mock.reset(); + }); + + afterAll(() => { mock.restore(); }); + + it('exchangeToken', async () => { + const spidToken = 'mocked-token'; + mock.onPost(AUTH_TOKEN_EXCHANGE(), { authorizationToken: spidToken }).reply(200, userResponse); + const res = await AuthApi.exchangeToken({ spidToken }); + expect(res).toStrictEqual(userResponse); + }); + + it('exchangeToken with rapidAccess', async () => { + const spidToken = 'mocked-token'; + const rapidAccess: [AppRouteParams, string] = [AppRouteParams.AAR, 'mocked-qr-code']; + mock + .onPost(AUTH_TOKEN_EXCHANGE(), { + authorizationToken: spidToken, + source: { + type: 'QR', + id: 'mocked-qr-code', + }, + }) + .reply(200, userResponseWithRetrievalId); + const res = await AuthApi.exchangeToken({ spidToken, rapidAccess }); + expect(res).toStrictEqual(userResponseWithRetrievalId); + }); }); diff --git a/packages/pn-personafisica-webapp/src/hooks/useRapidAccessParam.ts b/packages/pn-personafisica-webapp/src/hooks/useRapidAccessParam.ts new file mode 100644 index 0000000000..d4635a6955 --- /dev/null +++ b/packages/pn-personafisica-webapp/src/hooks/useRapidAccessParam.ts @@ -0,0 +1,10 @@ +import { useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; + +import { getRapidAccessParam } from '@pagopa-pn/pn-commons'; + +export function useRapidAccessParam() { + const [params] = useSearchParams(); + + return useMemo(() => getRapidAccessParam(params), [params]); +} diff --git a/packages/pn-personafisica-webapp/src/models/NotificationDetail.ts b/packages/pn-personafisica-webapp/src/models/NotificationDetail.ts index 449e9224ee..c473d908e4 100644 --- a/packages/pn-personafisica-webapp/src/models/NotificationDetail.ts +++ b/packages/pn-personafisica-webapp/src/models/NotificationDetail.ts @@ -1,6 +1,10 @@ -import { NotificationDetail, NotificationDetailRecipient } from '@pagopa-pn/pn-commons'; +import { AppRouteParams, NotificationDetail, NotificationDetailRecipient } from '@pagopa-pn/pn-commons'; export interface NotificationDetailForRecipient extends NotificationDetail { currentRecipient: NotificationDetailRecipient; currentRecipientIndex: number; } + +export type NotificationDetailRouteState = { + source?: AppRouteParams; // indicates whether the user arrived to the notification detail page from the QR code +}; \ No newline at end of file diff --git a/packages/pn-personafisica-webapp/src/models/User.ts b/packages/pn-personafisica-webapp/src/models/User.ts new file mode 100644 index 0000000000..58b63824ea --- /dev/null +++ b/packages/pn-personafisica-webapp/src/models/User.ts @@ -0,0 +1,42 @@ +import { AppRouteParams, BasicUser } from '@pagopa-pn/pn-commons'; + +export interface User extends BasicUser { + from_aa: boolean; + aud: string; + level: string; + iat: number; + exp: number; + iss: string; + jti: string; + source?: UserSource; +} + +export enum SourceChannel { + B2B = 'B2B', + WEB = 'WEB', + TPP = 'TPP', +} + +export interface UserSource { + channel: SourceChannel; + details: string; + retrievalId: string; +} + +export interface TokenExchangeBody { + authorizationToken: string; + source?: { + type: 'TPP' | 'QR'; + id: string; + }; +} + +export interface TokenExchangeRequest { + spidToken: string; + rapidAccess?: [AppRouteParams, string]; +} + +export const paramsToSourceType: Record = { + [AppRouteParams.AAR]: 'QR', + [AppRouteParams.RETRIEVAL_ID]: 'TPP', +}; diff --git a/packages/pn-personafisica-webapp/src/navigation/AARGuard.tsx b/packages/pn-personafisica-webapp/src/navigation/AARGuard.tsx deleted file mode 100644 index 0e5aafc449..0000000000 --- a/packages/pn-personafisica-webapp/src/navigation/AARGuard.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import { useEffect, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Outlet, useLocation, useNavigate } from 'react-router-dom'; - -import { AccessDenied, AppResponse, AppResponsePublisher, IllusQuestion, LoadingPage } from '@pagopa-pn/pn-commons'; -import { ServerResponseErrorCode } from '../utility/AppError/types'; - -import { NotificationId } from '../models/Notifications'; -import { PFEventsType } from '../models/PFEventsType'; -import { useAppDispatch } from '../redux/hooks'; -import { exchangeNotificationQrCode } from '../redux/notification/actions'; -import PFEventStrategyFactory from '../utility/MixpanelUtils/PFEventStrategyFactory'; -import { - DETTAGLIO_NOTIFICA_QRCODE_QUERY_PARAM, - GET_DETTAGLIO_NOTIFICA_DELEGATO_PATH, - GET_DETTAGLIO_NOTIFICA_PATH, - NOTIFICHE, -} from './routes.const'; - -function notificationDetailPath(notificationId: NotificationId): string { - return notificationId.mandateId - ? GET_DETTAGLIO_NOTIFICA_DELEGATO_PATH(notificationId.iun, notificationId.mandateId) - : GET_DETTAGLIO_NOTIFICA_PATH(notificationId.iun); -} - -const AARGuard = () => { - const location = useLocation(); - const dispatch = useAppDispatch(); - const navigate = useNavigate(); - const { t } = useTranslation(['notifiche']); - const [fetchError, setFetchError] = useState(false); - const [notificationId, setNotificationId] = useState(); - - const aar = useMemo(() => { - const queryParams = new URLSearchParams(location.search); - return queryParams.get(DETTAGLIO_NOTIFICA_QRCODE_QUERY_PARAM); - }, [location]); - - useEffect(() => { - if (aar) { - const fetchNotificationFromQrCode = () => - dispatch(exchangeNotificationQrCode({ aarQrCodeValue: aar })) - .unwrap() - .then((notification) => { - if (notification) { - setNotificationId(notification); - } - }) - .catch(() => { - setFetchError(true); - }); - void fetchNotificationFromQrCode(); - } - }, [aar]); - - const handleError =(e: AppResponse)=>{ - const error = e.errors ? e.errors[0] : null; - if(error && error.code === ServerResponseErrorCode.PN_DELIVERY_NOTIFICATIONNOTFOUND){ - return false; - } - return true; - }; - - useEffect(() => { - AppResponsePublisher.error.subscribe('exchangeNotificationQrCode', handleError); - - return () => { - AppResponsePublisher.error.unsubscribe('exchangeNotificationQrCode', handleError); - }; - }, []); - - useEffect(() => { - if (notificationId) { - PFEventStrategyFactory.triggerEvent(PFEventsType.SEND_RAPID_ACCESS); - navigate(notificationDetailPath(notificationId), { - replace: true, - state: { fromQrCode: true }, - }); - } - }, [notificationId]); - - if (!aar) { - return ; - } - if (fetchError) { - PFEventStrategyFactory.triggerEvent(PFEventsType.SEND_NOTIFICATION_NOT_ALLOWED); - return ( - } - message={t('from-qrcode.not-found')} - subtitle={t('from-qrcode.not-found-subtitle')} - isLogged={true} - goToHomePage={() => navigate(NOTIFICHE, { replace: true })} - goToLogin={() => {}} - /> - ); - } - - return ; -}; - -export default AARGuard; diff --git a/packages/pn-personafisica-webapp/src/navigation/RapidAccessGuard.tsx b/packages/pn-personafisica-webapp/src/navigation/RapidAccessGuard.tsx new file mode 100644 index 0000000000..274541fb0e --- /dev/null +++ b/packages/pn-personafisica-webapp/src/navigation/RapidAccessGuard.tsx @@ -0,0 +1,126 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Outlet, useNavigate } from 'react-router-dom'; + +import { + AccessDenied, + AppResponse, + AppResponsePublisher, + AppRouteParams, + IllusQuestion, + LoadingPage, +} from '@pagopa-pn/pn-commons'; + +import { useRapidAccessParam } from '../hooks/useRapidAccessParam'; +import { NotificationDetailRouteState } from '../models/NotificationDetail'; +import { NotificationId } from '../models/Notifications'; +import { PFEventsType } from '../models/PFEventsType'; +import { useAppDispatch } from '../redux/hooks'; +import { + NOTIFICATION_ACTIONS, + exchangeNotificationQrCode, + exchangeNotificationRetrievalId, +} from '../redux/notification/actions'; +import { ServerResponseErrorCode } from '../utility/AppError/types'; +import PFEventStrategyFactory from '../utility/MixpanelUtils/PFEventStrategyFactory'; +import { + GET_DETTAGLIO_NOTIFICA_DELEGATO_PATH, + GET_DETTAGLIO_NOTIFICA_PATH, + NOTIFICHE, +} from './routes.const'; + +function notificationDetailPath(notificationId: NotificationId): string { + return notificationId.mandateId + ? GET_DETTAGLIO_NOTIFICA_DELEGATO_PATH(notificationId.iun, notificationId.mandateId) + : GET_DETTAGLIO_NOTIFICA_PATH(notificationId.iun); +} + +/** + Il cittadino può accedere direttamente a SEND tramite: + - QR code dell'aar: https://cittadini.notifichedigitali.it/?aar=123456 + - Messaggi di cortesia da app di terze parti: https://cittadini.notifichedigitali.it/?retrievalId=123456 +*/ +const RapidAccessGuard = () => { + const dispatch = useAppDispatch(); + const navigate = useNavigate(); + const { t } = useTranslation('notifiche'); + const [fetchError, setFetchError] = useState(false); + const rapidAccess = useRapidAccessParam(); + + useEffect(() => { + const [param, value] = rapidAccess || []; + if (param && value) { + void exchangeNotification(param, value); + } + }, [rapidAccess]); + + const exchangeNotification = async (param: AppRouteParams, value: string) => { + try { + // eslint-disable-next-line functional/no-let + let path = ''; + if (param === AppRouteParams.AAR) { + const notificationId = await dispatch(exchangeNotificationQrCode(value)).unwrap(); + path = notificationDetailPath(notificationId); + } + if (param === AppRouteParams.RETRIEVAL_ID) { + const retrievalPayload = await dispatch(exchangeNotificationRetrievalId(value)).unwrap(); + path = GET_DETTAGLIO_NOTIFICA_PATH(retrievalPayload.originId!); + } + + PFEventStrategyFactory.triggerEvent(PFEventsType.SEND_RAPID_ACCESS, { source: param }); + + const state: NotificationDetailRouteState = { source: param }; + navigate(path, { + replace: true, + state, + }); + } catch (e: any) { + PFEventStrategyFactory.triggerEvent(PFEventsType.SEND_NOTIFICATION_NOT_ALLOWED); + setFetchError(true); + } + }; + + const handleErrorQrCode = (e: AppResponse) => { + // fix(12155): hide toast error when check aar api returns notification not found + const error = e.errors ? e.errors[0] : null; + if (error && error.code === ServerResponseErrorCode.PN_DELIVERY_NOTIFICATIONNOTFOUND) { + return false; + } + return true; + }; + + useEffect(() => { + AppResponsePublisher.error.subscribe( + NOTIFICATION_ACTIONS.EXCHANGE_NOTIFICATION_QR_CODE, + handleErrorQrCode + ); + + return () => { + AppResponsePublisher.error.unsubscribe( + NOTIFICATION_ACTIONS.EXCHANGE_NOTIFICATION_QR_CODE, + handleErrorQrCode + ); + }; + }, []); + + if (!rapidAccess || (fetchError && rapidAccess[0] !== AppRouteParams.AAR)) { + return ; + } + + if (fetchError) { + return ( + } + message={t('from-qrcode.not-found')} + subtitle={t('from-qrcode.not-found-subtitle')} + isLogged={true} + goToHomePage={() => navigate(NOTIFICHE, { replace: true })} + goToLogin={() => {}} + /> + ); + } + + return ; +}; + +export default RapidAccessGuard; diff --git a/packages/pn-personafisica-webapp/src/navigation/RouteGuard.tsx b/packages/pn-personafisica-webapp/src/navigation/RouteGuard.tsx index 3b49631bd7..aed69ec385 100644 --- a/packages/pn-personafisica-webapp/src/navigation/RouteGuard.tsx +++ b/packages/pn-personafisica-webapp/src/navigation/RouteGuard.tsx @@ -1,7 +1,8 @@ -import { Outlet, useNavigate, useSearchParams } from 'react-router-dom'; +import { Outlet, useNavigate } from 'react-router-dom'; -import { AccessDenied, AppRouteParams } from '@pagopa-pn/pn-commons'; +import { AccessDenied } from '@pagopa-pn/pn-commons'; +import { useRapidAccessParam } from '../hooks/useRapidAccessParam'; import { useAppSelector } from '../redux/hooks'; import { RootState } from '../redux/store'; import { goToLoginPortal } from './navigation.utility'; @@ -9,7 +10,7 @@ import * as routes from './routes.const'; const RouteGuard = () => { const navigate = useNavigate(); - const [params] = useSearchParams(); + const rapidAccess = useRapidAccessParam(); const { sessionToken } = useAppSelector((state: RootState) => state.userState.user); if (!sessionToken) { @@ -17,7 +18,7 @@ const RouteGuard = () => { navigate(routes.NOTIFICHE, { replace: true })} - goToLogin={() => goToLoginPortal(params.get(AppRouteParams.AAR))} + goToLogin={() => goToLoginPortal(rapidAccess)} /> ); } diff --git a/packages/pn-personafisica-webapp/src/navigation/SessionGuard.tsx b/packages/pn-personafisica-webapp/src/navigation/SessionGuard.tsx index 51e31903be..4e3ff3c31a 100644 --- a/packages/pn-personafisica-webapp/src/navigation/SessionGuard.tsx +++ b/packages/pn-personafisica-webapp/src/navigation/SessionGuard.tsx @@ -1,10 +1,9 @@ import { useCallback, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom'; +import { Outlet, useLocation, useNavigate } from 'react-router-dom'; import { AppResponsePublisher, - AppRouteParams, InactivityHandler, LoadingPage, SessionModal, @@ -14,6 +13,7 @@ import { useSessionCheck, } from '@pagopa-pn/pn-commons'; +import { useRapidAccessParam } from '../hooks/useRapidAccessParam'; import { AUTH_ACTIONS, exchangeToken, logout } from '../redux/auth/actions'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { RootState } from '../redux/store'; @@ -54,7 +54,7 @@ const manageUnforbiddenError = (e: any) => { * SessionGuardRender: logica di renderizzazione */ const SessionGuardRender = () => { - const [params] = useSearchParams(); + const rapidAccess = useRapidAccessParam(); const { IS_INACTIVITY_HANDLER_ENABLED } = getConfiguration(); const isInitialized = useAppSelector((state: RootState) => state.appState.isInitialized); @@ -94,8 +94,7 @@ const SessionGuardRender = () => { /> ); } else if (isAnonymousUser) { - const aar = params.get(AppRouteParams.AAR); - goToLoginPortal(aar); + goToLoginPortal(rapidAccess); return <>; } return ( @@ -126,6 +125,7 @@ const SessionGuard = () => { ); const dispatch = useAppDispatch(); const navigate = useNavigate(); + const rapidAccess = useRapidAccessParam(); const sessionCheck = useSessionCheck(200, () => dispatch(logout())); const { hasApiErrors, hasSpecificStatusError } = useErrors(); const { WORK_IN_PROGRESS } = getConfiguration(); @@ -161,7 +161,7 @@ const SessionGuard = () => { const spidToken = getTokenParam(); if (spidToken) { AppResponsePublisher.error.subscribe('exchangeToken', manageUnforbiddenError); - await dispatch(exchangeToken(spidToken)); + await dispatch(exchangeToken({ spidToken, rapidAccess })); } }; void performStep(INITIALIZATION_STEPS.USER_DETERMINATION, doUserDetermination); diff --git a/packages/pn-personafisica-webapp/src/navigation/__test__/AARGuard.test.tsx b/packages/pn-personafisica-webapp/src/navigation/__test__/AARGuard.test.tsx deleted file mode 100644 index 093ba807e5..0000000000 --- a/packages/pn-personafisica-webapp/src/navigation/__test__/AARGuard.test.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import MockAdapter from 'axios-mock-adapter'; -import { Route, Routes } from 'react-router-dom'; -import { vi } from 'vitest'; - -import { act, render, screen, waitFor } from '../../__test__/test-utils'; -import { apiClient } from '../../api/apiClients'; -import AARGuard from '../AARGuard'; -import { - DETTAGLIO_NOTIFICA_QRCODE_QUERY_PARAM, - GET_DETTAGLIO_NOTIFICA_DELEGATO_PATH, - GET_DETTAGLIO_NOTIFICA_PATH, -} from '../routes.const'; - -const mockNavigateFn = vi.fn(() => {}); - -// mock imports -vi.mock('react-router-dom', async () => ({ - ...(await vi.importActual('react-router-dom')), - useNavigate: () => mockNavigateFn, -})); - -vi.mock('react-i18next', () => ({ - useTranslation: () => ({ - t: (str: string) => str, - }), -})); - -const Guard = () => ( - - }> - Generic Page} /> - - -); - -describe('Notification from QR code', async () => { - const original = window.location; - let mock: MockAdapter; - - beforeAll(() => { - mock = new MockAdapter(apiClient); - Object.defineProperty(window, 'location', { - writable: true, - value: { search: '' }, - }); - }); - - afterEach(() => { - mock.reset(); - vi.clearAllMocks(); - }); - - afterAll(() => { - mock.restore(); - Object.defineProperty(window, 'location', { writable: true, value: original }); - }); - - it('QR code requested by a recipient', async () => { - const mockQrCode = 'qr-code'; - window.location.search = `?${DETTAGLIO_NOTIFICA_QRCODE_QUERY_PARAM}=${mockQrCode}`; - mock - .onPost('/bff/v1/notifications/received/check-aar-qr-code', { aarQrCodeValue: mockQrCode }) - .reply( - () => - new Promise((resolve) => { - setTimeout(() => { - resolve([200, { iun: 'mock-iun' }]); - }, 200); - }) - ); - await act(async () => { - render(); - }); - expect(mock.history.post).toHaveLength(1); - expect(mock.history.post[0].url).toBe('/bff/v1/notifications/received/check-aar-qr-code'); - expect(JSON.parse(mock.history.post[0].data)).toStrictEqual({ - aarQrCodeValue: mockQrCode, - }); - const loadingComponent = screen.queryByTestId('loading-skeleton'); - expect(loadingComponent).toBeInTheDocument(); - await waitFor(() => { - expect(mockNavigateFn).toBeCalledTimes(1); - expect(mockNavigateFn).toBeCalledWith(GET_DETTAGLIO_NOTIFICA_PATH('mock-iun'), { - replace: true, - state: { fromQrCode: true }, - }); - }); - const accessDeniedComponent = screen.queryByTestId('access-denied'); - expect(accessDeniedComponent).not.toBeInTheDocument(); - }); - - it('QR code requested by a delegate', async () => { - const mockQrCode = 'qr-code-delegate'; - window.location.search = `?${DETTAGLIO_NOTIFICA_QRCODE_QUERY_PARAM}=${mockQrCode}`; - mock - .onPost('/bff/v1/notifications/received/check-aar-qr-code', { aarQrCodeValue: mockQrCode }) - .reply(200, { iun: 'mock-iun', mandateId: 'mock-mandateId' }); - await act(async () => { - render(); - }); - expect(mock.history.post).toHaveLength(1); - expect(mock.history.post[0].url).toBe('/bff/v1/notifications/received/check-aar-qr-code'); - expect(JSON.parse(mock.history.post[0].data)).toStrictEqual({ - aarQrCodeValue: mockQrCode, - }); - await waitFor(() => { - expect(mockNavigateFn).toBeCalledTimes(1); - expect(mockNavigateFn).toBeCalledWith( - GET_DETTAGLIO_NOTIFICA_DELEGATO_PATH('mock-iun', 'mock-mandateId'), - { replace: true, state: { fromQrCode: true } } - ); - }); - const accessDeniedComponent = screen.queryByTestId('access-denied'); - expect(accessDeniedComponent).not.toBeInTheDocument(); - }); - - it('invalid QR code', async () => { - const mockQrCode = 'bad-qr-code'; - window.location.search = `?${DETTAGLIO_NOTIFICA_QRCODE_QUERY_PARAM}=${mockQrCode}`; - mock - .onPost('/bff/v1/notifications/received/check-aar-qr-code', { aarQrCodeValue: mockQrCode }) - .reply(500); - await act(async () => { - render(); - }); - expect(mock.history.post).toHaveLength(1); - expect(mock.history.post[0].url).toBe('/bff/v1/notifications/received/check-aar-qr-code'); - expect(JSON.parse(mock.history.post[0].data)).toStrictEqual({ - aarQrCodeValue: mockQrCode, - }); - await waitFor(() => { - expect(mockNavigateFn).toBeCalledTimes(0); - }); - const accessDeniedComponent = screen.getByTestId('access-denied'); - expect(accessDeniedComponent).toBeInTheDocument(); - expect(accessDeniedComponent).toHaveTextContent('from-qrcode.not-found'); - }); - - it('invalid recipient accesses to QR code', async () => { - const mockQrCode = 'bad-qr-code'; - window.location.search = `?${DETTAGLIO_NOTIFICA_QRCODE_QUERY_PARAM}=${mockQrCode}`; - mock - .onPost('/bff/v1/notifications/received/check-aar-qr-code', { aarQrCodeValue: mockQrCode }) - .reply(404); - await act(async () => { - render(); - }); - const pageComponent = screen.queryByText('Generic Page'); - const accessDeniedComponent = screen.queryByTestId('access-denied'); - const titleAccessDeniedComponent = await screen.findByText('from-qrcode.not-found'); - expect(pageComponent).toBeNull(); - expect(accessDeniedComponent).toBeTruthy(); - expect(titleAccessDeniedComponent).toBeInTheDocument(); - }); - - it('no QR code', async () => { - const mockQrCode = ''; - window.location.search = `?${DETTAGLIO_NOTIFICA_QRCODE_QUERY_PARAM}=${mockQrCode}`; - await act(async () => { - render(); - }); - expect(mock.history.post).toHaveLength(0); - expect(mockNavigateFn).toBeCalledTimes(0); - const pageComponent = screen.queryByText('Generic Page'); - expect(pageComponent).toBeTruthy(); - }); -}); diff --git a/packages/pn-personafisica-webapp/src/navigation/__test__/RapidAccessGuard.test.tsx b/packages/pn-personafisica-webapp/src/navigation/__test__/RapidAccessGuard.test.tsx new file mode 100644 index 0000000000..d3b1168b05 --- /dev/null +++ b/packages/pn-personafisica-webapp/src/navigation/__test__/RapidAccessGuard.test.tsx @@ -0,0 +1,203 @@ +import MockAdapter from 'axios-mock-adapter'; +import { Route, Routes } from 'react-router-dom'; +import { vi } from 'vitest'; + +import { AppRouteParams } from '@pagopa-pn/pn-commons'; + +import { act, render, screen, waitFor } from '../../__test__/test-utils'; +import { apiClient } from '../../api/apiClients'; +import { BffCheckTPPResponse } from '../../generated-client/notifications'; +import RapidAccessGuard from '../RapidAccessGuard'; +import { GET_DETTAGLIO_NOTIFICA_DELEGATO_PATH, GET_DETTAGLIO_NOTIFICA_PATH } from '../routes.const'; + +const mockNavigateFn = vi.fn(() => {}); + +// mock imports +vi.mock('react-router-dom', async () => ({ + ...(await vi.importActual('react-router-dom')), + useNavigate: () => mockNavigateFn, +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (str: string) => str, + }), +})); + +const Guard = () => ( + + }> + Generic Page} /> + + +); + +describe('Rapid access Guard', async () => { + const original = window.location; + let mock: MockAdapter; + + beforeAll(() => { + mock = new MockAdapter(apiClient); + Object.defineProperty(window, 'location', { + writable: true, + value: { search: '' }, + }); + }); + + afterEach(() => { + mock.reset(); + vi.clearAllMocks(); + }); + + afterAll(() => { + mock.restore(); + Object.defineProperty(window, 'location', { writable: true, value: original }); + }); + + describe('Notification from QR code', async () => { + it('QR code requested by a recipient', async () => { + const mockQrCode = 'qr-code'; + window.location.search = `?${AppRouteParams.AAR}=${mockQrCode}`; + mock + .onPost('/bff/v1/notifications/received/check-aar-qr-code', { aarQrCodeValue: mockQrCode }) + .reply( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve([200, { iun: 'mock-iun' }]); + }, 200); + }) + ); + await act(async () => { + render(); + }); + expect(mock.history.post).toHaveLength(1); + expect(mock.history.post[0].url).toBe('/bff/v1/notifications/received/check-aar-qr-code'); + expect(JSON.parse(mock.history.post[0].data)).toStrictEqual({ + aarQrCodeValue: mockQrCode, + }); + const loadingComponent = screen.queryByTestId('loading-skeleton'); + expect(loadingComponent).toBeInTheDocument(); + await waitFor(() => { + expect(mockNavigateFn).toHaveBeenCalledTimes(1); + expect(mockNavigateFn).toHaveBeenCalledWith(GET_DETTAGLIO_NOTIFICA_PATH('mock-iun'), { + replace: true, + state: { source: AppRouteParams.AAR }, + }); + }); + const accessDeniedComponent = screen.queryByTestId('access-denied'); + expect(accessDeniedComponent).not.toBeInTheDocument(); + }); + + it('QR code requested by a delegate', async () => { + const mockQrCode = 'qr-code-delegate'; + window.location.search = `?${AppRouteParams.AAR}=${mockQrCode}`; + mock + .onPost('/bff/v1/notifications/received/check-aar-qr-code', { aarQrCodeValue: mockQrCode }) + .reply(200, { iun: 'mock-iun', mandateId: 'mock-mandateId' }); + await act(async () => { + render(); + }); + expect(mock.history.post).toHaveLength(1); + expect(mock.history.post[0].url).toBe('/bff/v1/notifications/received/check-aar-qr-code'); + expect(JSON.parse(mock.history.post[0].data)).toStrictEqual({ + aarQrCodeValue: mockQrCode, + }); + await waitFor(() => { + expect(mockNavigateFn).toHaveBeenCalledTimes(1); + expect(mockNavigateFn).toHaveBeenCalledWith( + GET_DETTAGLIO_NOTIFICA_DELEGATO_PATH('mock-iun', 'mock-mandateId'), + { replace: true, state: { source: AppRouteParams.AAR } } + ); + }); + const accessDeniedComponent = screen.queryByTestId('access-denied'); + expect(accessDeniedComponent).not.toBeInTheDocument(); + }); + + it('invalid QR code', async () => { + const mockQrCode = 'bad-qr-code'; + window.location.search = `?${AppRouteParams.AAR}=${mockQrCode}`; + mock + .onPost('/bff/v1/notifications/received/check-aar-qr-code', { aarQrCodeValue: mockQrCode }) + .reply(500); + await act(async () => { + render(); + }); + expect(mock.history.post).toHaveLength(1); + expect(mock.history.post[0].url).toBe('/bff/v1/notifications/received/check-aar-qr-code'); + expect(JSON.parse(mock.history.post[0].data)).toStrictEqual({ + aarQrCodeValue: mockQrCode, + }); + await waitFor(() => { + expect(mockNavigateFn).toHaveBeenCalledTimes(0); + }); + const accessDeniedComponent = screen.getByTestId('access-denied'); + expect(accessDeniedComponent).toBeInTheDocument(); + expect(accessDeniedComponent).toHaveTextContent('from-qrcode.not-found'); + }); + + it('invalid recipient accesses to QR code', async () => { + const mockQrCode = 'bad-qr-code'; + window.location.search = `?${AppRouteParams.AAR}=${mockQrCode}`; + mock + .onPost('/bff/v1/notifications/received/check-aar-qr-code', { aarQrCodeValue: mockQrCode }) + .reply(404); + await act(async () => { + render(); + }); + const pageComponent = screen.queryByText('Generic Page'); + const accessDeniedComponent = screen.queryByTestId('access-denied'); + const titleAccessDeniedComponent = await screen.findByText('from-qrcode.not-found'); + expect(pageComponent).toBeNull(); + expect(accessDeniedComponent).toBeTruthy(); + expect(titleAccessDeniedComponent).toBeInTheDocument(); + }); + + it('no QR code', async () => { + const mockQrCode = ''; + window.location.search = `?${AppRouteParams.AAR}=${mockQrCode}`; + await act(async () => { + render(); + }); + expect(mock.history.post).toHaveLength(0); + expect(mockNavigateFn).toHaveBeenCalledTimes(0); + const pageComponent = screen.queryByText('Generic Page'); + expect(pageComponent).toBeTruthy(); + }); + }); + + describe('Notification from retrievalId', async () => { + it('retrievalId requested by a recipient', async () => { + const mockRetrievalId = 'retrieval-id'; + const url = `/bff/v1/notifications/received/check-tpp?retrievalId=${mockRetrievalId}`; + window.location.search = `?${AppRouteParams.RETRIEVAL_ID}=${mockRetrievalId}`; + mock.onGet(url).reply( + () => + new Promise((resolve) => { + setTimeout(() => { + resolve([ + 200, + { originId: 'mock-iun', retrievalId: mockRetrievalId } as BffCheckTPPResponse, + ]); + }, 200); + }) + ); + await act(async () => { + render(); + }); + expect(mock.history.get).toHaveLength(1); + expect(mock.history.get[0].url).toBe(url); + const loadingComponent = screen.queryByTestId('loading-skeleton'); + expect(loadingComponent).toBeInTheDocument(); + await waitFor(() => { + expect(mockNavigateFn).toHaveBeenCalledTimes(1); + expect(mockNavigateFn).toHaveBeenCalledWith(GET_DETTAGLIO_NOTIFICA_PATH('mock-iun'), { + replace: true, + state: { source: AppRouteParams.RETRIEVAL_ID }, + }); + }); + const accessDeniedComponent = screen.queryByTestId('access-denied'); + expect(accessDeniedComponent).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/pn-personafisica-webapp/src/navigation/__test__/navigation.utility.test.ts b/packages/pn-personafisica-webapp/src/navigation/__test__/navigation.utility.test.ts index f80563684b..67eba79d7e 100644 --- a/packages/pn-personafisica-webapp/src/navigation/__test__/navigation.utility.test.ts +++ b/packages/pn-personafisica-webapp/src/navigation/__test__/navigation.utility.test.ts @@ -1,9 +1,16 @@ import { vi } from 'vitest'; -import { EventPageType } from '@pagopa-pn/pn-commons'; +import { AppRouteParams, EventPageType } from '@pagopa-pn/pn-commons'; import { getCurrentEventTypePage, goToLoginPortal } from '../navigation.utility'; -import { APP_STATUS, DELEGHE, DETTAGLIO_NOTIFICA, LOGOUT, NOTIFICHE, RECAPITI } from '../routes.const'; +import { + APP_STATUS, + DELEGHE, + DETTAGLIO_NOTIFICA, + LOGOUT, + NOTIFICHE, + RECAPITI, +} from '../routes.const'; const mockOpenFn = vi.fn(); @@ -32,21 +39,21 @@ describe('Tests navigation utility methods', () => { }); it('goToLoginPortal - aar', () => { - goToLoginPortal('fake-aar-token'); + goToLoginPortal([AppRouteParams.AAR, 'fake-aar-token']); expect(mockOpenFn).toBeCalledTimes(1); - expect(mockOpenFn).toBeCalledWith( - `${LOGOUT}?aar=fake-aar-token`, - '_self' - ); + expect(mockOpenFn).toBeCalledWith(`${LOGOUT}?aar=fake-aar-token`, '_self'); + }); + + it('goToLoginPortal - retrievalId', () => { + goToLoginPortal([AppRouteParams.RETRIEVAL_ID, 'fake-id']); + expect(mockOpenFn).toBeCalledTimes(1); + expect(mockOpenFn).toBeCalledWith(`${LOGOUT}?retrievalId=fake-id`, '_self'); }); it('goToLoginPortal - aar with malicious code', () => { - goToLoginPortal('malicious-aar-token'); + goToLoginPortal([AppRouteParams.AAR, 'malicious-aar-token']); expect(mockOpenFn).toBeCalledTimes(1); - expect(mockOpenFn).toBeCalledWith( - `${LOGOUT}?aar=malicious-aar-token`, - '_self' - ); + expect(mockOpenFn).toBeCalledWith(`${LOGOUT}?aar=malicious-aar-token`, '_self'); }); it('getCurrentPage - test for notifications list page', () => { diff --git a/packages/pn-personafisica-webapp/src/navigation/navigation.utility.ts b/packages/pn-personafisica-webapp/src/navigation/navigation.utility.ts index fb61c763f7..e7aa49e6ed 100644 --- a/packages/pn-personafisica-webapp/src/navigation/navigation.utility.ts +++ b/packages/pn-personafisica-webapp/src/navigation/navigation.utility.ts @@ -13,13 +13,13 @@ import { RECAPITI, } from './routes.const'; -export function goToLoginPortal(aarToken?: string | null) { +export function goToLoginPortal(rapidAccess?: [AppRouteParams, string]) { // eslint-disable-next-line functional/no-let let urlToRedirect = `${LOGOUT}`; // the startsWith check is to prevent xss attacks - if (urlToRedirect.startsWith(LOGOUT) && aarToken) { + if (urlToRedirect.startsWith(LOGOUT) && rapidAccess) { // eslint-disable-next-line functional/immutable-data - urlToRedirect += `?${AppRouteParams.AAR}=${sanitizeString(aarToken)}`; + urlToRedirect += `?${rapidAccess[0]}=${sanitizeString(rapidAccess[1])}`; } // the indexOf check is to prevent xss attacks if (urlToRedirect.startsWith(LOGOUT)) { @@ -57,4 +57,4 @@ export const getCurrentEventTypePage = (location: string): EventPageType | undef } return pageType; -}; +}; \ No newline at end of file diff --git a/packages/pn-personafisica-webapp/src/navigation/routes.const.ts b/packages/pn-personafisica-webapp/src/navigation/routes.const.ts index 89562b9b7d..704e83b409 100644 --- a/packages/pn-personafisica-webapp/src/navigation/routes.const.ts +++ b/packages/pn-personafisica-webapp/src/navigation/routes.const.ts @@ -1,5 +1,4 @@ import { - AppRouteParams, PRIVACY_LINK_RELATIVE_PATH as PRIVACY_POLICY, TOS_LINK_RELATIVE_PATH as TERMS_OF_SERVICE, } from '@pagopa-pn/pn-commons'; @@ -11,7 +10,6 @@ export const PROFILO = '/profilo'; const NOTIFICA = '/dettaglio'; export const DETTAGLIO_NOTIFICA = `${NOTIFICHE}/:id${NOTIFICA}`; export const DETTAGLIO_NOTIFICA_DELEGATO = `${NOTIFICHE_DELEGATO}/:id${NOTIFICA}`; -export const DETTAGLIO_NOTIFICA_QRCODE_QUERY_PARAM = AppRouteParams.AAR; export const NUOVA_DELEGA = `${DELEGHE}/nuova`; export const GET_DETTAGLIO_NOTIFICA_PATH = (id: string) => `${NOTIFICHE}/${id}${NOTIFICA}`; export const GET_DETTAGLIO_NOTIFICA_DELEGATO_PATH = (id: string, mandateId: string) => diff --git a/packages/pn-personafisica-webapp/src/navigation/routes.tsx b/packages/pn-personafisica-webapp/src/navigation/routes.tsx index 318bbe1ab8..2affc537a4 100644 --- a/packages/pn-personafisica-webapp/src/navigation/routes.tsx +++ b/packages/pn-personafisica-webapp/src/navigation/routes.tsx @@ -10,7 +10,7 @@ import { } from '@pagopa-pn/pn-commons'; import { getConfiguration } from '../services/configuration.service'; -import AARGuard from './AARGuard'; +import RapidAccessGuard from './RapidAccessGuard'; import RouteGuard from './RouteGuard'; import SessionGuard from './SessionGuard'; import ToSGuard from './ToSGuard'; @@ -41,7 +41,7 @@ function Router() { {/* protected routes */} }> }> - }> + }> } /> } /> } /> diff --git a/packages/pn-personafisica-webapp/src/pages/NotificationDetail.page.tsx b/packages/pn-personafisica-webapp/src/pages/NotificationDetail.page.tsx index cf21b5f3d5..32bfdadb81 100644 --- a/packages/pn-personafisica-webapp/src/pages/NotificationDetail.page.tsx +++ b/packages/pn-personafisica-webapp/src/pages/NotificationDetail.page.tsx @@ -7,6 +7,7 @@ import { Alert, AlertTitle, Box, Grid, Paper, Stack, Typography } from '@mui/mat import { ApiError, ApiErrorWrapper, + AppRouteParams, EventPaymentRecipientType, GetDowntimeHistoryParams, LegalFactId, @@ -36,6 +37,7 @@ import { import DomicileBanner from '../components/DomicileBanner/DomicileBanner'; import LoadingPageWrapper from '../components/LoadingPageWrapper/LoadingPageWrapper'; +import { NotificationDetailRouteState } from '../models/NotificationDetail'; import { PFEventsType } from '../models/PFEventsType'; import { ContactSource } from '../models/contacts'; import * as routes from '../navigation/routes.const'; @@ -55,12 +57,6 @@ import { RootState } from '../redux/store'; import { getConfiguration } from '../services/configuration.service'; import PFEventStrategyFactory from '../utility/MixpanelUtils/PFEventStrategyFactory'; -// state for the invocations to this component -// (to include in navigation or Link to the route/s arriving to it) -type LocationState = { - fromQrCode?: boolean; // indicates whether the user arrived to the notification detail page from the QR code -}; - const NotificationDetail: React.FC = () => { const { id, mandateId } = useParams(); const location = useLocation(); @@ -378,8 +374,8 @@ const NotificationDetail: React.FC = () => { } }, []); - const fromQrCode = useMemo( - () => !!(location.state && (location.state as LocationState).fromQrCode), + const rapidAccessSource = useMemo( + () => (location.state as NotificationDetailRouteState)?.source, [location] ); @@ -387,14 +383,14 @@ const NotificationDetail: React.FC = () => { const backRoute = mandateId ? routes.GET_NOTIFICHE_DELEGATO_PATH(mandateId) : routes.NOTIFICHE; return ( navigate(backRoute)} /> ); - }, [fromQrCode, i18n.language]); + }, [rapidAccessSource, i18n.language]); const breadcrumb = ( @@ -440,7 +436,7 @@ const NotificationDetail: React.FC = () => { notificationStatus: notification.notificationStatus, checkIfUserHasPayments, userPayments, - fromQrCode, + source: rapidAccessSource, timeline: notification.timeline, }); diff --git a/packages/pn-personafisica-webapp/src/pages/__test__/NotificationDetail.page.test.tsx b/packages/pn-personafisica-webapp/src/pages/__test__/NotificationDetail.page.test.tsx index 9c925bb4f3..94fab900f7 100644 --- a/packages/pn-personafisica-webapp/src/pages/__test__/NotificationDetail.page.test.tsx +++ b/packages/pn-personafisica-webapp/src/pages/__test__/NotificationDetail.page.test.tsx @@ -4,6 +4,7 @@ import { vi } from 'vitest'; import { AppMessage, AppResponseMessage, + AppRouteParams, LegalFactId, NotificationDetail as NotificationDetailModel, NotificationDetailOtherDocument, @@ -46,7 +47,7 @@ import NotificationDetail from '../NotificationDetail.page'; const mockNavigateFn = vi.fn(); let mockIsDelegate = false; -let mockIsFromQrCode = false; +let mockSource: AppRouteParams | undefined = AppRouteParams.AAR; const mockAssignFn = vi.fn(); // mock imports @@ -57,7 +58,7 @@ vi.mock('react-router-dom', async () => ({ ? { id: 'DAPQ-LWQV-DKQH-202308-A-1', mandateId: '5' } : { id: 'DAPQ-LWQV-DKQH-202308-A-1' }, useNavigate: () => mockNavigateFn, - useLocation: () => ({ state: { fromQrCode: mockIsFromQrCode }, pathname: '/' }), + useLocation: () => ({ state: { source: mockSource }, pathname: '/' }), })); vi.mock('react-i18next', () => ({ @@ -100,7 +101,7 @@ describe('NotificationDetail Page', async () => { sessionStorage.removeItem(PAYMENT_CACHE_KEY); vi.clearAllMocks(); mock.reset(); - mockIsFromQrCode = false; + mockSource = undefined; mockIsDelegate = false; window.location.href = ''; }); @@ -474,7 +475,24 @@ describe('NotificationDetail Page', async () => { }); it('navigation from QR code - does not include back button', async () => { - mockIsFromQrCode = true; + mockSource = AppRouteParams.AAR; + mock.onGet(`/bff/v1/notifications/received/${notificationDTO.iun}`).reply(200, notificationDTO); + mock.onGet(`/bff/v1/payments/info`, paymentInfoRequest).reply(200, paymentInfo); + // we use regexp to not set the query parameters + mock.onGet(/\/bff\/v1\/downtime\/history.*/).reply(200, downtimesDTO); + await act(async () => { + result = render(, { + preloadedState: { + userState: { user: { fiscal_number: notificationDTO.recipients[2].taxId } }, + }, + }); + }); + const backButton = result.queryByTestId('breadcrumb-indietro-button'); + expect(backButton).not.toBeInTheDocument(); + }); + + it('navigation from Retrieval ID - does not include back button', async () => { + mockSource = AppRouteParams.RETRIEVAL_ID; mock.onGet(`/bff/v1/notifications/received/${notificationDTO.iun}`).reply(200, notificationDTO); mock.onGet(`/bff/v1/payments/info`, paymentInfoRequest).reply(200, paymentInfo); // we use regexp to not set the query parameters diff --git a/packages/pn-personafisica-webapp/src/redux/auth/__test__/reducers.test.ts b/packages/pn-personafisica-webapp/src/redux/auth/__test__/reducers.test.ts index 8295b5f228..285bb6fd56 100644 --- a/packages/pn-personafisica-webapp/src/redux/auth/__test__/reducers.test.ts +++ b/packages/pn-personafisica-webapp/src/redux/auth/__test__/reducers.test.ts @@ -37,7 +37,6 @@ describe('Auth redux state tests', () => { family_name: '', fiscal_number: '', email: '', - mobile_phone: '', from_aa: false, uid: '', level: '', @@ -81,7 +80,6 @@ describe('Auth redux state tests', () => { family_name: '', fiscal_number: '', email: '', - mobile_phone: '', from_aa: false, uid: '', level: '', diff --git a/packages/pn-personafisica-webapp/src/redux/auth/actions.ts b/packages/pn-personafisica-webapp/src/redux/auth/actions.ts index 3282fc4539..686241db8a 100644 --- a/packages/pn-personafisica-webapp/src/redux/auth/actions.ts +++ b/packages/pn-personafisica-webapp/src/redux/auth/actions.ts @@ -7,7 +7,7 @@ import { BffTosPrivacyActionBody, UserConsentsApiFactory, } from '../../generated-client/tos-privacy'; -import { User } from './types'; +import { TokenExchangeRequest, User } from '../../models/User'; export enum AUTH_ACTIONS { GET_TOS_PRIVACY_APPROVAL = 'getTosPrivacyApproval', @@ -18,11 +18,11 @@ export enum AUTH_ACTIONS { * Exchange token action between selfcare and pn. * If token is valid, user info are set in sessionStorage */ -export const exchangeToken = createAsyncThunk( +export const exchangeToken = createAsyncThunk( 'exchangeToken', - async (spidToken, { rejectWithValue }) => { + async (request: TokenExchangeRequest, { rejectWithValue }) => { try { - return await AuthApi.exchangeToken(spidToken); + return await AuthApi.exchangeToken(request); } catch (e: any) { return rejectWithValue(parseError(e)); } @@ -41,7 +41,6 @@ export const logout = createAsyncThunk('logout', async () => { family_name: '', fiscal_number: '', email: '', - mobile_phone: '', from_aa: false, uid: '', level: '', diff --git a/packages/pn-personafisica-webapp/src/redux/auth/reducers.ts b/packages/pn-personafisica-webapp/src/redux/auth/reducers.ts index 7edf9ea5ea..e4d943c392 100644 --- a/packages/pn-personafisica-webapp/src/redux/auth/reducers.ts +++ b/packages/pn-personafisica-webapp/src/redux/auth/reducers.ts @@ -10,8 +10,8 @@ import { } from '@pagopa-pn/pn-commons'; import { createSlice } from '@reduxjs/toolkit'; +import { SourceChannel, User } from '../../models/User'; import { acceptTosPrivacy, exchangeToken, getTosPrivacyApproval, logout } from './actions'; -import { User } from './types'; const userDataMatcher = yup .object({ @@ -23,13 +23,18 @@ const userDataMatcher = yup aud: yup.string().matches(dataRegex.simpleServer), iss: yup.string().url(), jti: yup.string().matches(dataRegex.lettersNumbersAndDashs), - mobile_phone: yup.string().matches(dataRegex.phoneNumber), + source: yup + .object({ + channel: yup.string().oneOf(Object.values(SourceChannel)), // UserSource.channel + details: yup.string(), + retrievalId: yup.string().matches(/^[ -~]{1,50}$/), + }) + .optional(), }) .noUnknown(true); const noLoggedUserData = { ...basicNoLoggedUserData, - mobile_phone: '', from_aa: false, level: '', iat: 0, diff --git a/packages/pn-personafisica-webapp/src/redux/auth/types.ts b/packages/pn-personafisica-webapp/src/redux/auth/types.ts deleted file mode 100644 index ef99d733a2..0000000000 --- a/packages/pn-personafisica-webapp/src/redux/auth/types.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { BasicUser } from "@pagopa-pn/pn-commons"; - -export interface User extends BasicUser { - // On 2023.01.19, I tried with several users in DEV and none of them - // includes mobile_phone in the response from token exchange. - // We leave the issue about the need to include this field to further investigation. - // ----------------------------- - // Carlos Lombardi, 2023.01.19 - mobile_phone: string; - from_aa: boolean; - aud: string; - level: string; - iat: number; - exp: number; - iss: string; - jti: string; -} diff --git a/packages/pn-personafisica-webapp/src/redux/notification/__test__/reducers.test.ts b/packages/pn-personafisica-webapp/src/redux/notification/__test__/reducers.test.ts index 09a433d843..036da52e63 100644 --- a/packages/pn-personafisica-webapp/src/redux/notification/__test__/reducers.test.ts +++ b/packages/pn-personafisica-webapp/src/redux/notification/__test__/reducers.test.ts @@ -25,9 +25,11 @@ import { } from '../../../__mocks__/NotificationDetail.mock'; import { createMockedStore } from '../../../__test__/test-utils'; import { apiClient } from '../../../api/apiClients'; +import { BffCheckTPPResponse } from '../../../generated-client/notifications'; import { getDowntimeLegalFact } from '../../appStatus/actions'; import { store } from '../../store'; import { + exchangeNotificationRetrievalId, getDowntimeHistory, getReceivedNotification, getReceivedNotificationDocument, @@ -61,6 +63,7 @@ const initialState = { paymentsData: { pagoPaF24: [], f24Only: [], + tpp: {}, }, downtimeEvents: [], }; @@ -440,4 +443,19 @@ describe('Notification detail redux state tests', () => { expect(action.type).toBe('getDowntimeLegalFact/fulfilled'); expect(action.payload).toEqual(mockResponse); }); + + it('Should be able to set paymentsData tpp', async () => { + const mockRetrievalId = 'mocked-retrieval-id'; + const mockResponse: BffCheckTPPResponse = { + paymentButton: 'Hype', + retrievalId: mockRetrievalId, + originId: 'mocked-iun', + }; + mock + .onGet(`/bff/v1/notifications/received/check-tpp?retrievalId=${mockRetrievalId}`) + .reply(200, mockResponse); + const action = await store.dispatch(exchangeNotificationRetrievalId(mockRetrievalId)); + expect(action.type).toBe('exchangeNotificationRetrievalId/fulfilled'); + expect(action.payload).toEqual(mockResponse); + }); }); diff --git a/packages/pn-personafisica-webapp/src/redux/notification/actions.ts b/packages/pn-personafisica-webapp/src/redux/notification/actions.ts index 1e9bb13870..cc05cd52b2 100644 --- a/packages/pn-personafisica-webapp/src/redux/notification/actions.ts +++ b/packages/pn-personafisica-webapp/src/redux/notification/actions.ts @@ -25,6 +25,7 @@ import { DowntimeApiFactory } from '../../generated-client/downtime-logs'; import { BffCheckAarRequest, BffCheckAarResponse, + BffCheckTPPResponse, NotificationReceivedApiFactory, } from '../../generated-client/notifications'; import { PaymentsApiFactory } from '../../generated-client/payments'; @@ -43,6 +44,7 @@ export enum NOTIFICATION_ACTIONS { GET_RECEIVED_NOTIFICATION_PAYMENT_URL = 'getReceivedNotificationPaymentUrl', GET_DOWNTIME_HISTORY = 'getNotificationDowntimeHistory', EXCHANGE_NOTIFICATION_QR_CODE = 'exchangeNotificationQrCode', + EXCHANGE_NOTIFICATION_RETRIEVAL_ID = 'exchangeNotificationRetrievalId', } export const getReceivedNotification = createAsyncThunk< @@ -262,19 +264,36 @@ export const getDowntimeHistory = createAsyncThunk( +export const exchangeNotificationQrCode = createAsyncThunk( NOTIFICATION_ACTIONS.EXCHANGE_NOTIFICATION_QR_CODE, - async (params: BffCheckAarRequest, { rejectWithValue }) => { + async (aarQrCodeValue: string, { rejectWithValue }) => { try { const notificationReceivedApiFactory = NotificationReceivedApiFactory( undefined, undefined, apiClient ); - const response = await notificationReceivedApiFactory.checkAarQrCodeV1(params); + const request: BffCheckAarRequest = { aarQrCodeValue }; + const response = await notificationReceivedApiFactory.checkAarQrCodeV1(request); + return response.data; + } catch (e: any) { + return rejectWithValue(parseError(e)); + } + } +); + +export const exchangeNotificationRetrievalId = createAsyncThunk( + NOTIFICATION_ACTIONS.EXCHANGE_NOTIFICATION_RETRIEVAL_ID, + async (retrievalId: string, { rejectWithValue }) => { + try { + const notificationReceivedApiFactory = NotificationReceivedApiFactory( + undefined, + undefined, + apiClient + ); + const response = await notificationReceivedApiFactory.checkTppV1(retrievalId); return response.data; } catch (e: any) { - return rejectWithValue(parseError(e)); } } diff --git a/packages/pn-personafisica-webapp/src/redux/notification/reducers.ts b/packages/pn-personafisica-webapp/src/redux/notification/reducers.ts index 09e1121905..e3ada9e6cb 100644 --- a/packages/pn-personafisica-webapp/src/redux/notification/reducers.ts +++ b/packages/pn-personafisica-webapp/src/redux/notification/reducers.ts @@ -18,8 +18,10 @@ import { } from '@pagopa-pn/pn-commons'; import { createSlice } from '@reduxjs/toolkit'; +import { BffCheckTPPResponse } from '../../generated-client/notifications'; import { NotificationDetailForRecipient } from '../../models/NotificationDetail'; import { + exchangeNotificationRetrievalId, getDowntimeHistory, getReceivedNotification, getReceivedNotificationPaymentInfo, @@ -50,6 +52,7 @@ const initialState = { paymentsData: { pagoPaF24: [] as Array, f24Only: [] as Array, + tpp: {} as BffCheckTPPResponse, }, downtimeEvents: [] as Array, }; @@ -156,6 +159,9 @@ const notificationSlice = createSlice({ builder.addCase(getDowntimeHistory.fulfilled, (state, action) => { state.downtimeEvents = action.payload.result; }); + builder.addCase(exchangeNotificationRetrievalId.fulfilled, (state, action) => { + state.paymentsData.tpp = action.payload; + }); }, }); diff --git a/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/SendNotificationDetailStrategy.ts b/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/SendNotificationDetailStrategy.ts index 3b53ea35e1..602e6db1c4 100644 --- a/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/SendNotificationDetailStrategy.ts +++ b/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/SendNotificationDetailStrategy.ts @@ -1,4 +1,5 @@ import { + AppRouteParams, Downtime, EventAction, EventCategory, @@ -13,6 +14,7 @@ import { TimelineCategory, TrackedEvent, } from '@pagopa-pn/pn-commons'; +import { appRouteParamToEventSource } from '../../notification.utility'; type NotificationData = { downtimeEvents: Array; @@ -20,7 +22,7 @@ type NotificationData = { notificationStatus: NotificationStatus; checkIfUserHasPayments: boolean; userPayments: { pagoPaF24: Array; f24Only: Array }; - fromQrCode: boolean; + source: AppRouteParams | undefined; timeline: Array; }; @@ -31,7 +33,7 @@ export class SendNotificationDetailStrategy implements EventStrategy { notificationStatus, checkIfUserHasPayments, userPayments, - fromQrCode, + source, timeline, }: NotificationData): TrackedEvent { // eslint-disable-next-line functional/no-let @@ -64,8 +66,8 @@ export class SendNotificationDetailStrategy implements EventStrategy { contains_f24: hasF24 ? 'yes' : 'no', first_time_opening: timeline.findIndex((el) => el.category === TimelineCategory.NOTIFICATION_VIEWED) === -1, - source: fromQrCode ? 'QRcode' : 'LISTA_NOTIFICHE', + source: appRouteParamToEventSource(source) || 'LISTA_NOTIFICHE', }, }; } -} +} \ No newline at end of file diff --git a/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/TechStrategy.ts b/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/TechStrategy.ts index e03aed1484..d9f7bb65bd 100644 --- a/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/TechStrategy.ts +++ b/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/TechStrategy.ts @@ -1,15 +1,24 @@ import { + AppRouteParams, EventCategory, EventPropertyType, EventStrategy, TrackedEvent, } from '@pagopa-pn/pn-commons'; +import { EventNotificationSource } from '@pagopa-pn/pn-commons/src/models/MixpanelEvents'; + +import { appRouteParamToEventSource } from '../../notification.utility'; + +type Tech = { + source?: EventNotificationSource; +}; export class TechStrategy implements EventStrategy { - performComputations(): TrackedEvent { + performComputations(data?: { source: AppRouteParams }): TrackedEvent { return { [EventPropertyType.TRACK]: { event_category: EventCategory.TECH, + source: appRouteParamToEventSource(data?.source), }, }; } diff --git a/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/__test__/SendNotificationDetailStrategy.test.ts b/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/__test__/SendNotificationDetailStrategy.test.ts index 170d3db339..1a7be5538f 100644 --- a/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/__test__/SendNotificationDetailStrategy.test.ts +++ b/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/__test__/SendNotificationDetailStrategy.test.ts @@ -1,4 +1,5 @@ import { + AppRouteParams, DowntimeStatus, EventAction, EventCategory, @@ -32,7 +33,7 @@ describe('Mixpanel - Notification detail Strategy', () => { pagoPaF24: paymentsData.pagoPaF24, f24Only: paymentsData.f24Only, }, - fromQrCode: false, + source: AppRouteParams.AAR, timeline: timeline, }; @@ -72,7 +73,7 @@ describe('Mixpanel - Notification detail Strategy', () => { contains_f24: hasF24 ? 'yes' : 'no', first_time_opening: timeline.findIndex((el) => el.category === TimelineCategory.NOTIFICATION_VIEWED) === -1, - source: notificationData.fromQrCode ? 'QRcode' : 'LISTA_NOTIFICHE', + source: 'QRcode', }, }); }); diff --git a/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/__test__/TechStrategy.test.ts b/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/__test__/TechStrategy.test.ts index 6d65d987eb..a277e7768f 100644 --- a/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/__test__/TechStrategy.test.ts +++ b/packages/pn-personafisica-webapp/src/utility/MixpanelUtils/Strategies/__test__/TechStrategy.test.ts @@ -1,4 +1,4 @@ -import { EventCategory, EventPropertyType } from '@pagopa-pn/pn-commons'; +import { AppRouteParams, EventCategory, EventPropertyType } from '@pagopa-pn/pn-commons'; import { TechStrategy } from '../TechStrategy'; @@ -13,4 +13,16 @@ describe('Mixpanel - Tech Strategy', () => { }, }); }); + + it('should return tech event with additional properties', () => { + const strategy = new TechStrategy(); + + const techEvent = strategy.performComputations({ source: AppRouteParams.AAR }); + expect(techEvent).toEqual({ + [EventPropertyType.TRACK]: { + event_category: EventCategory.TECH, + source: 'QRcode', + }, + }); + }); }); diff --git a/packages/pn-personafisica-webapp/src/utility/notification.utility.ts b/packages/pn-personafisica-webapp/src/utility/notification.utility.ts index 530673f3df..5be05e2d90 100644 --- a/packages/pn-personafisica-webapp/src/utility/notification.utility.ts +++ b/packages/pn-personafisica-webapp/src/utility/notification.utility.ts @@ -1,4 +1,5 @@ -import { NotificationDetail } from '@pagopa-pn/pn-commons'; +import { AppRouteParams, NotificationDetail } from '@pagopa-pn/pn-commons'; +import { EventNotificationSource } from '@pagopa-pn/pn-commons/src/models/MixpanelEvents'; import { NotificationDetailForRecipient } from '../models/NotificationDetail'; import { Delegator } from '../redux/delegation/types'; @@ -40,3 +41,15 @@ export function parseNotificationDetailForRecipient( currentRecipientIndex, }; } + +export const appRouteParamToEventSource = ( + param: AppRouteParams | undefined +): EventNotificationSource | undefined => { + if (param === AppRouteParams.AAR) { + return 'QRcode'; + } + if (param === AppRouteParams.RETRIEVAL_ID) { + return '3Papp'; + } + return undefined; +}; diff --git a/packages/pn-personagiuridica-webapp/src/App.tsx b/packages/pn-personagiuridica-webapp/src/App.tsx index 12560ed691..3a2905fd0b 100644 --- a/packages/pn-personagiuridica-webapp/src/App.tsx +++ b/packages/pn-personagiuridica-webapp/src/App.tsx @@ -33,7 +33,7 @@ import { PartyEntity, ProductEntity } from '@pagopa/mui-italia'; import Router from './navigation/routes'; import * as routes from './navigation/routes.const'; import { getCurrentAppStatus } from './redux/appStatus/actions'; -import { PNRole } from './redux/auth/types'; +import { PNRole } from './models/User'; import { getDigitalAddresses } from './redux/contact/actions'; import { useAppDispatch, useAppSelector } from './redux/hooks'; import { getSidemenuInformation } from './redux/sidemenu/actions'; diff --git a/packages/pn-personagiuridica-webapp/src/__mocks__/Auth.mock.ts b/packages/pn-personagiuridica-webapp/src/__mocks__/Auth.mock.ts index 31e9cb8dbb..0f35237333 100644 --- a/packages/pn-personagiuridica-webapp/src/__mocks__/Auth.mock.ts +++ b/packages/pn-personagiuridica-webapp/src/__mocks__/Auth.mock.ts @@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import { authClient } from '../api/apiClients'; import { AUTH_TOKEN_EXCHANGE } from '../api/auth/auth.routes'; import { exchangeToken, logout } from '../redux/auth/actions'; -import { PNRole, PartyRole, User } from '../redux/auth/types'; +import { PNRole, PartyRole, User } from '../models/User'; import { store } from '../redux/store'; export const mockLogin = async (body: User | string = userResponse): Promise => { diff --git a/packages/pn-personagiuridica-webapp/src/__test__/App.test.tsx b/packages/pn-personagiuridica-webapp/src/__test__/App.test.tsx index 2667a6ad00..7e0e4c125b 100644 --- a/packages/pn-personagiuridica-webapp/src/__test__/App.test.tsx +++ b/packages/pn-personagiuridica-webapp/src/__test__/App.test.tsx @@ -12,7 +12,7 @@ import { digitalAddresses } from '../__mocks__/Contacts.mock'; import { apiClient } from '../api/apiClients'; import { DelegationStatus } from '../models/Deleghe'; import { SELFCARE_LOGOUT } from '../navigation/routes.const'; -import { PNRole, PartyRole } from '../redux/auth/types'; +import { PNRole, PartyRole } from '../models/User'; import { getConfiguration } from '../services/configuration.service'; import { RenderResult, act, render } from './test-utils'; diff --git a/packages/pn-personagiuridica-webapp/src/api/auth/Auth.api.ts b/packages/pn-personagiuridica-webapp/src/api/auth/Auth.api.ts index 2400c84aea..f470c3d4ce 100644 --- a/packages/pn-personagiuridica-webapp/src/api/auth/Auth.api.ts +++ b/packages/pn-personagiuridica-webapp/src/api/auth/Auth.api.ts @@ -1,4 +1,4 @@ -import { User } from '../../redux/auth/types'; +import { User } from '../../models/User'; import { authClient } from '../apiClients'; import { AUTH_TOKEN_EXCHANGE } from './auth.routes'; diff --git a/packages/pn-personagiuridica-webapp/src/components/Deleghe/DelegationsElements.tsx b/packages/pn-personagiuridica-webapp/src/components/Deleghe/DelegationsElements.tsx index 37f165d122..ac8feb828f 100644 --- a/packages/pn-personagiuridica-webapp/src/components/Deleghe/DelegationsElements.tsx +++ b/packages/pn-personagiuridica-webapp/src/components/Deleghe/DelegationsElements.tsx @@ -16,7 +16,7 @@ import { Tag } from '@pagopa/mui-italia'; import { AnyAction } from '@reduxjs/toolkit'; import { DelegationColumnData, DelegationStatus } from '../../models/Deleghe'; -import { User } from '../../redux/auth/types'; +import { User } from '../../models/User'; import { acceptMandate, rejectMandate, diff --git a/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeyContextMenu.tsx b/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeyContextMenu.tsx index 22b9aabe8b..83208aa8f3 100644 --- a/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeyContextMenu.tsx +++ b/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeyContextMenu.tsx @@ -7,7 +7,7 @@ import { Row, useHasPermissions } from '@pagopa-pn/pn-commons'; import { BffVirtualKeysResponse, VirtualKeyStatus } from '../../generated-client/pg-apikeys'; import { ApiKeyColumnData, ModalApiKeyView } from '../../models/ApiKeys'; -import { PNRole } from '../../redux/auth/types'; +import { PNRole } from '../../models/User'; import { useAppSelector } from '../../redux/hooks'; import { RootState } from '../../redux/store'; diff --git a/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeys.tsx b/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeys.tsx index ed984427c1..c6810ec8c4 100644 --- a/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeys.tsx +++ b/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeys.tsx @@ -24,7 +24,7 @@ import { deleteVirtualApiKey, getVirtualApiKeys, } from '../../redux/apikeys/actions'; -import { PNRole } from '../../redux/auth/types'; +import { PNRole } from '../../models/User'; import { useAppDispatch, useAppSelector } from '../../redux/hooks'; import { RootState } from '../../redux/store'; import ApiKeyModal from './ApiKeyModal'; diff --git a/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeysTable.tsx b/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeysTable.tsx index 48aa6271fa..3ebe136f99 100644 --- a/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeysTable.tsx +++ b/packages/pn-personagiuridica-webapp/src/components/IntegrazioneApi/VirtualKeysTable.tsx @@ -18,7 +18,7 @@ import { import { BffVirtualKeysResponse, VirtualKey } from '../../generated-client/pg-apikeys'; import { ApiKeyColumnData, ModalApiKeyView } from '../../models/ApiKeys'; -import { PNRole } from '../../redux/auth/types'; +import { PNRole } from '../../models/User'; import { useAppSelector } from '../../redux/hooks'; import { RootState } from '../../redux/store'; import ApiKeysDataSwitch from './ApiKeysDataSwitch'; diff --git a/packages/pn-personagiuridica-webapp/src/redux/auth/types.ts b/packages/pn-personagiuridica-webapp/src/models/User.ts similarity index 99% rename from packages/pn-personagiuridica-webapp/src/redux/auth/types.ts rename to packages/pn-personagiuridica-webapp/src/models/User.ts index 4197aff53b..7417d50234 100644 --- a/packages/pn-personagiuridica-webapp/src/redux/auth/types.ts +++ b/packages/pn-personagiuridica-webapp/src/models/User.ts @@ -36,4 +36,4 @@ export interface User extends BasicUser { organization: Organization; desired_exp: number; hasGroup?: boolean; -} +} \ No newline at end of file diff --git a/packages/pn-personagiuridica-webapp/src/navigation/routes.tsx b/packages/pn-personagiuridica-webapp/src/navigation/routes.tsx index c591a7158d..993607209b 100644 --- a/packages/pn-personagiuridica-webapp/src/navigation/routes.tsx +++ b/packages/pn-personagiuridica-webapp/src/navigation/routes.tsx @@ -12,7 +12,7 @@ import { import DelegatesByCompany from '../components/Deleghe/DelegatesByCompany'; import DelegationsOfTheCompany from '../components/Deleghe/DelegationsOfTheCompany'; -import { PNRole } from '../redux/auth/types'; +import { PNRole } from '../models/User'; import { useAppSelector } from '../redux/hooks'; import { RootState } from '../redux/store'; import { getConfiguration } from '../services/configuration.service'; diff --git a/packages/pn-personagiuridica-webapp/src/pages/ApiIntegration.page.tsx b/packages/pn-personagiuridica-webapp/src/pages/ApiIntegration.page.tsx index c66c43bc8c..f0858ac8c6 100644 --- a/packages/pn-personagiuridica-webapp/src/pages/ApiIntegration.page.tsx +++ b/packages/pn-personagiuridica-webapp/src/pages/ApiIntegration.page.tsx @@ -13,7 +13,7 @@ import { PublicKeysIssuerResponseIssuerStatusEnum, } from '../generated-client/pg-apikeys'; import { checkPublicKeyIssuer, getPublicKeys, getVirtualApiKeys } from '../redux/apikeys/actions'; -import { PNRole } from '../redux/auth/types'; +import { PNRole } from '../models/User'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { RootState } from '../redux/store'; diff --git a/packages/pn-personagiuridica-webapp/src/pages/NotificationDetail.page.tsx b/packages/pn-personagiuridica-webapp/src/pages/NotificationDetail.page.tsx index d1cf62ea9c..0d4ba87a93 100644 --- a/packages/pn-personagiuridica-webapp/src/pages/NotificationDetail.page.tsx +++ b/packages/pn-personagiuridica-webapp/src/pages/NotificationDetail.page.tsx @@ -39,7 +39,7 @@ import LoadingPageWrapper from '../components/LoadingPageWrapper/LoadingPageWrap import { ContactSource } from '../models/contacts'; import * as routes from '../navigation/routes.const'; import { getDowntimeLegalFact } from '../redux/appStatus/actions'; -import { PNRole } from '../redux/auth/types'; +import { PNRole } from '../models/User'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; import { NOTIFICATION_ACTIONS, diff --git a/packages/pn-personagiuridica-webapp/src/pages/Notifiche.page.tsx b/packages/pn-personagiuridica-webapp/src/pages/Notifiche.page.tsx index 3186b66a98..9951aa017b 100644 --- a/packages/pn-personagiuridica-webapp/src/pages/Notifiche.page.tsx +++ b/packages/pn-personagiuridica-webapp/src/pages/Notifiche.page.tsx @@ -20,7 +20,7 @@ import DesktopNotifications from '../components/Notifications/DesktopNotificatio import GroupSelector from '../components/Notifications/GroupSelector'; import MobileNotifications from '../components/Notifications/MobileNotifications'; import { ContactSource } from '../models/contacts'; -import { PNRole } from '../redux/auth/types'; +import { PNRole } from '../models/User'; import { DASHBOARD_ACTIONS, getReceivedNotifications } from '../redux/dashboard/actions'; import { setNotificationFilters, setPagination, setSorting } from '../redux/dashboard/reducers'; import { useAppDispatch, useAppSelector } from '../redux/hooks'; diff --git a/packages/pn-personagiuridica-webapp/src/redux/auth/__test__/reducers.test.ts b/packages/pn-personagiuridica-webapp/src/redux/auth/__test__/reducers.test.ts index a345fbe5aa..184f2d05dc 100644 --- a/packages/pn-personagiuridica-webapp/src/redux/auth/__test__/reducers.test.ts +++ b/packages/pn-personagiuridica-webapp/src/redux/auth/__test__/reducers.test.ts @@ -11,7 +11,7 @@ import { errorMock } from '../../../__mocks__/Errors.mock'; import { apiClient } from '../../../api/apiClients'; import { store } from '../../store'; import { acceptTosPrivacy, getTosPrivacyApproval } from '../actions'; -import { PNRole, PartyRole } from '../types'; +import { PNRole, PartyRole } from '../../../models/User'; describe('Auth redux state tests', () => { let mock: MockAdapter; diff --git a/packages/pn-personagiuridica-webapp/src/redux/auth/actions.ts b/packages/pn-personagiuridica-webapp/src/redux/auth/actions.ts index 253009ab2e..71c22edc41 100644 --- a/packages/pn-personagiuridica-webapp/src/redux/auth/actions.ts +++ b/packages/pn-personagiuridica-webapp/src/redux/auth/actions.ts @@ -7,7 +7,7 @@ import { BffTosPrivacyActionBody, UserConsentsApiFactory, } from '../../generated-client/tos-privacy'; -import { PNRole, PartyRole, User } from './types'; +import { PNRole, PartyRole, User } from '../../models/User'; export enum AUTH_ACTIONS { GET_TOS_PRIVACY_APPROVAL = 'getTosPrivacyApproval', diff --git a/packages/pn-personagiuridica-webapp/src/redux/auth/reducers.ts b/packages/pn-personagiuridica-webapp/src/redux/auth/reducers.ts index 520c36a1ea..eb61d2efee 100644 --- a/packages/pn-personagiuridica-webapp/src/redux/auth/reducers.ts +++ b/packages/pn-personagiuridica-webapp/src/redux/auth/reducers.ts @@ -10,8 +10,8 @@ import { } from '@pagopa-pn/pn-commons'; import { createSlice } from '@reduxjs/toolkit'; +import { PNRole, PartyRole, User } from '../../models/User'; import { acceptTosPrivacy, exchangeToken, getTosPrivacyApproval, logout } from './actions'; -import { PNRole, PartyRole, User } from './types'; const roleMatcher = yup.object({ role: yup.string().oneOf(Object.values(PNRole)), From 96accd97aceca1ecbec43a384e68eac7ddaf3eb9 Mon Sep 17 00:00:00 2001 From: Francesco Bianchi <105631409+fbianchicodermine@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:17:40 +0100 Subject: [PATCH 2/3] feat(pn-13915): show custom payment button when user comes from TPP app (#1468) --- .../NotificationPaymentRecipient.tsx | 179 ++++++++++++------ .../NotificationPaymentRecipient.test.tsx | 48 +++++ .../src/models/NotificationDetail.ts | 6 + .../pn-personafisica-webapp/openapitools.json | 2 +- .../public/locales/it/notifiche.json | 2 + .../src/navigation/RapidAccessGuard.tsx | 7 +- .../src/pages/NotificationDetail.page.tsx | 39 ++++ .../__test__/NotificationDetail.page.test.tsx | 39 ++++ .../notification/__test__/reducers.test.ts | 18 -- .../src/redux/notification/actions.ts | 33 ++-- .../src/redux/notification/reducers.ts | 9 +- .../redux/sidemenu/__test__/reducers.test.ts | 19 +- .../src/redux/sidemenu/actions.ts | 26 +++ .../src/redux/sidemenu/reducers.ts | 22 ++- 14 files changed, 351 insertions(+), 98 deletions(-) diff --git a/packages/pn-commons/src/components/NotificationDetail/NotificationPaymentRecipient.tsx b/packages/pn-commons/src/components/NotificationDetail/NotificationPaymentRecipient.tsx index e80079bde5..b8eeb09d8e 100644 --- a/packages/pn-commons/src/components/NotificationDetail/NotificationPaymentRecipient.tsx +++ b/packages/pn-commons/src/components/NotificationDetail/NotificationPaymentRecipient.tsx @@ -1,4 +1,4 @@ -import React, { Fragment, memo, useEffect, useState } from 'react'; +import React, { memo, useEffect, useState } from 'react'; import { Download } from '@mui/icons-material/'; import { Alert, Box, Button, Link, RadioGroup, Typography } from '@mui/material'; @@ -14,6 +14,7 @@ import { PaymentStatus, PaymentsData, } from '../../models'; +import { PaymentTpp } from '../../models/NotificationDetail'; import { formatEurocentToCurrency } from '../../utility'; import { getLocalizedOrDefaultLabel } from '../../utility/localization.utility'; import { getPaymentCache, setPaymentCache } from '../../utility/paymentCaching.utility'; @@ -26,6 +27,7 @@ const FAQ_NOTIFICATION_CANCELLED_REFUND = '/faq#notifica-pagata-rimborso'; type Props = { payments: PaymentsData; + paymentTpp?: PaymentTpp; isCancelled: boolean; timerF24: number; landingSiteUrl: string; @@ -38,18 +40,21 @@ type Props = { unwrap: () => Promise; }; onPayClick: (noticeCode?: string, creditorTaxId?: string, amount?: number) => void; + onPayTppClick?: (noticeCode?: string, creditorTaxId?: string, retrievalId?: string) => void; handleTrackEvent?: (event: EventPaymentRecipientType, param?: object) => void; handleFetchPaymentsInfo: (payment: Array) => void; }; const NotificationPaymentRecipient: React.FC = ({ payments, + paymentTpp, isCancelled, timerF24, landingSiteUrl, iun, getPaymentAttachmentAction, onPayClick, + onPayTppClick, handleTrackEvent, handleFetchPaymentsInfo, }) => { @@ -137,9 +142,17 @@ const NotificationPaymentRecipient: React.FC = ({ } }; - const handleCheckPaymentSelected = () => { + const handleCheckPaymentSelected = (paymentType: 'default' | 'tpp') => { if (selectedPayment.pagoPa) { setErrorOnPayment(false); + if (paymentType === 'tpp') { + onPayTppClick?.( + selectedPayment?.pagoPa?.noticeCode, + selectedPayment?.pagoPa?.creditorTaxId, + paymentTpp?.retrievalId + ); + return; + } onPayClick( selectedPayment.pagoPa.noticeCode, selectedPayment.pagoPa.creditorTaxId, @@ -233,57 +246,19 @@ const NotificationPaymentRecipient: React.FC = ({ )} {!allPaymentsIsPaid && ( - - - {selectedPayment?.pagoPa?.attachment && ( - - )} - {selectedPayment?.f24 ? ( - - - handleTrackEventFn(EventPaymentRecipientType.SEND_F24_DOWNLOAD) - } - handleTrackDownloadF24Success={() => - handleTrackEventFn(EventPaymentRecipientType.SEND_F24_DOWNLOAD_SUCCESS) - } - handleTrackDownloadF24Timeout={() => - handleTrackEventFn(EventPaymentRecipientType.SEND_F24_DOWNLOAD_TIMEOUT) - } - timerF24={timerF24} - disableDownload={areOtherDowloading} - handleDownload={setAreOtherDowloading} - /> - - ) : null} - + )} )} @@ -323,3 +298,101 @@ const NotificationPaymentRecipient: React.FC = ({ }; export default memo(NotificationPaymentRecipient); + +type PaymentButtonsProps = Pick< + Props, + 'paymentTpp' | 'iun' | 'getPaymentAttachmentAction' | 'timerF24' +> & { + selectedPayment?: PaymentDetails | { pagoPa: null; f24?: null }; + areOtherDowloading: boolean; + errorOnPayment: boolean; + setAreOtherDowloading: (value: boolean) => void; + downloadAttachment: (attachmentName: PaymentAttachmentSName) => void; + handleTrackEventFn: (event: EventPaymentRecipientType, param?: object) => void; + handleCheckPaymentSelected: (paymentType: 'default' | 'tpp') => void; +}; + +const PaymentButtons = ({ + paymentTpp, + iun, + selectedPayment, + timerF24, + areOtherDowloading, + errorOnPayment, + setAreOtherDowloading, + downloadAttachment, + getPaymentAttachmentAction, + handleTrackEventFn, + handleCheckPaymentSelected, +}: PaymentButtonsProps) => { + const hasPaymentTpp = paymentTpp?.iun === iun; + return ( + <> + {hasPaymentTpp && ( + + )} + + {selectedPayment?.pagoPa?.attachment && ( + + )} + {selectedPayment?.f24 && ( + + + handleTrackEventFn(EventPaymentRecipientType.SEND_F24_DOWNLOAD) + } + handleTrackDownloadF24Success={() => + handleTrackEventFn(EventPaymentRecipientType.SEND_F24_DOWNLOAD_SUCCESS) + } + handleTrackDownloadF24Timeout={() => + handleTrackEventFn(EventPaymentRecipientType.SEND_F24_DOWNLOAD_TIMEOUT) + } + timerF24={timerF24} + disableDownload={areOtherDowloading} + handleDownload={setAreOtherDowloading} + /> + + )} + + ); +}; diff --git a/packages/pn-commons/src/components/NotificationDetail/__test__/NotificationPaymentRecipient.test.tsx b/packages/pn-commons/src/components/NotificationDetail/__test__/NotificationPaymentRecipient.test.tsx index 2ac828b8bd..402b0319c2 100644 --- a/packages/pn-commons/src/components/NotificationDetail/__test__/NotificationPaymentRecipient.test.tsx +++ b/packages/pn-commons/src/components/NotificationDetail/__test__/NotificationPaymentRecipient.test.tsx @@ -432,4 +432,52 @@ describe('NotificationPaymentRecipient Component', () => { fireEvent.click(f24ButtonToCheck); expect(getPaymentAttachmentActionMk).toBeCalledTimes(1); }); + + it('should show tpp button if payments tpp is present and click onPayTppClick', () => { + const onPayTppClick = vi.fn(); + const paymentTpp = { + retrievalId: 'retrievalId', + iun, + paymentButton: 'paymentButton', + }; + const { getByTestId, queryAllByTestId } = render( + {}} + onPayTppClick={onPayTppClick} + handleFetchPaymentsInfo={() => {}} + landingSiteUrl="" + /> + ); + const payTppButton = getByTestId('tpp-pay-button'); + expect(payTppButton).toBeInTheDocument(); + + const payButton = getByTestId('pay-button'); + expect(payButton).toHaveTextContent('detail.payment.pay-with-other-methods'); + + const pageSelector = getByTestId('pageSelector'); + const pageButtons = pageSelector?.querySelectorAll('button'); + fireEvent.click(pageButtons[1]); + + // select payment + const paymentIndex = paymentsData.pagoPaF24.findIndex( + (payment) => payment.pagoPa?.status === PaymentStatus.REQUIRED + ); + const item = queryAllByTestId('pagopa-item')[paymentIndex]; + const radioButton = item.querySelector('[data-testid="radio-button"] input'); + fireEvent.click(radioButton!); + + // click pay + fireEvent.click(payTppButton); + expect(onPayTppClick).toHaveBeenCalledWith( + paymentsData.pagoPaF24[paymentIndex].pagoPa?.noticeCode, + paymentsData.pagoPaF24[paymentIndex].pagoPa?.creditorTaxId, + paymentTpp.retrievalId + ); + }); }); diff --git a/packages/pn-commons/src/models/NotificationDetail.ts b/packages/pn-commons/src/models/NotificationDetail.ts index b8ae38e635..5f567e85bf 100644 --- a/packages/pn-commons/src/models/NotificationDetail.ts +++ b/packages/pn-commons/src/models/NotificationDetail.ts @@ -23,6 +23,12 @@ export interface NotificationDetail { radd?: INotificationDetailTimeline; } +export type PaymentTpp = { + paymentButton: string; + retrievalId: string; + iun: string; +}; + export type PaymentsData = { pagoPaF24: Array; f24Only: Array; diff --git a/packages/pn-personafisica-webapp/openapitools.json b/packages/pn-personafisica-webapp/openapitools.json index 9706144975..2393e611f2 100644 --- a/packages/pn-personafisica-webapp/openapitools.json +++ b/packages/pn-personafisica-webapp/openapitools.json @@ -39,7 +39,7 @@ }, "bff-payments": { "generatorName": "typescript-axios", - "inputSpec": "https://raw.githubusercontent.com/pagopa/pn-bff/5355801397984dd3c3187e417d49e90c6c4076ae/docs/openapi/api-external-pn-bff-payments.yaml", + "inputSpec": "https://raw.githubusercontent.com/pagopa/pn-bff/0857ea8cbecae4f892bbb2ed19f24f91db2fcbb2/docs/openapi/api-external-pn-bff-payments.yaml", "output": "./src/generated-client/payments", "additionalProperties": { "supportsES6": true, diff --git a/packages/pn-personafisica-webapp/public/locales/it/notifiche.json b/packages/pn-personafisica-webapp/public/locales/it/notifiche.json index d61aa1e880..0f8f8bf465 100644 --- a/packages/pn-personafisica-webapp/public/locales/it/notifiche.json +++ b/packages/pn-personafisica-webapp/public/locales/it/notifiche.json @@ -80,6 +80,8 @@ "summary-in-progress": "Pagamento in corso", "amount": "Importo", "submit": "Paga", + "submit-tpp": "Paga con {{name}}", + "pay-with-other-methods": "Paga con altri metodi di pagamento", "download-f24": "Scarica F24", "pay-with-f24": "Se preferisci, puoi pagare tramite F24.", "download-f24-in-progress": "Documento in elaborazione", diff --git a/packages/pn-personafisica-webapp/src/navigation/RapidAccessGuard.tsx b/packages/pn-personafisica-webapp/src/navigation/RapidAccessGuard.tsx index 274541fb0e..da4b58b3ac 100644 --- a/packages/pn-personafisica-webapp/src/navigation/RapidAccessGuard.tsx +++ b/packages/pn-personafisica-webapp/src/navigation/RapidAccessGuard.tsx @@ -16,11 +16,8 @@ import { NotificationDetailRouteState } from '../models/NotificationDetail'; import { NotificationId } from '../models/Notifications'; import { PFEventsType } from '../models/PFEventsType'; import { useAppDispatch } from '../redux/hooks'; -import { - NOTIFICATION_ACTIONS, - exchangeNotificationQrCode, - exchangeNotificationRetrievalId, -} from '../redux/notification/actions'; +import { NOTIFICATION_ACTIONS, exchangeNotificationQrCode } from '../redux/notification/actions'; +import { exchangeNotificationRetrievalId } from '../redux/sidemenu/actions'; import { ServerResponseErrorCode } from '../utility/AppError/types'; import PFEventStrategyFactory from '../utility/MixpanelUtils/PFEventStrategyFactory'; import { diff --git a/packages/pn-personafisica-webapp/src/pages/NotificationDetail.page.tsx b/packages/pn-personafisica-webapp/src/pages/NotificationDetail.page.tsx index 32bfdadb81..82487ab0d9 100644 --- a/packages/pn-personafisica-webapp/src/pages/NotificationDetail.page.tsx +++ b/packages/pn-personafisica-webapp/src/pages/NotificationDetail.page.tsx @@ -50,9 +50,11 @@ import { getReceivedNotificationDocument, getReceivedNotificationPayment, getReceivedNotificationPaymentInfo, + getReceivedNotificationPaymentTppUrl, getReceivedNotificationPaymentUrl, } from '../redux/notification/actions'; import { resetState } from '../redux/notification/reducers'; +import { exchangeNotificationRetrievalId } from '../redux/sidemenu/actions'; import { RootState } from '../redux/store'; import { getConfiguration } from '../services/configuration.service'; import PFEventStrategyFactory from '../utility/MixpanelUtils/PFEventStrategyFactory'; @@ -90,6 +92,7 @@ const NotificationDetail: React.FC = () => { const currentRecipient = notification?.currentRecipient; const userPayments = useAppSelector((state: RootState) => state.notificationState.paymentsData); + const paymentTpp = useAppSelector((state: RootState) => state.generalInfoState.paymentTpp); const unfilteredDetailTableRows: Array<{ label: string; @@ -270,6 +273,26 @@ const NotificationDetail: React.FC = () => { } }; + const onPayTppClick = (noticeCode?: string, creditorTaxId?: string, retrievalId?: string) => { + if (noticeCode && creditorTaxId && retrievalId) { + PFEventStrategyFactory.triggerEvent(PFEventsType.SEND_START_PAYMENT); + dispatch( + getReceivedNotificationPaymentTppUrl({ + noticeCode, + creditorTaxId, + retrievalId, + }) + ) + .unwrap() + .then((res) => { + if (res.paymentUrl) { + window.location.assign(res.paymentUrl); + } + }) + .catch(() => undefined); + } + }; + const hasNotificationReceivedApiError = hasApiErrors( NOTIFICATION_ACTIONS.GET_RECEIVED_NOTIFICATION ); @@ -351,6 +374,20 @@ const NotificationDetail: React.FC = () => { return () => void dispatch(resetState()); }, []); + /* if retrievalId is in user token and payment info is not in redux, get payment info PN-13915 */ + useEffect(() => { + if (!checkIfUserHasPayments) { + return; + } + if (!currentUser.source?.retrievalId) { + return; + } + if (currentUser.source?.retrievalId === paymentTpp.retrievalId) { + return; + } + void dispatch(exchangeNotificationRetrievalId(currentUser.source.retrievalId)); + }, [currentUser, checkIfUserHasPayments]); + /* function which loads relevant information about donwtimes */ const fetchDowntimeEvents = useCallback((fromDate: string, toDate: string | undefined) => { const fetchParams: GetDowntimeHistoryParams = { @@ -511,10 +548,12 @@ const NotificationDetail: React.FC = () => { > { configurable: true, value: { href: '', assign: mockAssignFn }, }); + initLocalizationForTest(); }); afterEach(() => { @@ -919,4 +922,40 @@ describe('NotificationDetail Page', async () => { expect(alertRadd).toBeInTheDocument(); expect(alertRadd).toHaveTextContent('detail.timeline.radd.title'); }); + + it('should show pay tpp button after call check-tpp api with retrievalId in user token', async () => { + const mockRetrievalId = 'retrieval-id'; + mock.onGet(`/bff/v1/notifications/received/${notificationDTO.iun}`).reply(200, notificationDTO); + mock + .onGet(`/bff/v1/notifications/received/check-tpp?retrievalId=${mockRetrievalId}`) + .reply(200, { + originId: notificationDTO.iun, + retrievalId: mockRetrievalId, + paymentButton: 'MOCK BANK', + } as BffCheckTPPResponse); + + await act(async () => { + result = render(, { + preloadedState: { + userState: { + user: { + fiscal_number: notificationDTO.recipients[2].taxId, + source: { + channel: 'TPP', + details: 'mock-tpp-id', + retrievalId: mockRetrievalId, + }, + }, + }, + }, + }); + }); + + expect( + mock.history.get.find(({ url }) => url?.includes('bff/v1/notifications/received/check-tpp')) + ).toBeDefined(); + const tppPayButton = await waitFor(() => result.getByTestId('tpp-pay-button')); + expect(tppPayButton).toBeInTheDocument(); + expect(tppPayButton).toHaveTextContent('MOCK BANK'); + }); }); diff --git a/packages/pn-personafisica-webapp/src/redux/notification/__test__/reducers.test.ts b/packages/pn-personafisica-webapp/src/redux/notification/__test__/reducers.test.ts index 036da52e63..09a433d843 100644 --- a/packages/pn-personafisica-webapp/src/redux/notification/__test__/reducers.test.ts +++ b/packages/pn-personafisica-webapp/src/redux/notification/__test__/reducers.test.ts @@ -25,11 +25,9 @@ import { } from '../../../__mocks__/NotificationDetail.mock'; import { createMockedStore } from '../../../__test__/test-utils'; import { apiClient } from '../../../api/apiClients'; -import { BffCheckTPPResponse } from '../../../generated-client/notifications'; import { getDowntimeLegalFact } from '../../appStatus/actions'; import { store } from '../../store'; import { - exchangeNotificationRetrievalId, getDowntimeHistory, getReceivedNotification, getReceivedNotificationDocument, @@ -63,7 +61,6 @@ const initialState = { paymentsData: { pagoPaF24: [], f24Only: [], - tpp: {}, }, downtimeEvents: [], }; @@ -443,19 +440,4 @@ describe('Notification detail redux state tests', () => { expect(action.type).toBe('getDowntimeLegalFact/fulfilled'); expect(action.payload).toEqual(mockResponse); }); - - it('Should be able to set paymentsData tpp', async () => { - const mockRetrievalId = 'mocked-retrieval-id'; - const mockResponse: BffCheckTPPResponse = { - paymentButton: 'Hype', - retrievalId: mockRetrievalId, - originId: 'mocked-iun', - }; - mock - .onGet(`/bff/v1/notifications/received/check-tpp?retrievalId=${mockRetrievalId}`) - .reply(200, mockResponse); - const action = await store.dispatch(exchangeNotificationRetrievalId(mockRetrievalId)); - expect(action.type).toBe('exchangeNotificationRetrievalId/fulfilled'); - expect(action.payload).toEqual(mockResponse); - }); }); diff --git a/packages/pn-personafisica-webapp/src/redux/notification/actions.ts b/packages/pn-personafisica-webapp/src/redux/notification/actions.ts index cc05cd52b2..0db094f719 100644 --- a/packages/pn-personafisica-webapp/src/redux/notification/actions.ts +++ b/packages/pn-personafisica-webapp/src/redux/notification/actions.ts @@ -25,10 +25,9 @@ import { DowntimeApiFactory } from '../../generated-client/downtime-logs'; import { BffCheckAarRequest, BffCheckAarResponse, - BffCheckTPPResponse, NotificationReceivedApiFactory, } from '../../generated-client/notifications'; -import { PaymentsApiFactory } from '../../generated-client/payments'; +import { BffPaymentTppResponse, PaymentsApiFactory } from '../../generated-client/payments'; import { NotificationDetailForRecipient } from '../../models/NotificationDetail'; import { PFEventsType } from '../../models/PFEventsType'; import PFEventStrategyFactory from '../../utility/MixpanelUtils/PFEventStrategyFactory'; @@ -44,7 +43,6 @@ export enum NOTIFICATION_ACTIONS { GET_RECEIVED_NOTIFICATION_PAYMENT_URL = 'getReceivedNotificationPaymentUrl', GET_DOWNTIME_HISTORY = 'getNotificationDowntimeHistory', EXCHANGE_NOTIFICATION_QR_CODE = 'exchangeNotificationQrCode', - EXCHANGE_NOTIFICATION_RETRIEVAL_ID = 'exchangeNotificationRetrievalId', } export const getReceivedNotification = createAsyncThunk< @@ -282,16 +280,29 @@ export const exchangeNotificationQrCode = createAsyncThunk( - NOTIFICATION_ACTIONS.EXCHANGE_NOTIFICATION_RETRIEVAL_ID, - async (retrievalId: string, { rejectWithValue }) => { +export const getReceivedNotificationPaymentTppUrl = createAsyncThunk< + BffPaymentTppResponse, + { retrievalId: string; noticeCode: string; creditorTaxId: string } +>( + 'getReceivedNotificationPaymentTppUrl', + async ({ retrievalId, noticeCode, creditorTaxId }, { rejectWithValue }) => { try { - const notificationReceivedApiFactory = NotificationReceivedApiFactory( - undefined, - undefined, - apiClient + const paymentsApiFactory = PaymentsApiFactory(undefined, undefined, apiClient); + const iun = store.getState().notificationState.notification.iun; + setPaymentCache( + { + currentPayment: { + noticeCode, + creditorTaxId, + }, + }, + iun + ); + const response = await paymentsApiFactory.paymentsTppV1( + retrievalId, + noticeCode, + creditorTaxId ); - const response = await notificationReceivedApiFactory.checkTppV1(retrievalId); return response.data; } catch (e: any) { return rejectWithValue(parseError(e)); diff --git a/packages/pn-personafisica-webapp/src/redux/notification/reducers.ts b/packages/pn-personafisica-webapp/src/redux/notification/reducers.ts index e3ada9e6cb..d3b38794d8 100644 --- a/packages/pn-personafisica-webapp/src/redux/notification/reducers.ts +++ b/packages/pn-personafisica-webapp/src/redux/notification/reducers.ts @@ -10,6 +10,7 @@ import { PaymentDetails, PaymentInfoDetail, PaymentStatus, + PaymentsData, RecipientType, TimelineCategory, getF24Payments, @@ -18,10 +19,8 @@ import { } from '@pagopa-pn/pn-commons'; import { createSlice } from '@reduxjs/toolkit'; -import { BffCheckTPPResponse } from '../../generated-client/notifications'; import { NotificationDetailForRecipient } from '../../models/NotificationDetail'; import { - exchangeNotificationRetrievalId, getDowntimeHistory, getReceivedNotification, getReceivedNotificationPaymentInfo, @@ -52,8 +51,7 @@ const initialState = { paymentsData: { pagoPaF24: [] as Array, f24Only: [] as Array, - tpp: {} as BffCheckTPPResponse, - }, + } as PaymentsData, downtimeEvents: [] as Array, }; @@ -159,9 +157,6 @@ const notificationSlice = createSlice({ builder.addCase(getDowntimeHistory.fulfilled, (state, action) => { state.downtimeEvents = action.payload.result; }); - builder.addCase(exchangeNotificationRetrievalId.fulfilled, (state, action) => { - state.paymentsData.tpp = action.payload; - }); }, }); diff --git a/packages/pn-personafisica-webapp/src/redux/sidemenu/__test__/reducers.test.ts b/packages/pn-personafisica-webapp/src/redux/sidemenu/__test__/reducers.test.ts index f6b0d7a156..0940d54fe0 100644 --- a/packages/pn-personafisica-webapp/src/redux/sidemenu/__test__/reducers.test.ts +++ b/packages/pn-personafisica-webapp/src/redux/sidemenu/__test__/reducers.test.ts @@ -7,14 +7,16 @@ import { createMockedStore } from '../../../__test__/test-utils'; import { apiClient } from '../../../api/apiClients'; import { acceptMandate, rejectMandate } from '../../delegation/actions'; import { store } from '../../store'; -import { getDomicileInfo, getSidemenuInformation } from '../actions'; +import { exchangeNotificationRetrievalId, getDomicileInfo, getSidemenuInformation } from '../actions'; import { closeDomicileBanner } from '../reducers'; +import { BffCheckTPPResponse } from '../../../generated-client/notifications'; const initialState = { pendingDelegators: 0, delegators: [], digitalAddresses: [], domicileBannerOpened: true, + paymentTpp: {}, }; const pendingDelegators = mandatesByDelegate.filter((d) => d.status === 'pending'); @@ -121,4 +123,19 @@ describe('Sidemenu redux state tests', () => { expect(action.type).toBe('getDomicileInfo/fulfilled'); expect(action.payload).toEqual(digitalAddresses); }); + + it('Should be able to set tpp info from retrievalId', async () => { + const mockRetrievalId = 'mocked-retrieval-id'; + const mockResponse: BffCheckTPPResponse = { + paymentButton: 'Hype', + retrievalId: mockRetrievalId, + originId: 'mocked-iun', + }; + mock + .onGet(`/bff/v1/notifications/received/check-tpp?retrievalId=${mockRetrievalId}`) + .reply(200, mockResponse); + const action = await store.dispatch(exchangeNotificationRetrievalId(mockRetrievalId)); + expect(action.type).toBe('exchangeNotificationRetrievalId/fulfilled'); + expect(action.payload).toEqual(mockResponse); + }); }); diff --git a/packages/pn-personafisica-webapp/src/redux/sidemenu/actions.ts b/packages/pn-personafisica-webapp/src/redux/sidemenu/actions.ts index b754789d02..a89300da68 100644 --- a/packages/pn-personafisica-webapp/src/redux/sidemenu/actions.ts +++ b/packages/pn-personafisica-webapp/src/redux/sidemenu/actions.ts @@ -4,12 +4,17 @@ import { createAsyncThunk } from '@reduxjs/toolkit'; import { apiClient } from '../../api/apiClients'; import { AddressesApiFactory } from '../../generated-client/digital-addresses'; import { MandateApiFactory } from '../../generated-client/mandate'; +import { + BffCheckTPPResponse, + NotificationReceivedApiFactory, +} from '../../generated-client/notifications'; import { AddressType, DigitalAddress } from '../../models/contacts'; import { Delegator } from '../delegation/types'; export enum SIDEMENU_ACTIONS { GET_SIDEMENU_INFORMATION = 'getSidemenuInformation', GET_DOMICILE_INFO = 'getDomicileInfo', + EXCHANGE_NOTIFICATION_RETRIEVAL_ID = 'exchangeNotificationRetrievalId', } export const getSidemenuInformation = createAsyncThunk>( @@ -39,3 +44,24 @@ export const getDomicileInfo = createAsyncThunk>( } } ); + +/** + * TPP + */ + +export const exchangeNotificationRetrievalId = createAsyncThunk( + SIDEMENU_ACTIONS.EXCHANGE_NOTIFICATION_RETRIEVAL_ID, + async (retrievalId: string, { rejectWithValue }) => { + try { + const notificationReceivedApiFactory = NotificationReceivedApiFactory( + undefined, + undefined, + apiClient + ); + const result = await notificationReceivedApiFactory.checkTppV1(retrievalId); + return result.data; + } catch (e: any) { + return rejectWithValue(parseError(e)); + } + } +); diff --git a/packages/pn-personafisica-webapp/src/redux/sidemenu/reducers.ts b/packages/pn-personafisica-webapp/src/redux/sidemenu/reducers.ts index 6f16f77a7c..62af538bb5 100644 --- a/packages/pn-personafisica-webapp/src/redux/sidemenu/reducers.ts +++ b/packages/pn-personafisica-webapp/src/redux/sidemenu/reducers.ts @@ -1,11 +1,21 @@ +import { PaymentTpp } from '@pagopa-pn/pn-commons/src/models/NotificationDetail'; import { createSlice } from '@reduxjs/toolkit'; import { AddressType, ChannelType, DigitalAddress, IOAllowedValues } from '../../models/contacts'; import { removeAddress, updateAddressesList } from '../../utility/contacts.utility'; -import { createOrUpdateAddress, deleteAddress, disableIOAddress, enableIOAddress } from '../contact/actions'; +import { + createOrUpdateAddress, + deleteAddress, + disableIOAddress, + enableIOAddress, +} from '../contact/actions'; import { acceptMandate, rejectMandate } from '../delegation/actions'; import { Delegator } from '../delegation/types'; -import { getDomicileInfo, getSidemenuInformation } from './actions'; +import { + exchangeNotificationRetrievalId, + getDomicileInfo, + getSidemenuInformation, +} from './actions'; /* eslint-disable functional/immutable-data */ const generalInfoSlice = createSlice({ @@ -15,6 +25,7 @@ const generalInfoSlice = createSlice({ delegators: [] as Array, digitalAddresses: [] as Array, domicileBannerOpened: true, + paymentTpp: {} as PaymentTpp, }, reducers: { closeDomicileBanner: (state) => { @@ -84,6 +95,13 @@ const generalInfoSlice = createSlice({ state.pendingDelegators--; // so we also need to update pendingDelegators state } }); + builder.addCase(exchangeNotificationRetrievalId.fulfilled, (state, action) => { + state.paymentTpp = { + retrievalId: action.payload.retrievalId, + paymentButton: action.payload.paymentButton || '', + iun: action.payload.originId || '', + }; + }); }, }); From 5f2da713e0cce937388d107f25d7e0a358de9e81 Mon Sep 17 00:00:00 2001 From: Francesco Bianchi <105631409+fbianchicodermine@users.noreply.github.com> Date: Mon, 24 Feb 2025 16:43:04 +0100 Subject: [PATCH 3/3] feat(pn-13895): [PG] pass source AAR in exchangeToken (#1476) --- .../src/__mocks__/Auth.mock.ts | 14 +++- .../src/api/auth/Auth.api.ts | 65 +++++++++++-------- .../src/api/auth/__test__/Auth.api.test.ts | 25 +++++-- .../src/models/User.ts | 16 ++++- .../src/navigation/SessionGuard.tsx | 4 +- .../src/redux/auth/actions.ts | 6 +- .../src/redux/auth/reducers.ts | 6 ++ 7 files changed, 97 insertions(+), 39 deletions(-) diff --git a/packages/pn-personagiuridica-webapp/src/__mocks__/Auth.mock.ts b/packages/pn-personagiuridica-webapp/src/__mocks__/Auth.mock.ts index 0f35237333..62142564b5 100644 --- a/packages/pn-personagiuridica-webapp/src/__mocks__/Auth.mock.ts +++ b/packages/pn-personagiuridica-webapp/src/__mocks__/Auth.mock.ts @@ -2,14 +2,14 @@ import MockAdapter from 'axios-mock-adapter'; import { authClient } from '../api/apiClients'; import { AUTH_TOKEN_EXCHANGE } from '../api/auth/auth.routes'; -import { exchangeToken, logout } from '../redux/auth/actions'; import { PNRole, PartyRole, User } from '../models/User'; +import { exchangeToken, logout } from '../redux/auth/actions'; import { store } from '../redux/store'; export const mockLogin = async (body: User | string = userResponse): Promise => { const mock = new MockAdapter(authClient); mock.onPost(AUTH_TOKEN_EXCHANGE(), { authorizationToken: 'mocked-token' }).reply(200, body); - const action = store.dispatch(exchangeToken('mocked-token')); + const action = store.dispatch(exchangeToken({ spidToken: 'mocked-token' })); mock.reset(); mock.restore(); return action; @@ -23,7 +23,7 @@ export const mockAuthentication = () => { beforeAll(() => { mock = new MockAdapter(authClient); mock.onPost(AUTH_TOKEN_EXCHANGE()).reply(200, userResponse); - store.dispatch(exchangeToken('mocked-token')); + store.dispatch(exchangeToken({ spidToken: 'mocked-token' })); }); afterAll(() => { @@ -61,3 +61,11 @@ export const userResponse: User = { }, hasGroup: false, }; + +export const userResponseWithSource: User = { + ...userResponse, + source: { + channel: 'WEB', + details: 'QR_CODE', + }, +}; diff --git a/packages/pn-personagiuridica-webapp/src/api/auth/Auth.api.ts b/packages/pn-personagiuridica-webapp/src/api/auth/Auth.api.ts index f470c3d4ce..268ed24f27 100644 --- a/packages/pn-personagiuridica-webapp/src/api/auth/Auth.api.ts +++ b/packages/pn-personagiuridica-webapp/src/api/auth/Auth.api.ts @@ -1,31 +1,44 @@ -import { User } from '../../models/User'; +import { TokenExchangeBody, User } from '../../models/User'; import { authClient } from '../apiClients'; import { AUTH_TOKEN_EXCHANGE } from './auth.routes'; export const AuthApi = { - exchangeToken: (spidToken: string): Promise => - authClient - .post(AUTH_TOKEN_EXCHANGE(), { authorizationToken: spidToken }) - .then((response) => ({ - sessionToken: response.data.sessionToken, - email: response.data.email, - name: response.data.name, - family_name: response.data.family_name, - uid: response.data.uid, - fiscal_number: response.data.fiscal_number, - from_aa: response.data.from_aa, - aud: response.data.aud, - level: response.data.level, - iat: response.data.iat, - exp: response.data.exp, - iss: response.data.iss, - jti: response.data.jti, - organization: response.data.organization, - desired_exp: response.data.desired_exp, - hasGroup: Boolean( - response.data.organization && - response.data.organization.groups && - response.data.organization.groups.length > 0 - ), - })), + exchangeToken: async (spidToken: string, aar?: string): Promise => { + const body: TokenExchangeBody = { authorizationToken: spidToken }; + if (aar) { + // eslint-disable-next-line functional/immutable-data + body.source = { + type: 'QR', + id: aar, + }; + } + const response = await authClient.post(AUTH_TOKEN_EXCHANGE(), body); + const user: User = { + sessionToken: response.data.sessionToken, + email: response.data.email, + name: response.data.name, + family_name: response.data.family_name, + uid: response.data.uid, + fiscal_number: response.data.fiscal_number, + from_aa: response.data.from_aa, + aud: response.data.aud, + level: response.data.level, + iat: response.data.iat, + exp: response.data.exp, + iss: response.data.iss, + jti: response.data.jti, + organization: response.data.organization, + desired_exp: response.data.desired_exp, + hasGroup: Boolean( + response.data.organization && + response.data.organization.groups && + response.data.organization.groups.length > 0 + ), + }; + if (aar && response.data.source) { + /* eslint-disable-next-line functional/immutable-data */ + user.source = response.data.source; + } + return user; + }, }; diff --git a/packages/pn-personagiuridica-webapp/src/api/auth/__test__/Auth.api.test.ts b/packages/pn-personagiuridica-webapp/src/api/auth/__test__/Auth.api.test.ts index 034a64614a..df829a2e3c 100644 --- a/packages/pn-personagiuridica-webapp/src/api/auth/__test__/Auth.api.test.ts +++ b/packages/pn-personagiuridica-webapp/src/api/auth/__test__/Auth.api.test.ts @@ -1,18 +1,35 @@ import MockAdapter from 'axios-mock-adapter'; -import { userResponse } from '../../../__mocks__/Auth.mock'; +import { userResponse, userResponseWithSource } from '../../../__mocks__/Auth.mock'; import { authClient } from '../../apiClients'; import { AuthApi } from '../Auth.api'; import { AUTH_TOKEN_EXCHANGE } from '../auth.routes'; describe('Auth api tests', () => { + let mock: MockAdapter; + + beforeAll(() => { + mock = new MockAdapter(authClient); + }); + + afterEach(() => { + mock.reset(); + }); + it('exchangeToken', async () => { const token = 'mocked-token'; - const mock = new MockAdapter(authClient); mock.onPost(AUTH_TOKEN_EXCHANGE(), { authorizationToken: token }).reply(200, userResponse); const res = await AuthApi.exchangeToken(token); expect(res).toStrictEqual(userResponse); - mock.reset(); - mock.restore(); + }); + + it('exchangeToken with aar', async () => { + const token = 'mocked-token'; + const aar = 'mock-aar'; + mock + .onPost(AUTH_TOKEN_EXCHANGE(), { authorizationToken: token, source: { type: 'QR', id: aar } }) + .reply(200, userResponseWithSource); + const res = await AuthApi.exchangeToken(token, aar); + expect(res).toStrictEqual(userResponseWithSource); }); }); diff --git a/packages/pn-personagiuridica-webapp/src/models/User.ts b/packages/pn-personagiuridica-webapp/src/models/User.ts index 7417d50234..1670b596ee 100644 --- a/packages/pn-personagiuridica-webapp/src/models/User.ts +++ b/packages/pn-personagiuridica-webapp/src/models/User.ts @@ -36,4 +36,18 @@ export interface User extends BasicUser { organization: Organization; desired_exp: number; hasGroup?: boolean; -} \ No newline at end of file + source?: UserSource; +} + +export interface UserSource { + channel: 'WEB'; + details: string; // 'QR_CODE'; +} + +export interface TokenExchangeBody { + authorizationToken: string; + source?: { + type: 'QR'; + id: string; + }; +} diff --git a/packages/pn-personagiuridica-webapp/src/navigation/SessionGuard.tsx b/packages/pn-personagiuridica-webapp/src/navigation/SessionGuard.tsx index e1b210b9ce..8a4e3a9d04 100644 --- a/packages/pn-personagiuridica-webapp/src/navigation/SessionGuard.tsx +++ b/packages/pn-personagiuridica-webapp/src/navigation/SessionGuard.tsx @@ -44,7 +44,6 @@ const manageUnforbiddenError = (e: any) => { return true; }; - // Perché ci sono due componenti. // Il codice in SessionGuard implementa i steps necessari per determinare se c'è sessione, se è utente abilitato, se è sessione anonima, ecc.. // D'altra parte, SessionGuardRender implementa la logica di cosa si deve renderizzare, @@ -174,8 +173,9 @@ const SessionGuard = () => { // ---------------------- const spidToken = getTokenParam(); if (spidToken) { + const aar = localStorage.getItem(AppRouteParams.AAR) || undefined; AppResponsePublisher.error.subscribe('exchangeToken', manageUnforbiddenError); - await dispatch(exchangeToken(spidToken)); + await dispatch(exchangeToken({ spidToken, aar })); } }; void performStep(INITIALIZATION_STEPS.USER_DETERMINATION, doUserDetermination); diff --git a/packages/pn-personagiuridica-webapp/src/redux/auth/actions.ts b/packages/pn-personagiuridica-webapp/src/redux/auth/actions.ts index 71c22edc41..d53e593267 100644 --- a/packages/pn-personagiuridica-webapp/src/redux/auth/actions.ts +++ b/packages/pn-personagiuridica-webapp/src/redux/auth/actions.ts @@ -18,11 +18,11 @@ export enum AUTH_ACTIONS { * Exchange token action between selfcare and pn. * If token is valid, user info are set in sessionStorage */ -export const exchangeToken = createAsyncThunk( +export const exchangeToken = createAsyncThunk( 'exchangeToken', - async (spidToken, { rejectWithValue }) => { + async ({ spidToken, aar }, { rejectWithValue }) => { try { - return await AuthApi.exchangeToken(spidToken); + return await AuthApi.exchangeToken(spidToken, aar); } catch (e: any) { return rejectWithValue(parseError(e)); } diff --git a/packages/pn-personagiuridica-webapp/src/redux/auth/reducers.ts b/packages/pn-personagiuridica-webapp/src/redux/auth/reducers.ts index eb61d2efee..5b7598f778 100644 --- a/packages/pn-personagiuridica-webapp/src/redux/auth/reducers.ts +++ b/packages/pn-personagiuridica-webapp/src/redux/auth/reducers.ts @@ -39,6 +39,12 @@ const userDataMatcher = yup organization: organizationMatcher, desired_exp: yup.number(), hasGroup: yup.boolean(), + source: yup + .object({ + channel: yup.string().oneOf(['WEB']), + details: yup.string(), + }) + .optional(), }) .noUnknown(true);