From 15dd29c11575e24a81ec7c2c104988aa0b1b7360 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Mon, 17 Feb 2025 17:52:49 +0100 Subject: [PATCH 01/13] WiP FIMS start flow --- .../Markdown/handlers/__test__/link.test.ts | 60 +-------- ts/components/ui/Markdown/handlers/link.ts | 15 +-- ts/features/fims/common/analytics/index.ts | 2 +- ts/features/fims/common/hooks/index.tsx | 122 +++++++++++++++++- .../screens/FimsFlowHandlerScreen.tsx | 54 +++++++- .../singleSignOn/utils/__test__/index.test.ts | 61 +++++++++ ts/features/fims/singleSignOn/utils/index.ts | 61 +++++++++ .../screens/IdPayInitiativeDetailsScreen.tsx | 18 ++- .../ItwPresentationDetailsFooter.tsx | 28 +--- .../MessageDetailsStickyFooter.tsx | 23 ++-- .../components/MessageDetail/detailsUtils.ts | 21 --- .../messages/saga/handleLoadMessageData.ts | 6 +- ts/features/messages/utils/ctas.ts | 29 +---- .../details/screens/ServiceDetailsScreen.tsx | 25 +--- .../reducers/backendStatus/remoteConfig.ts | 22 ++++ ts/utils/__tests__/internalLink.test.ts | 2 +- ts/utils/internalLink.ts | 2 +- ts/utils/navigation.ts | 3 - 18 files changed, 355 insertions(+), 199 deletions(-) create mode 100644 ts/features/fims/singleSignOn/utils/__test__/index.test.ts diff --git a/ts/components/ui/Markdown/handlers/__test__/link.test.ts b/ts/components/ui/Markdown/handlers/__test__/link.test.ts index b91f9921ca3..2d52a26072f 100644 --- a/ts/components/ui/Markdown/handlers/__test__/link.test.ts +++ b/ts/components/ui/Markdown/handlers/__test__/link.test.ts @@ -2,9 +2,7 @@ import * as E from "fp-ts/lib/Either"; import { deriveCustomHandledLink, isHttpsLink, - isIoFIMSLink, - isIoInternalLink, - removeFIMSPrefixFromUrl + isIoInternalLink } from "../link"; const loadingCases: ReadonlyArray< @@ -73,19 +71,6 @@ const loadingCases: ReadonlyArray< ] ]; -const fimsCases: ReadonlyArray< - [input: string, expectedResult: ReturnType] -> = [ - [ - "iosso://https://italia.io/main/messages?messageId=4&serviceId=5", - "https://italia.io/main/messages?messageId=4&serviceId=5" - ], - [ - "iOsSo://https://italia.io/main/messages?messageId=4&serviceId=5", - "https://italia.io/main/messages?messageId=4&serviceId=5" - ] -]; - describe("deriveCustomHandledLink", () => { test.each(loadingCases)( "given %p as argument, returns %p", @@ -96,16 +81,6 @@ describe("deriveCustomHandledLink", () => { ); }); -describe("removeFIMSPrefixFromUrl", () => { - test.each(fimsCases)( - "given %p as argument, returns %p", - (firstArg, expectedResult) => { - const result = removeFIMSPrefixFromUrl(firstArg); - expect(result).toEqual(expectedResult); - } - ); -}); - describe("isHttpsLink", () => { ["https://", "hTtPs://", "HTTPS://"].forEach(protocol => { it(`should return true for '${protocol}'`, () => { @@ -139,39 +114,6 @@ describe("isHttpsLink", () => { }); }); -describe("isIoFIMSLink", () => { - ["iosso://", "iOsSo://", "IOSSO://"].forEach(protocol => { - it(`should return true for '${protocol}'`, () => { - const isIOFIMSLink = isIoFIMSLink(`${protocol}whatever`); - expect(isIOFIMSLink).toBe(true); - }); - }); - [ - "iosso:/", - "iosso:", - "iosso", - "https://", - "http://", - "ioit://", - "iohandledlink://", - "clipboard://", - "clipboard:", - "sms://", - "sms:", - "tel://", - "tel:", - "mailto://", - "mailto:", - "copy://", - "copy:" - ].forEach(protocol => { - it(`should return false for '${protocol}'`, () => { - const isIOFIMSLink = isIoFIMSLink(`${protocol}whatever`); - expect(isIOFIMSLink).toBe(false); - }); - }); -}); - describe("isIoInternalLink", () => { ["ioit://", "iOiT://", "IOIT://"].forEach(protocol => { it(`should return true for '${protocol}'`, () => { diff --git a/ts/components/ui/Markdown/handlers/link.ts b/ts/components/ui/Markdown/handlers/link.ts index dc510ca84aa..2a4d1ce0400 100644 --- a/ts/components/ui/Markdown/handlers/link.ts +++ b/ts/components/ui/Markdown/handlers/link.ts @@ -2,28 +2,15 @@ import { IOToast } from "@pagopa/io-app-design-system"; import * as E from "fp-ts/lib/Either"; import * as t from "io-ts"; import I18n from "../../../../i18n"; -import { - IO_FIMS_LINK_PREFIX, - IO_FIMS_LINK_PROTOCOL, - IO_INTERNAL_LINK_PREFIX -} from "../../../../utils/navigation"; +import { IO_INTERNAL_LINK_PREFIX } from "../../../../utils/navigation"; import { openWebUrl } from "../../../../utils/url"; export const isIoInternalLink = (href: string): boolean => href.toLowerCase().startsWith(IO_INTERNAL_LINK_PREFIX); -export const isIoFIMSLink = (href: string): boolean => - href.toLowerCase().startsWith(IO_FIMS_LINK_PREFIX); - export const isHttpsLink = (href: string): boolean => href.toLowerCase().startsWith("https://"); -export const removeFIMSPrefixFromUrl = (fimsUrlWithProtocol: string) => { - // eslint-disable-next-line no-useless-escape - const regexp = new RegExp(`^${IO_FIMS_LINK_PROTOCOL}\/\/`, "i"); - return fimsUrlWithProtocol.replace(regexp, ""); -}; - /** * a dedicated codec for CustomHandledLink * ex: iohandledlink://tel:1234567 -> {url: tel:1234567, type: tel, value:1234567} diff --git a/ts/features/fims/common/analytics/index.ts b/ts/features/fims/common/analytics/index.ts index 08e7119c41c..fc1f98a901d 100644 --- a/ts/features/fims/common/analytics/index.ts +++ b/ts/features/fims/common/analytics/index.ts @@ -12,7 +12,7 @@ export const trackAuthenticationStart = ( organizationName: string | undefined, organizationFiscalCode: string | undefined, ctaLabel: string, - source: "message_detail" | "service_detail" | "credential_detail" + source: string ) => { const eventName = `FIMS_START`; const props = buildEventProperties("UX", "action", { diff --git a/ts/features/fims/common/hooks/index.tsx b/ts/features/fims/common/hooks/index.tsx index 288138d6e7b..1ab56d293d8 100644 --- a/ts/features/fims/common/hooks/index.tsx +++ b/ts/features/fims/common/hooks/index.tsx @@ -1,9 +1,21 @@ -import { useEffect } from "react"; +import { useCallback, useEffect, useMemo } from "react"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import { useIODispatch, useIOSelector } from "../../../../store/hooks"; -import { serviceByIdPotSelector } from "../../../services/details/store/reducers"; +import { + useIODispatch, + useIOSelector, + useIOStore +} from "../../../../store/hooks"; +import { + serviceByIdPotSelector, + serviceByIdSelector +} from "../../../services/details/store/reducers"; import { loadServiceDetail } from "../../../services/details/store/actions/details"; import { isStrictNone } from "../../../../utils/pot"; +import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { FIMS_ROUTES } from "../navigation"; +import { removeFIMSPrefixFromUrl } from "../../singleSignOn/utils"; +import { isTestEnv } from "../../../../utils/environment"; +import { fimsServiceConfiguration } from "../../../../store/reducers/backendStatus/remoteConfig"; export const useAutoFetchingServiceByIdPot = (serviceId: ServiceId) => { const dispatch = useIODispatch(); @@ -23,3 +35,107 @@ export const useAutoFetchingServiceByIdPot = (serviceId: ServiceId) => { return serviceData; }; + +export const useFIMSFromServiceId = (serviceId: ServiceId) => { + const serviceData = useServiceDataFromServiceId(serviceId); + const startFIMSAuthenticationFlow = useFIMSFromServiceData(serviceData); + return useMemo( + () => ({ + serviceData, + startFIMSAuthenticationFlow + }), + [serviceData, startFIMSAuthenticationFlow] + ); +}; + +export const useFIMSRemoteServiceConfiguration = (configurationId: string) => { + const serviceData = useServiceDataFromConfigurationId(configurationId); + const startFIMSAuthenticationFlow = useFIMSFromServiceData(serviceData); + return useMemo( + () => ({ + serviceData, + startFIMSAuthenticationFlow + }), + [serviceData, startFIMSAuthenticationFlow] + ); +}; + +type FIMSServiceData = { + organizationFiscalCode?: string; + organizationName?: string; + serviceId: ServiceId; + serviceName?: string; +}; + +const useFIMSFromServiceData = (serviceData: FIMSServiceData | undefined) => { + const navigation = useIONavigation(); + + const source = navigation.getState().key; + + return useCallback( + (label: string, url: string) => { + if (serviceData == null) { + return; + } + const sanitizedUrl = removeFIMSPrefixFromUrl(url); + navigation.navigate(FIMS_ROUTES.MAIN, { + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: sanitizedUrl, + ctaText: label, + organizationFiscalCode: serviceData.organizationFiscalCode, + organizationName: serviceData.organizationName, + serviceId: serviceData.serviceId, + serviceName: serviceData.serviceName, + source + } + }); + }, + [navigation, serviceData, source] + ); +}; + +const useServiceDataFromServiceId = ( + serviceId: ServiceId +): FIMSServiceData | undefined => { + const store = useIOStore(); + return useMemo(() => { + const service = serviceByIdSelector(store.getState(), serviceId); + return service != null + ? { + organizationFiscalCode: service.organization_fiscal_code, + organizationName: service.organization_name, + serviceId: service.service_id, + serviceName: service.service_name + } + : undefined; + }, [serviceId, store]); +}; + +const useServiceDataFromConfigurationId = ( + configurationId: string +): FIMSServiceData | undefined => { + const store = useIOStore(); + return useMemo(() => { + const serviceConfiguration = fimsServiceConfiguration( + store.getState(), + configurationId + ); + return serviceConfiguration != null + ? { + organizationFiscalCode: serviceConfiguration.organization_fiscal_code, + organizationName: serviceConfiguration.organization_name, + serviceId: serviceConfiguration.service_id as ServiceId, + serviceName: serviceConfiguration.service_name + } + : undefined; + }, [configurationId, store]); +}; + +export const testable = isTestEnv + ? { + useFIMSFromServiceData, + useServiceDataFromConfigurationId, + useServiceDataFromServiceId + } + : undefined; diff --git a/ts/features/fims/singleSignOn/screens/FimsFlowHandlerScreen.tsx b/ts/features/fims/singleSignOn/screens/FimsFlowHandlerScreen.tsx index 8683cd570c2..440b96d5471 100644 --- a/ts/features/fims/singleSignOn/screens/FimsFlowHandlerScreen.tsx +++ b/ts/features/fims/singleSignOn/screens/FimsFlowHandlerScreen.tsx @@ -11,7 +11,10 @@ import I18n from "../../../../i18n"; import { IOStackNavigationRouteProps } from "../../../../navigation/params/AppParamsList"; import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { fimsRequiresAppUpdateSelector } from "../../../../store/reducers/backendStatus/remoteConfig"; -import { trackAuthenticationError } from "../../common/analytics"; +import { + trackAuthenticationError, + trackAuthenticationStart +} from "../../common/analytics"; import { FimsUpdateAppAlert } from "../../common/components/FimsUpdateAppAlert"; import { FimsParamsList } from "../../common/navigation"; import { FimsSSOFullScreenError } from "../components/FimsFullScreenErrors"; @@ -25,10 +28,29 @@ import { fimsAuthenticationFailedSelector, fimsLoadingStateSelector } from "../store/selectors"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; export type FimsFlowHandlerScreenRouteParams = { + /* The label on the button that started the FIMS flow */ ctaText: string; + /* The Relying Party's url that starts the FIMS flow, + * without the iosso:// protocol prefix */ ctaUrl: string; + /* A Relying Party is always associated with a service. + * This is the fiscal code of the service organization */ + organizationFiscalCode: string | undefined; + /* A Relying Party is always associated with a service. + * This is the name of the service organization */ + organizationName: string | undefined; + /* A Relying Party is always associated with a service. + * This is service id */ + serviceId: ServiceId; + /* A Relying Party is always associated with a service. + * This is service name */ + serviceName: string | undefined; + /* This is the entry point from which the FIMS's flow + * has been starded (e.g., the screen's route name) */ + source: string; }; type FimsFlowHandlerScreenRouteProps = IOStackNavigationRouteProps< @@ -39,7 +61,15 @@ type FimsFlowHandlerScreenRouteProps = IOStackNavigationRouteProps< export const FimsFlowHandlerScreen = ( props: FimsFlowHandlerScreenRouteProps ) => { - const { ctaText, ctaUrl } = props.route.params; + const { + ctaText, + ctaUrl, + organizationFiscalCode, + organizationName, + serviceId, + serviceName, + source + } = props.route.params; const dispatch = useIODispatch(); const requiresAppUpdate = useIOSelector(fimsRequiresAppUpdateSelector); @@ -73,6 +103,14 @@ export const FimsFlowHandlerScreen = ( useEffect(() => { if (ctaUrl && !requiresAppUpdate) { + trackAuthenticationStart( + serviceId, + serviceName, + organizationName, + organizationFiscalCode, + ctaText, + source + ); dispatch(fimsGetConsentsListAction.request({ ctaText, ctaUrl })); } else if (requiresAppUpdate) { trackAuthenticationError( @@ -83,7 +121,17 @@ export const FimsFlowHandlerScreen = ( "update_required" ); } - }, [ctaText, ctaUrl, dispatch, requiresAppUpdate]); + }, [ + ctaText, + ctaUrl, + dispatch, + organizationFiscalCode, + organizationName, + requiresAppUpdate, + serviceId, + serviceName, + source + ]); if (requiresAppUpdate) { return ; diff --git a/ts/features/fims/singleSignOn/utils/__test__/index.test.ts b/ts/features/fims/singleSignOn/utils/__test__/index.test.ts new file mode 100644 index 00000000000..d97b277b201 --- /dev/null +++ b/ts/features/fims/singleSignOn/utils/__test__/index.test.ts @@ -0,0 +1,61 @@ +import { isFIMSLink, removeFIMSPrefixFromUrl } from ".."; + +describe("index", () => { + describe("removeFIMSPrefixFromUrl", () => { + const fimsCases: ReadonlyArray< + [ + input: string, + expectedResult: ReturnType + ] + > = [ + [ + "iosso://https://italia.io/main/messages?messageId=4&serviceId=5", + "https://italia.io/main/messages?messageId=4&serviceId=5" + ], + [ + "iOsSo://https://italia.io/main/messages?messageId=4&serviceId=5", + "https://italia.io/main/messages?messageId=4&serviceId=5" + ] + ]; + test.each(fimsCases)( + "given %p as argument, returns %p", + (firstArg, expectedResult) => { + const result = removeFIMSPrefixFromUrl(firstArg); + expect(result).toEqual(expectedResult); + } + ); + }); +}); + +describe("isIoFIMSLink", () => { + ["iosso://", "iOsSo://", "IOSSO://"].forEach(protocol => { + it(`should return true for '${protocol}'`, () => { + const isIOFIMSLink = isFIMSLink(`${protocol}whatever`); + expect(isIOFIMSLink).toBe(true); + }); + }); + [ + "iosso:/", + "iosso:", + "iosso", + "https://", + "http://", + "ioit://", + "iohandledlink://", + "clipboard://", + "clipboard:", + "sms://", + "sms:", + "tel://", + "tel:", + "mailto://", + "mailto:", + "copy://", + "copy:" + ].forEach(protocol => { + it(`should return false for '${protocol}'`, () => { + const isIOFIMSLink = isFIMSLink(`${protocol}whatever`); + expect(isIOFIMSLink).toBe(false); + }); + }); +}); diff --git a/ts/features/fims/singleSignOn/utils/index.ts b/ts/features/fims/singleSignOn/utils/index.ts index 545d3216cc2..bd98cb89f6c 100644 --- a/ts/features/fims/singleSignOn/utils/index.ts +++ b/ts/features/fims/singleSignOn/utils/index.ts @@ -3,6 +3,58 @@ import { ActionType } from "typesafe-actions"; import { FimsFlowStateTags, FimsSSOState } from "../store/reducers"; import { startApplicationInitialization } from "../../../../store/actions/application"; import { isStrictSome } from "../../../../utils/pot"; +import { FIMS_ROUTES } from "../../common/navigation"; +import NavigationService from "../../../../navigation/NavigationService"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import { FimsServiceConfiguration_config } from "../../../../../definitions/content/FimsServiceConfiguration"; + +export const IO_FIMS_LINK_PROTOCOL = "iosso:"; +export const IO_FIMS_LINK_PREFIX = IO_FIMS_LINK_PROTOCOL + "//"; + +export const startAuthenticationFlow = ( + label: string, + organizationFiscalCode: string | undefined, + organizationName: string | undefined, + serviceId: ServiceId, + serviceName: string | undefined, + source: string, + url: string +) => { + const sanitizedUrl = removeFIMSPrefixFromUrl(url); + NavigationService.navigate(FIMS_ROUTES.MAIN, { + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: sanitizedUrl, + ctaText: label, + organizationFiscalCode, + organizationName, + serviceId, + serviceName, + source + } + }); +}; + +export const startAuthenticationFlowWithServiceConfiguration = ( + label: string, + serviceConfiguration: FimsServiceConfiguration_config, + source: string, + url: string +) => { + const sanitizedUrl = removeFIMSPrefixFromUrl(url); + NavigationService.navigate(FIMS_ROUTES.MAIN, { + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: sanitizedUrl, + ctaText: label, + organizationFiscalCode: serviceConfiguration.organization_fiscal_code, + organizationName: serviceConfiguration.organization_name, + serviceId: serviceConfiguration.service_id as ServiceId, + serviceName: serviceConfiguration.service_name, + source + } + }); +}; export const foldFimsFlowState = ( flowState: FimsFlowStateTags, @@ -64,3 +116,12 @@ export const shouldRestartFimsAuthAfterFastLoginFailure = ( } return false; }; + +export const removeFIMSPrefixFromUrl = (fimsUrlWithProtocol: string) => { + // eslint-disable-next-line no-useless-escape + const regexp = new RegExp(`^${IO_FIMS_LINK_PROTOCOL}\/\/`, "i"); + return fimsUrlWithProtocol.replace(regexp, ""); +}; + +export const isFIMSLink = (href: string): boolean => + href.toLowerCase().startsWith(IO_FIMS_LINK_PREFIX); diff --git a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx index c33b598f6fe..b18ebba328b 100644 --- a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx +++ b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx @@ -52,9 +52,7 @@ import { initiativeNeedsConfigurationSelector } from "../store"; import { idpayInitiativeGet, idpayTimelinePageGet } from "../store/actions"; -import NavigationService from "../../../../navigation/NavigationService"; -import { FIMS_ROUTES } from "../../../fims/common/navigation"; -import { removeFIMSPrefixFromUrl } from "../../../../components/ui/Markdown/handlers/link"; +import { useFIMSRemoteServiceConfiguration } from "../../../fims/common/hooks"; export type IdPayInitiativeDetailsScreenParams = { initiativeId: string; @@ -87,18 +85,18 @@ const IdPayInitiativeDetailsScreen = () => { }); }; + const { startFIMSAuthenticationFlow } = useFIMSRemoteServiceConfiguration( + "idPayGuidoniaSummerCamp" + ); const onAddExpense = () => { const addExpenseFimsUrl = pot.toUndefined(initiativeDataPot)?.webViewUrl; if (!addExpenseFimsUrl) { return; } - NavigationService.navigate(FIMS_ROUTES.MAIN, { - screen: FIMS_ROUTES.CONSENTS, - params: { - ctaText: I18n.t("idpay.initiative.discountDetails.addExpenseButton"), - ctaUrl: removeFIMSPrefixFromUrl(addExpenseFimsUrl) - } - }); + startFIMSAuthenticationFlow( + I18n.t("idpay.initiative.discountDetails.addExpenseButton"), + addExpenseFimsUrl + ); }; const navigateToConfiguration = () => { diff --git a/ts/features/itwallet/presentation/details/components/ItwPresentationDetailsFooter.tsx b/ts/features/itwallet/presentation/details/components/ItwPresentationDetailsFooter.tsx index e3cdc4a6517..33248878a73 100644 --- a/ts/features/itwallet/presentation/details/components/ItwPresentationDetailsFooter.tsx +++ b/ts/features/itwallet/presentation/details/components/ItwPresentationDetailsFooter.tsx @@ -3,14 +3,12 @@ import { memo, ReactNode, useMemo } from "react"; import { Alert, View } from "react-native"; import { useStartSupportRequest } from "../../../../../hooks/useStartSupportRequest.ts"; import I18n from "../../../../../i18n.ts"; -import NavigationService from "../../../../../navigation/NavigationService.ts"; import { useIONavigation } from "../../../../../navigation/params/AppParamsList.ts"; import { useIODispatch, useIOSelector, useIOStore } from "../../../../../store/hooks.ts"; -import { FIMS_ROUTES } from "../../../../fims/common/navigation"; import { CREDENTIALS_MAP, trackCredentialDeleteProperties, @@ -20,8 +18,7 @@ import { import { itwIPatenteCtaConfigSelector } from "../../../common/store/selectors/remoteConfig.ts"; import { StoredCredential } from "../../../common/utils/itwTypesUtils.ts"; import { itwCredentialsRemove } from "../../../credentials/store/actions"; -import { trackAuthenticationStart } from "../../../../fims/common/analytics"; -import { ServiceId } from "../../../../../../definitions/backend/ServiceId.ts"; +import { useFIMSRemoteServiceConfiguration } from "../../../../fims/common/hooks/index.tsx"; type ItwPresentationDetailFooterProps = { credential: StoredCredential; @@ -133,6 +130,8 @@ const getCredentialActions = (credentialType: string): ReactNode => * Renders the IPatente service action item */ const IPatenteListItemAction = () => { + const { startFIMSAuthenticationFlow } = + useFIMSRemoteServiceConfiguration("iPatente"); const ctaConfig = useIOSelector(itwIPatenteCtaConfigSelector); if (!ctaConfig?.visibility) { @@ -143,32 +142,13 @@ const IPatenteListItemAction = () => { "features.itWallet.presentation.credentialDetails.actions.openIPatente" ); - const trackIPatenteAuthenticationStart = (label: string) => - trackAuthenticationStart( - ctaConfig.service_id as ServiceId, - ctaConfig.service_name, - ctaConfig.service_organization_name, - ctaConfig.service_organization_fiscal_code, - label, - "credential_detail" - ); - return ( { - trackIPatenteAuthenticationStart(label); - NavigationService.navigate(FIMS_ROUTES.MAIN, { - screen: FIMS_ROUTES.CONSENTS, - params: { - ctaText: label, - ctaUrl: ctaConfig.url - } - }); - }} + onPress={() => startFIMSAuthenticationFlow(label, ctaConfig.url)} /> ); }; diff --git a/ts/features/messages/components/MessageDetail/MessageDetailsStickyFooter.tsx b/ts/features/messages/components/MessageDetail/MessageDetailsStickyFooter.tsx index c13119cdc06..ecac1bc0a27 100644 --- a/ts/features/messages/components/MessageDetail/MessageDetailsStickyFooter.tsx +++ b/ts/features/messages/components/MessageDetail/MessageDetailsStickyFooter.tsx @@ -18,14 +18,12 @@ import { paymentsButtonStateSelector } from "../../store/reducers/payments"; import { trackPNOptInMessageAccepted } from "../../../pn/analytics"; -import { CTAActionType, handleCtaAction } from "../../utils/ctas"; +import { handleCtaAction } from "../../utils/ctas"; import { CTA, CTAS } from "../../types/MessageCTA"; import { ServiceId } from "../../../../../definitions/backend/ServiceId"; +import { useFIMSFromServiceId } from "../../../fims/common/hooks"; import { MessageDetailsPaymentButton } from "./MessageDetailsPaymentButton"; -import { - computeAndTrackCTAPressAnalytics, - computeAndTrackFIMSAuthenticationStart -} from "./detailsUtils"; +import { computeAndTrackCTAPressAnalytics } from "./detailsUtils"; const styles = StyleSheet.create({ container: { @@ -287,14 +285,7 @@ export const MessageDetailsStickyFooter = ({ canNavigateToPaymentFromMessageSelector(state) ); - const onCTAPreActionCallback = useCallback( - (cta: CTA) => (type: CTAActionType) => { - const state = store.getState(); - computeAndTrackFIMSAuthenticationStart(type, cta.text, serviceId, state); - }, - [serviceId, store] - ); - + const { startFIMSAuthenticationFlow } = useFIMSFromServiceId(serviceId); const linkTo = useLinkTo(); const onCTAPressedCallback = useCallback( (isFirstCTA: boolean, cta: CTA, isPNOptInMessage: boolean) => { @@ -303,9 +294,11 @@ export const MessageDetailsStickyFooter = ({ if (isPNOptInMessage) { trackPNOptInMessageAccepted(); } - handleCtaAction(cta, linkTo, onCTAPreActionCallback(cta)); + handleCtaAction(cta, linkTo, (label, url) => + startFIMSAuthenticationFlow(label, url) + ); }, - [linkTo, onCTAPreActionCallback, serviceId, store] + [linkTo, serviceId, startFIMSAuthenticationFlow, store] ); const footerData = computeFooterData(paymentData, paymentButtonStatus, ctas); diff --git a/ts/features/messages/components/MessageDetail/detailsUtils.ts b/ts/features/messages/components/MessageDetail/detailsUtils.ts index ed75babbe41..c6404cc0866 100644 --- a/ts/features/messages/components/MessageDetail/detailsUtils.ts +++ b/ts/features/messages/components/MessageDetail/detailsUtils.ts @@ -1,10 +1,8 @@ import { ServiceId } from "../../../../../definitions/backend/ServiceId"; import { GlobalState } from "../../../../store/reducers/types"; -import { trackAuthenticationStart } from "../../../fims/common/analytics"; import { serviceByIdSelector } from "../../../services/details/store/reducers"; import { trackCTAPressed, trackPaymentStart } from "../../analytics"; import { CTA } from "../../types/MessageCTA"; -import { CTAActionType } from "../../utils/ctas"; export const computeAndTrackCTAPressAnalytics = ( isFirstCTA: boolean, @@ -35,22 +33,3 @@ export const computeAndTrackPaymentStart = ( service?.organization_fiscal_code ); }; - -export const computeAndTrackFIMSAuthenticationStart = ( - type: CTAActionType, - ctaLabel: string, - serviceId: ServiceId, - state: GlobalState -) => { - if (type === "fims") { - const service = serviceByIdSelector(state, serviceId); - trackAuthenticationStart( - serviceId, - service?.service_name, - service?.organization_name, - service?.organization_fiscal_code, - ctaLabel, - "message_detail" - ); - } -}; diff --git a/ts/features/messages/saga/handleLoadMessageData.ts b/ts/features/messages/saga/handleLoadMessageData.ts index 879a4ba1158..c8edad25fa7 100644 --- a/ts/features/messages/saga/handleLoadMessageData.ts +++ b/ts/features/messages/saga/handleLoadMessageData.ts @@ -39,8 +39,8 @@ import { RemoteContentDetails } from "../../../../definitions/backend/RemoteCont import { MessageGetStatusFailurePhaseType } from "../store/reducers/messageGetStatus"; import { ServicePublic } from "../../../../definitions/backend/ServicePublic"; import { ctaFromMessageCTA, unsafeMessageCTAFromInput } from "../utils/ctas"; -import { isIoFIMSLink } from "../../../components/ui/Markdown/handlers/link"; import { extractContentFromMessageSources } from "../utils"; +import { isFIMSLink } from "../../fims/singleSignOn/utils"; export function* handleLoadMessageData( action: ActionType @@ -397,10 +397,10 @@ const computeHasFIMSCTA = ( ); const unsafeMessageCTA = unsafeMessageCTAFromInput(markdownWithCTAs); const cta = ctaFromMessageCTA(unsafeMessageCTA); - if (cta != null && isIoFIMSLink(cta.cta_1.action)) { + if (cta != null && isFIMSLink(cta.cta_1.action)) { return true; } - if (cta?.cta_2 != null && isIoFIMSLink(cta.cta_2.action)) { + if (cta?.cta_2 != null && isFIMSLink(cta.cta_2.action)) { return true; } return false; diff --git a/ts/features/messages/utils/ctas.ts b/ts/features/messages/utils/ctas.ts index 03b5b0b29b3..cf2979420b5 100644 --- a/ts/features/messages/utils/ctas.ts +++ b/ts/features/messages/utils/ctas.ts @@ -14,21 +14,18 @@ import { ServiceMetadata } from "../../../../definitions/backend/ServiceMetadata import { Locales } from "../../../../locales/locales"; import { deriveCustomHandledLink, - isIoFIMSLink, - isIoInternalLink, - removeFIMSPrefixFromUrl + isIoInternalLink } from "../../../components/ui/Markdown/handlers/link"; import { trackMessageCTAFrontMatterDecodingError } from "../analytics"; import { localeFallback } from "../../../i18n"; -import NavigationService from "../../../navigation/NavigationService"; import { CTA, CTAS, MessageCTA, MessageCTALocales } from "../types/MessageCTA"; import { getInternalRoute, handleInternalLink } from "../../../utils/internalLink"; import { getLocalePrimaryWithFallback } from "../../../utils/locale"; -import { FIMS_ROUTES } from "../../fims/common/navigation"; import { isTestEnv } from "../../../utils/environment"; +import { isFIMSLink } from "../../fims/singleSignOn/utils"; export type CTAActionType = | "io_handled_link" @@ -39,33 +36,19 @@ export type CTAActionType = export const handleCtaAction = ( cta: CTA, linkTo: (path: string) => void, - preActionCallback?: (actionType: CTAActionType) => void + fimsCallback: (label: string, url: string) => void ) => { if (isIoInternalLink(cta.action)) { - preActionCallback?.("io_internal_link"); const convertedLink = getInternalRoute(cta.action); handleInternalLink(linkTo, `${convertedLink}`); - return; - } else if (isIoFIMSLink(cta.action)) { - preActionCallback?.("fims"); - const url = removeFIMSPrefixFromUrl(cta.action); - NavigationService.navigate(FIMS_ROUTES.MAIN, { - screen: FIMS_ROUTES.CONSENTS, - params: { - ctaText: cta.text, - ctaUrl: url - } - }); - return; + } else if (isFIMSLink(cta.action)) { + fimsCallback(cta.text, cta.action); } else { const maybeHandledAction = deriveCustomHandledLink(cta.action); if (E.isRight(maybeHandledAction)) { - preActionCallback?.("io_handled_link"); Linking.openURL(maybeHandledAction.right.url).catch(() => 0); - return; } } - preActionCallback?.("none"); }; const hasMetadataTokenName = (metadata?: ServiceMetadata): boolean => @@ -221,7 +204,7 @@ const isCtaActionValid = ( O.getOrElse(() => true) ); } - if (isIoFIMSLink(cta.action)) { + if (isFIMSLink(cta.action)) { return pipe( E.tryCatch( () => new URL(cta.action), diff --git a/ts/features/services/details/screens/ServiceDetailsScreen.tsx b/ts/features/services/details/screens/ServiceDetailsScreen.tsx index 8800f431b6f..fb0e2a9d4e2 100644 --- a/ts/features/services/details/screens/ServiceDetailsScreen.tsx +++ b/ts/features/services/details/screens/ServiceDetailsScreen.tsx @@ -12,11 +12,7 @@ import { useIODispatch, useIOSelector } from "../../../../store/hooks"; import { useOnFirstRender } from "../../../../utils/hooks/useOnFirstRender"; import { logosForService } from "../../common/utils"; import { CTA, CTAS } from "../../../messages/types/MessageCTA"; -import { - CTAActionType, - getServiceCTA, - handleCtaAction -} from "../../../messages/utils/ctas"; +import { getServiceCTA, handleCtaAction } from "../../../messages/utils/ctas"; import * as analytics from "../../common/analytics"; import { CtaCategoryType } from "../../common/analytics"; import { ServicesHeaderSection } from "../../common/components/ServicesHeaderSection"; @@ -44,7 +40,7 @@ import { serviceMetadataInfoSelector } from "../store/reducers"; import { ServiceMetadataInfo } from "../types/ServiceMetadataInfo"; -import { trackAuthenticationStart } from "../../../fims/common/analytics"; +import { useFIMSFromServiceId } from "../../../fims/common/hooks"; export type ServiceDetailsScreenRouteParams = { serviceId: ServiceId; @@ -99,6 +95,8 @@ export const ServiceDetailsScreen = ({ route }: ServiceDetailsScreenProps) => { [serviceMetadata] ); + const { startFIMSAuthenticationFlow } = useFIMSFromServiceId(serviceId); + useOnFirstRender( () => { analytics.trackServiceDetails({ @@ -152,18 +150,9 @@ export const ServiceDetailsScreen = ({ route }: ServiceDetailsScreenProps) => { cta_category: ctaType, service_id: serviceId }); - handleCtaAction(cta, linkTo, (type: CTAActionType) => { - if (type === "fims") { - trackAuthenticationStart( - service.service_id, - service.service_name, - service.organization_name, - service.organization_fiscal_code, - cta.text, - "service_detail" - ); - } - }); + handleCtaAction(cta, linkTo, (label, url) => + startFIMSAuthenticationFlow(label, url) + ); }; const getActionsProps = ( diff --git a/ts/store/reducers/backendStatus/remoteConfig.ts b/ts/store/reducers/backendStatus/remoteConfig.ts index d6eef7460da..54171141091 100644 --- a/ts/store/reducers/backendStatus/remoteConfig.ts +++ b/ts/store/reducers/backendStatus/remoteConfig.ts @@ -1,4 +1,5 @@ import * as O from "fp-ts/lib/Option"; +import * as RA from "fp-ts/lib/ReadonlyArray"; import { pipe } from "fp-ts/lib/function"; import { Platform } from "react-native"; import { createSelector } from "reselect"; @@ -21,6 +22,7 @@ import { Action } from "../../actions/types"; import { isPropertyWithMinAppVersionEnabled } from "../featureFlagWithMinAppVersionStatus"; import { isIdPayLocallyEnabledSelector } from "../persistedPreferences"; import { GlobalState } from "../types"; +import { FimsServiceConfiguration_config } from "../../../../definitions/content/FimsServiceConfiguration"; export type RemoteConfigState = O.Option; @@ -121,6 +123,26 @@ export const fimsRequiresAppUpdateSelector = (state: GlobalState) => }) ); +export const fimsServiceConfiguration = createSelector( + [ + remoteConfigSelector, + (_state: GlobalState, configurationId: string) => configurationId + ], + ( + remoteConfig, + configurationId: string + ): FimsServiceConfiguration_config | undefined => + pipe( + remoteConfig, + O.chainNullableK(config => config.fims.services), + O.map( + RA.findFirst(service => service.configuration_id === configurationId) + ), + O.flatten, + O.toUndefined + ) +); + export const oidcProviderDomainSelector = (state: GlobalState) => pipe( state, diff --git a/ts/utils/__tests__/internalLink.test.ts b/ts/utils/__tests__/internalLink.test.ts index fba09b3501d..1bcd9c1db9f 100644 --- a/ts/utils/__tests__/internalLink.test.ts +++ b/ts/utils/__tests__/internalLink.test.ts @@ -5,10 +5,10 @@ import { testableALLOWED_ROUTE_NAMES } from "../internalLink"; import { - IO_FIMS_LINK_PREFIX, IO_INTERNAL_LINK_PREFIX, IO_UNIVERSAL_LINK_PREFIX } from "../navigation"; +import { IO_FIMS_LINK_PREFIX } from "../../features/fims/singleSignOn/utils"; describe("getInternalRoute", () => { const allowedRoutes = Object.entries(testableALLOWED_ROUTE_NAMES!).map( diff --git a/ts/utils/internalLink.ts b/ts/utils/internalLink.ts index 2af65e5e060..5957b23a079 100644 --- a/ts/utils/internalLink.ts +++ b/ts/utils/internalLink.ts @@ -10,9 +10,9 @@ import { FCI_ROUTES } from "../features/fci/navigation/routes"; import ROUTES from "../navigation/routes"; import { MESSAGES_ROUTES } from "../features/messages/navigation/routes"; import { SERVICES_ROUTES } from "../features/services/common/navigation/routes"; +import { IO_FIMS_LINK_PREFIX } from "../features/fims/singleSignOn/utils"; import { isTestEnv } from "./environment"; import { - IO_FIMS_LINK_PREFIX, IO_INTERNAL_LINK_PREFIX, IO_INTERNAL_LINK_PROTOCOL, IO_UNIVERSAL_LINK_PREFIX diff --git a/ts/utils/navigation.ts b/ts/utils/navigation.ts index 729568fe16c..ce82b41b4af 100644 --- a/ts/utils/navigation.ts +++ b/ts/utils/navigation.ts @@ -5,9 +5,6 @@ import { Platform } from "react-native"; export const IO_INTERNAL_LINK_PROTOCOL = "ioit:"; export const IO_INTERNAL_LINK_PREFIX = IO_INTERNAL_LINK_PROTOCOL + "//"; -export const IO_FIMS_LINK_PROTOCOL = "iosso:"; -export const IO_FIMS_LINK_PREFIX = IO_FIMS_LINK_PROTOCOL + "//"; - export const IO_UNIVERSAL_LINK_PREFIX = "https://continua.io.pagopa.it"; /** From 3fbd2b8ce95a747ba118d4d90f1b16b06a79e52b Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Tue, 18 Feb 2025 17:47:52 +0100 Subject: [PATCH 02/13] hook for IdPay --- ts/features/fims/common/hooks/index.tsx | 174 +++++++++++------- .../screens/IdPayInitiativeDetailsScreen.tsx | 8 +- 2 files changed, 115 insertions(+), 67 deletions(-) diff --git a/ts/features/fims/common/hooks/index.tsx b/ts/features/fims/common/hooks/index.tsx index 1ab56d293d8..e12cbf44581 100644 --- a/ts/features/fims/common/hooks/index.tsx +++ b/ts/features/fims/common/hooks/index.tsx @@ -11,11 +11,23 @@ import { } from "../../../services/details/store/reducers"; import { loadServiceDetail } from "../../../services/details/store/actions/details"; import { isStrictNone } from "../../../../utils/pot"; -import { useIONavigation } from "../../../../navigation/params/AppParamsList"; +import { + AppParamsList, + IOStackNavigationProp, + useIONavigation +} from "../../../../navigation/params/AppParamsList"; import { FIMS_ROUTES } from "../navigation"; import { removeFIMSPrefixFromUrl } from "../../singleSignOn/utils"; import { isTestEnv } from "../../../../utils/environment"; import { fimsServiceConfiguration } from "../../../../store/reducers/backendStatus/remoteConfig"; +import { GlobalState } from "../../../../store/reducers/types"; + +type FIMSServiceData = { + organizationFiscalCode?: string; + organizationName?: string; + serviceId: ServiceId; + serviceName?: string; +}; export const useAutoFetchingServiceByIdPot = (serviceId: ServiceId) => { const dispatch = useIODispatch(); @@ -36,8 +48,17 @@ export const useAutoFetchingServiceByIdPot = (serviceId: ServiceId) => { return serviceData; }; +/** + * Use this hook to retrieve a function that starts the FIMS authentication flow. + * Choose this hook when the service data is already loaded into redux + * (e.g., when coming from a message details or service details). + */ export const useFIMSFromServiceId = (serviceId: ServiceId) => { - const serviceData = useServiceDataFromServiceId(serviceId); + const store = useIOStore(); + const serviceData = useMemo( + () => serviceDataFromServiceId(serviceId, store.getState()), + [serviceId, store] + ); const startFIMSAuthenticationFlow = useFIMSFromServiceData(serviceData); return useMemo( () => ({ @@ -48,8 +69,37 @@ export const useFIMSFromServiceId = (serviceId: ServiceId) => { ); }; +/** + * Use this hook to retrieve a function that starts the FIMS authentication flow. + * Choose this hook when the service id is not available upon hook invocation + * but it will be later, when the returned function is called. + */ +export const useFIMSAuthenticationFlow = () => { + const navigation = useIONavigation(); + const store = useIOStore(); + return useCallback( + (label: string, serviceId: ServiceId, url: string) => { + const serviceData = serviceDataFromServiceId(serviceId, store.getState()); + if (serviceData == null) { + return; + } + navigateToFIMSAuthorizationFlow(label, navigation, serviceData, url); + }, + [navigation, store] + ); +}; + +/** + * Use this hook to retrieve a function that starts the FIMS authentication flow. + * Choose this hook when the service data are stored into the remote CDN and you + * know the configuration id that identifies and retrieves such data. + */ export const useFIMSRemoteServiceConfiguration = (configurationId: string) => { - const serviceData = useServiceDataFromConfigurationId(configurationId); + const store = useIOStore(); + const serviceData = useMemo( + () => serviceDataFromConfigurationId(configurationId, store.getState()), + [configurationId, store] + ); const startFIMSAuthenticationFlow = useFIMSFromServiceData(serviceData); return useMemo( () => ({ @@ -60,82 +110,80 @@ export const useFIMSRemoteServiceConfiguration = (configurationId: string) => { ); }; -type FIMSServiceData = { - organizationFiscalCode?: string; - organizationName?: string; - serviceId: ServiceId; - serviceName?: string; +const serviceDataFromServiceId = ( + serviceId: ServiceId, + state: GlobalState +): FIMSServiceData => { + const service = serviceByIdSelector(state, serviceId); + return service != null + ? { + organizationFiscalCode: service.organization_fiscal_code, + organizationName: service.organization_name, + serviceId: service.service_id, + serviceName: service.service_name + } + : { serviceId }; }; -const useFIMSFromServiceData = (serviceData: FIMSServiceData | undefined) => { - const navigation = useIONavigation(); - - const source = navigation.getState().key; +const serviceDataFromConfigurationId = ( + configurationId: string, + state: GlobalState +): FIMSServiceData | undefined => { + const serviceConfiguration = fimsServiceConfiguration(state, configurationId); + return serviceConfiguration != null + ? { + organizationFiscalCode: serviceConfiguration.organization_fiscal_code, + organizationName: serviceConfiguration.organization_name, + serviceId: serviceConfiguration.service_id as ServiceId, + serviceName: serviceConfiguration.service_name + } + : undefined; +}; +const useFIMSFromServiceData = ( + serviceData: FIMSServiceData | undefined +): ((label: string, url: string) => void) => { + const navigation = useIONavigation(); return useCallback( (label: string, url: string) => { if (serviceData == null) { return; } - const sanitizedUrl = removeFIMSPrefixFromUrl(url); - navigation.navigate(FIMS_ROUTES.MAIN, { - screen: FIMS_ROUTES.CONSENTS, - params: { - ctaUrl: sanitizedUrl, - ctaText: label, - organizationFiscalCode: serviceData.organizationFiscalCode, - organizationName: serviceData.organizationName, - serviceId: serviceData.serviceId, - serviceName: serviceData.serviceName, - source - } - }); + navigateToFIMSAuthorizationFlow(label, navigation, serviceData, url); }, - [navigation, serviceData, source] + [navigation, serviceData] ); }; -const useServiceDataFromServiceId = ( - serviceId: ServiceId -): FIMSServiceData | undefined => { - const store = useIOStore(); - return useMemo(() => { - const service = serviceByIdSelector(store.getState(), serviceId); - return service != null - ? { - organizationFiscalCode: service.organization_fiscal_code, - organizationName: service.organization_name, - serviceId: service.service_id, - serviceName: service.service_name - } - : undefined; - }, [serviceId, store]); -}; +const navigateToFIMSAuthorizationFlow = ( + label: string, + navigation: IOStackNavigationProp, + serviceData: FIMSServiceData, + url: string +): void => { + const navigationState = navigation.getState(); + const source = navigationState.routes[navigationState.index].name; -const useServiceDataFromConfigurationId = ( - configurationId: string -): FIMSServiceData | undefined => { - const store = useIOStore(); - return useMemo(() => { - const serviceConfiguration = fimsServiceConfiguration( - store.getState(), - configurationId - ); - return serviceConfiguration != null - ? { - organizationFiscalCode: serviceConfiguration.organization_fiscal_code, - organizationName: serviceConfiguration.organization_name, - serviceId: serviceConfiguration.service_id as ServiceId, - serviceName: serviceConfiguration.service_name - } - : undefined; - }, [configurationId, store]); + const sanitizedUrl = removeFIMSPrefixFromUrl(url); + navigation.navigate(FIMS_ROUTES.MAIN, { + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: sanitizedUrl, + ctaText: label, + organizationFiscalCode: serviceData.organizationFiscalCode, + organizationName: serviceData.organizationName, + serviceId: serviceData.serviceId, + serviceName: serviceData.serviceName, + source + } + }); }; export const testable = isTestEnv ? { - useFIMSFromServiceData, - useServiceDataFromConfigurationId, - useServiceDataFromServiceId + navigateToFIMSAuthorizationFlow, + serviceDataFromConfigurationId, + serviceDataFromServiceId, + useFIMSFromServiceData } : undefined; diff --git a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx index b18ebba328b..25004a9ccf9 100644 --- a/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx +++ b/ts/features/idpay/details/screens/IdPayInitiativeDetailsScreen.tsx @@ -52,7 +52,8 @@ import { initiativeNeedsConfigurationSelector } from "../store"; import { idpayInitiativeGet, idpayTimelinePageGet } from "../store/actions"; -import { useFIMSRemoteServiceConfiguration } from "../../../fims/common/hooks"; +import { useFIMSAuthenticationFlow } from "../../../fims/common/hooks"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; export type IdPayInitiativeDetailsScreenParams = { initiativeId: string; @@ -85,9 +86,7 @@ const IdPayInitiativeDetailsScreen = () => { }); }; - const { startFIMSAuthenticationFlow } = useFIMSRemoteServiceConfiguration( - "idPayGuidoniaSummerCamp" - ); + const startFIMSAuthenticationFlow = useFIMSAuthenticationFlow(); const onAddExpense = () => { const addExpenseFimsUrl = pot.toUndefined(initiativeDataPot)?.webViewUrl; if (!addExpenseFimsUrl) { @@ -95,6 +94,7 @@ const IdPayInitiativeDetailsScreen = () => { } startFIMSAuthenticationFlow( I18n.t("idpay.initiative.discountDetails.addExpenseButton"), + "01JKB969XNTW23RZTV61XAE824" as ServiceId, // TODO change this as soon as the serviceId is available in the initiativeDataPot addExpenseFimsUrl ); }; From ea88474bcfe81ccf933d8c1f2a3e1a53587caa06 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Wed, 19 Feb 2025 11:25:17 +0100 Subject: [PATCH 03/13] Tests for useAutoFetchingServiceByIdPot --- .../common/analytics/__tests__/index.test.ts | 67 +++++------ .../fims/common/hooks/__test__/index.test.tsx | 112 ++++++++++++++++++ ts/features/fims/singleSignOn/utils/index.ts | 49 -------- 3 files changed, 144 insertions(+), 84 deletions(-) create mode 100644 ts/features/fims/common/hooks/__test__/index.test.tsx diff --git a/ts/features/fims/common/analytics/__tests__/index.test.ts b/ts/features/fims/common/analytics/__tests__/index.test.ts index 46af155f9ac..10769274e60 100644 --- a/ts/features/fims/common/analytics/__tests__/index.test.ts +++ b/ts/features/fims/common/analytics/__tests__/index.test.ts @@ -16,6 +16,7 @@ import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; import * as mixpanel from "../../../../../mixpanel"; import { GlobalState } from "../../../../../store/reducers/types"; +import { MESSAGES_ROUTES } from "../../../../messages/navigation/routes"; import * as serviceSelectors from "../../../../services/details/store/reducers"; import * as fimsAuthenticationSelectors from "../../../singleSignOn/store/selectors"; @@ -26,9 +27,6 @@ const organizationNames = [undefined, "organization name"]; const referenceReason = "The reason"; const referenceServiceId = "01J9RSWBB4VSHVRJSY33XGA6YH" as ServiceId; const serviceNames = [undefined, "service name"]; -const sources: ReadonlyArray< - "message_detail" | "service_detail" | "credential_detail" -> = ["message_detail", "service_detail", "credential_detail"] as const; describe("trackAuthenticationStart", () => { beforeEach(() => { @@ -38,38 +36,37 @@ describe("trackAuthenticationStart", () => { organizationFiscalCodes.forEach(organizationFiscalCode => organizationNames.forEach(organizationName => serviceNames.forEach(serviceName => - sources.forEach(source => - it(`should match event name, and expected parameters for ${ - organizationFiscalCode ? "defined " : "undefined " - } organization fiscal code, ${ - organizationName ? "defined " : "undefined " - } organization name, ${ - serviceName ? "defined " : "undefined " - } service name, ${source} source, `, () => { - const mixpanelTrackMock = generateMixpanelTrackMock(); - void trackAuthenticationStart( - referenceServiceId, - serviceName, - organizationName, - organizationFiscalCode, - referenceCtaLabel, - source - ); - expect(mixpanelTrackMock.mock.calls.length).toBe(1); - expect(mixpanelTrackMock.mock.calls[0].length).toBe(2); - expect(mixpanelTrackMock.mock.calls[0][0]).toBe("FIMS_START"); - expect(mixpanelTrackMock.mock.calls[0][1]).toEqual({ - event_category: "UX", - event_type: "action", - fims_label: referenceCtaLabel, - organization_fiscal_code: organizationFiscalCode, - organization_name: organizationName, - service_id: referenceServiceId, - service_name: serviceName, - source - }); - }) - ) + it(`should match event name, and expected parameters for ${ + organizationFiscalCode ? "defined " : "undefined " + } organization fiscal code, ${ + organizationName ? "defined " : "undefined " + } organization name, ${ + serviceName ? "defined " : "undefined " + } service name`, () => { + const source = MESSAGES_ROUTES.MESSAGE_DETAIL; + const mixpanelTrackMock = generateMixpanelTrackMock(); + void trackAuthenticationStart( + referenceServiceId, + serviceName, + organizationName, + organizationFiscalCode, + referenceCtaLabel, + source + ); + expect(mixpanelTrackMock.mock.calls.length).toBe(1); + expect(mixpanelTrackMock.mock.calls[0].length).toBe(2); + expect(mixpanelTrackMock.mock.calls[0][0]).toBe("FIMS_START"); + expect(mixpanelTrackMock.mock.calls[0][1]).toEqual({ + event_category: "UX", + event_type: "action", + fims_label: referenceCtaLabel, + organization_fiscal_code: organizationFiscalCode, + organization_name: organizationName, + service_id: referenceServiceId, + service_name: serviceName, + source + }); + }) ) ) ); diff --git a/ts/features/fims/common/hooks/__test__/index.test.tsx b/ts/features/fims/common/hooks/__test__/index.test.tsx new file mode 100644 index 00000000000..0d1e3e9bc81 --- /dev/null +++ b/ts/features/fims/common/hooks/__test__/index.test.tsx @@ -0,0 +1,112 @@ +import * as pot from "@pagopa/ts-commons/lib/pot"; +import { createStore } from "redux"; +import { useAutoFetchingServiceByIdPot } from ".."; +import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { MESSAGES_ROUTES } from "../../../../messages/navigation/routes"; +import { GlobalState } from "../../../../../store/reducers/types"; +import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; +import { loadServiceDetail } from "../../../../services/details/store/actions/details"; + +const mockDispatch = jest.fn(); +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: () => mockDispatch +})); + +// eslint-disable-next-line functional/no-let +let serviceData: pot.Pot | undefined; + +describe("index", () => { + afterEach(() => { + jest.restoreAllMocks(); + jest.resetAllMocks(); + }); + describe("useAutoFetchingServiceByIdPot", () => { + const service = {} as ServicePublic; + [ + pot.none, + pot.noneLoading, + pot.noneUpdating(service), + pot.noneError(Error("")), + pot.some(service), + pot.someLoading(service), + pot.someUpdating(service, service), + pot.someError(service, Error("")) + ].forEach(servicePot => { + const shouldHaveCalledDispatch = + servicePot.kind === "PotNone" || servicePot.kind === "PotNoneError"; + it(`should ${ + shouldHaveCalledDispatch ? "" : "not " + }have dispatched 'loadServiceDetail.request' and returned proper data for input pot of type '${ + servicePot.kind + }'`, () => { + const serviceId = "01JMESJKA9HS28MEW12P7WPYVC" as ServiceId; + + renderHook(serviceId, servicePot, serviceId); + + expect(serviceData).toEqual(servicePot); + if (shouldHaveCalledDispatch) { + expect(mockDispatch.mock.calls.length).toBe(1); + expect(mockDispatch.mock.calls[0].length).toBe(1); + expect(mockDispatch.mock.calls[0][0]).toEqual( + loadServiceDetail.request(serviceId) + ); + } else { + expect(mockDispatch.mock.calls.length).toBe(0); + } + }); + }); + it(`should have dispatched 'loadServiceDetail.request' and returned proper data for unmatching serviceId`, () => { + const hookServiceId = "01JMESJKA9HS28MEW12P7WPYVC" as ServiceId; + renderHook( + hookServiceId, + pot.some({} as ServicePublic), + "8MEW12P7WPYVC01JMESJKA9HS2" as ServiceId + ); + + expect(serviceData).toEqual(pot.none); + + expect(mockDispatch.mock.calls.length).toBe(1); + expect(mockDispatch.mock.calls[0].length).toBe(1); + expect(mockDispatch.mock.calls[0][0]).toEqual( + loadServiceDetail.request(hookServiceId) + ); + }); + }); +}); + +const renderHook = ( + hookServiceId: ServiceId, + servicePot: pot.Pot, + storeServiceId: ServiceId +) => { + const initialState = appReducer( + { + features: { + services: { + details: { + byId: { + [storeServiceId]: servicePot + } + } + } + } + } as GlobalState, + applicationChangeState("active") + ); + const store = createStore(appReducer, initialState as any); + return renderScreenWithNavigationStoreContext( + () => HookWrapper(hookServiceId), + MESSAGES_ROUTES.MESSAGE_DETAIL, + {}, + store + ); +}; + +const HookWrapper = (serviceId: ServiceId) => { + serviceData = useAutoFetchingServiceByIdPot(serviceId); + return undefined; +}; diff --git a/ts/features/fims/singleSignOn/utils/index.ts b/ts/features/fims/singleSignOn/utils/index.ts index bd98cb89f6c..3e070286a51 100644 --- a/ts/features/fims/singleSignOn/utils/index.ts +++ b/ts/features/fims/singleSignOn/utils/index.ts @@ -3,59 +3,10 @@ import { ActionType } from "typesafe-actions"; import { FimsFlowStateTags, FimsSSOState } from "../store/reducers"; import { startApplicationInitialization } from "../../../../store/actions/application"; import { isStrictSome } from "../../../../utils/pot"; -import { FIMS_ROUTES } from "../../common/navigation"; -import NavigationService from "../../../../navigation/NavigationService"; -import { ServiceId } from "../../../../../definitions/backend/ServiceId"; -import { FimsServiceConfiguration_config } from "../../../../../definitions/content/FimsServiceConfiguration"; export const IO_FIMS_LINK_PROTOCOL = "iosso:"; export const IO_FIMS_LINK_PREFIX = IO_FIMS_LINK_PROTOCOL + "//"; -export const startAuthenticationFlow = ( - label: string, - organizationFiscalCode: string | undefined, - organizationName: string | undefined, - serviceId: ServiceId, - serviceName: string | undefined, - source: string, - url: string -) => { - const sanitizedUrl = removeFIMSPrefixFromUrl(url); - NavigationService.navigate(FIMS_ROUTES.MAIN, { - screen: FIMS_ROUTES.CONSENTS, - params: { - ctaUrl: sanitizedUrl, - ctaText: label, - organizationFiscalCode, - organizationName, - serviceId, - serviceName, - source - } - }); -}; - -export const startAuthenticationFlowWithServiceConfiguration = ( - label: string, - serviceConfiguration: FimsServiceConfiguration_config, - source: string, - url: string -) => { - const sanitizedUrl = removeFIMSPrefixFromUrl(url); - NavigationService.navigate(FIMS_ROUTES.MAIN, { - screen: FIMS_ROUTES.CONSENTS, - params: { - ctaUrl: sanitizedUrl, - ctaText: label, - organizationFiscalCode: serviceConfiguration.organization_fiscal_code, - organizationName: serviceConfiguration.organization_name, - serviceId: serviceConfiguration.service_id as ServiceId, - serviceName: serviceConfiguration.service_name, - source - } - }); -}; - export const foldFimsFlowState = ( flowState: FimsFlowStateTags, onConsents: (state: "consents") => A, From 6cd4d09e9c54b46088b3bb85340b111f9b082651 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Wed, 19 Feb 2025 12:30:00 +0100 Subject: [PATCH 04/13] Tests for useFIMSFromServiceId --- .../fims/common/hooks/__test__/index.test.tsx | 199 +++++++++++++++--- 1 file changed, 175 insertions(+), 24 deletions(-) diff --git a/ts/features/fims/common/hooks/__test__/index.test.tsx b/ts/features/fims/common/hooks/__test__/index.test.tsx index 0d1e3e9bc81..5495faf765b 100644 --- a/ts/features/fims/common/hooks/__test__/index.test.tsx +++ b/ts/features/fims/common/hooks/__test__/index.test.tsx @@ -1,6 +1,7 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; +import { ComponentType } from "react"; import { createStore } from "redux"; -import { useAutoFetchingServiceByIdPot } from ".."; +import { useAutoFetchingServiceByIdPot, useFIMSFromServiceId } from ".."; import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; import { applicationChangeState } from "../../../../../store/actions/application"; import { appReducer } from "../../../../../store/reducers"; @@ -9,6 +10,7 @@ import { MESSAGES_ROUTES } from "../../../../messages/navigation/routes"; import { GlobalState } from "../../../../../store/reducers/types"; import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; import { loadServiceDetail } from "../../../../services/details/store/actions/details"; +import { FIMS_ROUTES } from "../../navigation"; const mockDispatch = jest.fn(); jest.mock("react-redux", () => ({ @@ -16,8 +18,31 @@ jest.mock("react-redux", () => ({ useDispatch: () => mockDispatch })); +const mockNavigation = jest.fn(); +jest.mock("@react-navigation/native", () => { + const actualNav = jest.requireActual("@react-navigation/native"); + return { + ...actualNav, + useNavigation: () => ({ + getState: () => ({ + index: 0, + routes: [ + { + name: "MESSAGE_DETAIL" + } + ] + }), + navigate: mockNavigation + }) + }; +}); + +// eslint-disable-next-line functional/no-let +let serviceDataPot: pot.Pot | undefined; // eslint-disable-next-line functional/no-let -let serviceData: pot.Pot | undefined; +let serviceData: unknown; +// eslint-disable-next-line functional/no-let +let authenticationCallback: (label: string, url: string) => void | undefined; describe("index", () => { afterEach(() => { @@ -45,9 +70,9 @@ describe("index", () => { }'`, () => { const serviceId = "01JMESJKA9HS28MEW12P7WPYVC" as ServiceId; - renderHook(serviceId, servicePot, serviceId); + renderAutoFetchHook(serviceId, servicePot, serviceId); - expect(serviceData).toEqual(servicePot); + expect(serviceDataPot).toEqual(servicePot); if (shouldHaveCalledDispatch) { expect(mockDispatch.mock.calls.length).toBe(1); expect(mockDispatch.mock.calls[0].length).toBe(1); @@ -61,13 +86,14 @@ describe("index", () => { }); it(`should have dispatched 'loadServiceDetail.request' and returned proper data for unmatching serviceId`, () => { const hookServiceId = "01JMESJKA9HS28MEW12P7WPYVC" as ServiceId; - renderHook( + + renderAutoFetchHook( hookServiceId, pot.some({} as ServicePublic), "8MEW12P7WPYVC01JMESJKA9HS2" as ServiceId ); - expect(serviceData).toEqual(pot.none); + expect(serviceDataPot).toEqual(pot.none); expect(mockDispatch.mock.calls.length).toBe(1); expect(mockDispatch.mock.calls[0].length).toBe(1); @@ -76,37 +102,162 @@ describe("index", () => { ); }); }); + describe("useFIMSFromServiceId", () => { + const serviceId = "01JMEWNY9BC3KVRCGTY1737J0S" as ServiceId; + const service = { + organization_fiscal_code: "01234567891", + organization_name: "An organization name", + service_id: serviceId, + service_name: "A service name" + } as ServicePublic; + [ + pot.none, + pot.noneLoading, + pot.noneUpdating(service), + pot.noneError(Error("")), + pot.some(service), + pot.someLoading(service), + pot.someUpdating(service, service), + pot.someError(service, Error("")) + ].forEach(servicePot => { + it(`should call 'navigation.navigate' with proper paramters for '${servicePot.kind}' service and return proper service data for analytics`, () => { + const expectedServiceData = pot.isSome(servicePot) + ? { + serviceId, + organizationFiscalCode: service.organization_fiscal_code, + organizationName: service.organization_name, + serviceName: service.service_name + } + : { + serviceId + }; + + renderFromServiceIdHook(serviceId, servicePot, serviceId); + + expect(serviceData).toEqual(expectedServiceData); + expect(authenticationCallback).toBeDefined(); + + const label = "A label"; + const url = "iosso://https://relyingParty.url/login"; + authenticationCallback(label, url); + + expect(mockNavigation.mock.calls.length).toBe(1); + expect(mockNavigation.mock.calls[0].length).toBe(2); + expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigation.mock.calls[0][1]).toEqual({ + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: "https://relyingParty.url/login", + ctaText: label, + organizationFiscalCode: pot.isSome(servicePot) + ? service.organization_fiscal_code + : undefined, + organizationName: pot.isSome(servicePot) + ? service.organization_name + : undefined, + serviceId: service.service_id, + serviceName: pot.isSome(servicePot) + ? service.service_name + : undefined, + source: MESSAGES_ROUTES.MESSAGE_DETAIL + } + }); + }); + }); + it(`should call 'navigation.navigate' with proper paramters for unmatching service id service and return proper service data for analytics`, () => { + const hookServiceId = "01JMEZB6QNR7KKDEFRR6WZEH6F" as ServiceId; + const expectedServiceData = { + serviceId: hookServiceId + }; + + renderFromServiceIdHook(hookServiceId, pot.some(service), serviceId); + + expect(serviceData).toEqual(expectedServiceData); + expect(authenticationCallback).toBeDefined(); + + const label = "A label"; + const url = "iosso://https://relyingParty.url/login"; + authenticationCallback(label, url); + + expect(mockNavigation.mock.calls.length).toBe(1); + expect(mockNavigation.mock.calls[0].length).toBe(2); + expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigation.mock.calls[0][1]).toEqual({ + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: "https://relyingParty.url/login", + ctaText: label, + organizationFiscalCode: undefined, + organizationName: undefined, + serviceId: hookServiceId, + serviceName: undefined, + source: MESSAGES_ROUTES.MESSAGE_DETAIL + } + }); + }); + }); }); -const renderHook = ( +const renderAutoFetchHook = ( hookServiceId: ServiceId, servicePot: pot.Pot, storeServiceId: ServiceId ) => { - const initialState = appReducer( - { - features: { - services: { - details: { - byId: { - [storeServiceId]: servicePot - } + const appState = { + features: { + services: { + details: { + byId: { + [storeServiceId]: servicePot } } } - } as GlobalState, - applicationChangeState("active") - ); + } + } as GlobalState; + return genericRender(() => AutoFetchHookWrapper(hookServiceId), appState); +}; + +const AutoFetchHookWrapper = (serviceId: ServiceId) => { + serviceDataPot = useAutoFetchingServiceByIdPot(serviceId); + return undefined; +}; + +const renderFromServiceIdHook = ( + hookServiceId: ServiceId, + servicePot: pot.Pot, + storeServiceId: ServiceId +) => { + const appState = { + features: { + services: { + details: { + byId: { + [storeServiceId]: servicePot + } + } + } + } + } as GlobalState; + return genericRender(() => FromServiceIdHookWrapper(hookServiceId), appState); +}; + +const FromServiceIdHookWrapper = (serviceId: ServiceId) => { + const hookData = useFIMSFromServiceId(serviceId); + serviceData = hookData.serviceData; + authenticationCallback = hookData.startFIMSAuthenticationFlow; + return undefined; +}; + +const genericRender = ( + hookWrapper: ComponentType, + appState: GlobalState +) => { + const initialState = appReducer(appState, applicationChangeState("active")); const store = createStore(appReducer, initialState as any); return renderScreenWithNavigationStoreContext( - () => HookWrapper(hookServiceId), + hookWrapper, MESSAGES_ROUTES.MESSAGE_DETAIL, {}, store ); }; - -const HookWrapper = (serviceId: ServiceId) => { - serviceData = useAutoFetchingServiceByIdPot(serviceId); - return undefined; -}; From e8e77b72872cf034a9dcb05366ba628cd13a56cb Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Wed, 19 Feb 2025 12:42:17 +0100 Subject: [PATCH 05/13] Tests for useFIMSAuthenticationFlow --- .../fims/common/hooks/__test__/index.test.tsx | 122 +++++++++++++++++- 1 file changed, 115 insertions(+), 7 deletions(-) diff --git a/ts/features/fims/common/hooks/__test__/index.test.tsx b/ts/features/fims/common/hooks/__test__/index.test.tsx index 5495faf765b..d2e6a186641 100644 --- a/ts/features/fims/common/hooks/__test__/index.test.tsx +++ b/ts/features/fims/common/hooks/__test__/index.test.tsx @@ -1,7 +1,11 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { ComponentType } from "react"; import { createStore } from "redux"; -import { useAutoFetchingServiceByIdPot, useFIMSFromServiceId } from ".."; +import { + useAutoFetchingServiceByIdPot, + useFIMSAuthenticationFlow, + useFIMSFromServiceId +} from ".."; import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; import { applicationChangeState } from "../../../../../store/actions/application"; import { appReducer } from "../../../../../store/reducers"; @@ -43,6 +47,12 @@ let serviceDataPot: pot.Pot | undefined; let serviceData: unknown; // eslint-disable-next-line functional/no-let let authenticationCallback: (label: string, url: string) => void | undefined; +// eslint-disable-next-line functional/no-let +let authenticationCallbackWithServiceId: ( + label: string, + serviceId: ServiceId, + url: string +) => void | undefined; describe("index", () => { afterEach(() => { @@ -164,7 +174,7 @@ describe("index", () => { }); }); }); - it(`should call 'navigation.navigate' with proper paramters for unmatching service id service and return proper service data for analytics`, () => { + it(`should call 'navigation.navigate' with proper paramters for unmatching service id and return proper service data for analytics`, () => { const hookServiceId = "01JMEZB6QNR7KKDEFRR6WZEH6F" as ServiceId; const expectedServiceData = { serviceId: hookServiceId @@ -196,6 +206,83 @@ describe("index", () => { }); }); }); + describe("useFIMSAuthenticationFlow", () => { + const serviceId = "01JMEWNY9BC3KVRCGTY1737J0S" as ServiceId; + const service = { + organization_fiscal_code: "01234567891", + organization_name: "An organization name", + service_id: serviceId, + service_name: "A service name" + } as ServicePublic; + [ + pot.none, + pot.noneLoading, + pot.noneUpdating(service), + pot.noneError(Error("")), + pot.some(service), + pot.someLoading(service), + pot.someUpdating(service, service), + pot.someError(service, Error("")) + ].forEach(servicePot => { + it(`should call 'navigation.navigate' with proper paramters for '${servicePot.kind}' service and return proper service data for analytics`, () => { + renderFromAuthenticationFlowHook(servicePot, serviceId); + + expect(authenticationCallbackWithServiceId).toBeDefined(); + + const label = "A label"; + const url = "iosso://https://relyingParty.url/login"; + authenticationCallbackWithServiceId(label, serviceId, url); + + expect(mockNavigation.mock.calls.length).toBe(1); + expect(mockNavigation.mock.calls[0].length).toBe(2); + expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigation.mock.calls[0][1]).toEqual({ + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: "https://relyingParty.url/login", + ctaText: label, + organizationFiscalCode: pot.isSome(servicePot) + ? service.organization_fiscal_code + : undefined, + organizationName: pot.isSome(servicePot) + ? service.organization_name + : undefined, + serviceId: service.service_id, + serviceName: pot.isSome(servicePot) + ? service.service_name + : undefined, + source: MESSAGES_ROUTES.MESSAGE_DETAIL + } + }); + }); + }); + it(`should call 'navigation.navigate' with proper paramters for unmatching service id and return proper service data for analytics`, () => { + renderFromAuthenticationFlowHook(pot.some(service), serviceId); + + expect(authenticationCallbackWithServiceId).toBeDefined(); + + const label = "A label"; + const callbackServiceId = "01JMEZB6QNR7KKDEFRR6WZEH6F" as ServiceId; + const url = "iosso://https://relyingParty.url/login"; + authenticationCallbackWithServiceId(label, callbackServiceId, url); + + expect(mockNavigation.mock.calls.length).toBe(1); + expect(mockNavigation.mock.calls[0].length).toBe(2); + expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigation.mock.calls[0][1]).toEqual({ + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: "https://relyingParty.url/login", + ctaText: label, + organizationFiscalCode: undefined, + organizationName: undefined, + serviceId: callbackServiceId, + serviceName: undefined, + source: MESSAGES_ROUTES.MESSAGE_DETAIL + } + }); + }); + }); }); const renderAutoFetchHook = ( @@ -217,11 +304,6 @@ const renderAutoFetchHook = ( return genericRender(() => AutoFetchHookWrapper(hookServiceId), appState); }; -const AutoFetchHookWrapper = (serviceId: ServiceId) => { - serviceDataPot = useAutoFetchingServiceByIdPot(serviceId); - return undefined; -}; - const renderFromServiceIdHook = ( hookServiceId: ServiceId, servicePot: pot.Pot, @@ -241,12 +323,38 @@ const renderFromServiceIdHook = ( return genericRender(() => FromServiceIdHookWrapper(hookServiceId), appState); }; +const renderFromAuthenticationFlowHook = ( + servicePot: pot.Pot, + storeServiceId: ServiceId +) => { + const appState = { + features: { + services: { + details: { + byId: { + [storeServiceId]: servicePot + } + } + } + } + } as GlobalState; + return genericRender(() => AuthenticationFlowHookWrapper(), appState); +}; + +const AutoFetchHookWrapper = (serviceId: ServiceId) => { + serviceDataPot = useAutoFetchingServiceByIdPot(serviceId); + return undefined; +}; const FromServiceIdHookWrapper = (serviceId: ServiceId) => { const hookData = useFIMSFromServiceId(serviceId); serviceData = hookData.serviceData; authenticationCallback = hookData.startFIMSAuthenticationFlow; return undefined; }; +const AuthenticationFlowHookWrapper = () => { + authenticationCallbackWithServiceId = useFIMSAuthenticationFlow(); + return undefined; +}; const genericRender = ( hookWrapper: ComponentType, From 1c005bacb41b049ae3570d162b02051295a0200b Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Wed, 19 Feb 2025 15:38:01 +0100 Subject: [PATCH 06/13] Tests for renderFromRemoteConfigurationHook --- .../fims/common/hooks/__test__/index.test.tsx | 132 ++++++++++++++++-- 1 file changed, 117 insertions(+), 15 deletions(-) diff --git a/ts/features/fims/common/hooks/__test__/index.test.tsx b/ts/features/fims/common/hooks/__test__/index.test.tsx index d2e6a186641..0c1b211442b 100644 --- a/ts/features/fims/common/hooks/__test__/index.test.tsx +++ b/ts/features/fims/common/hooks/__test__/index.test.tsx @@ -1,10 +1,12 @@ +import * as O from "fp-ts/lib/Option"; import * as pot from "@pagopa/ts-commons/lib/pot"; import { ComponentType } from "react"; import { createStore } from "redux"; import { useAutoFetchingServiceByIdPot, useFIMSAuthenticationFlow, - useFIMSFromServiceId + useFIMSFromServiceId, + useFIMSRemoteServiceConfiguration } from ".."; import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; import { applicationChangeState } from "../../../../../store/actions/application"; @@ -15,6 +17,7 @@ import { GlobalState } from "../../../../../store/reducers/types"; import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; import { loadServiceDetail } from "../../../../services/details/store/actions/details"; import { FIMS_ROUTES } from "../../navigation"; +import { FimsServiceConfiguration_config } from "../../../../../../definitions/content/FimsServiceConfiguration"; const mockDispatch = jest.fn(); jest.mock("react-redux", () => ({ @@ -46,18 +49,20 @@ let serviceDataPot: pot.Pot | undefined; // eslint-disable-next-line functional/no-let let serviceData: unknown; // eslint-disable-next-line functional/no-let -let authenticationCallback: (label: string, url: string) => void | undefined; +let authenticationCallback: ((label: string, url: string) => void) | undefined; // eslint-disable-next-line functional/no-let -let authenticationCallbackWithServiceId: ( - label: string, - serviceId: ServiceId, - url: string -) => void | undefined; +let authenticationCallbackWithServiceId: + | ((label: string, serviceId: ServiceId, url: string) => void) + | undefined; describe("index", () => { afterEach(() => { jest.restoreAllMocks(); jest.resetAllMocks(); + serviceDataPot = undefined; + serviceData = undefined; + authenticationCallback = undefined; + authenticationCallbackWithServiceId = undefined; }); describe("useAutoFetchingServiceByIdPot", () => { const service = {} as ServicePublic; @@ -130,7 +135,7 @@ describe("index", () => { pot.someUpdating(service, service), pot.someError(service, Error("")) ].forEach(servicePot => { - it(`should call 'navigation.navigate' with proper paramters for '${servicePot.kind}' service and return proper service data for analytics`, () => { + it(`should call 'navigation.navigate' with proper parameters for '${servicePot.kind}' service and return proper service data for analytics`, () => { const expectedServiceData = pot.isSome(servicePot) ? { serviceId, @@ -149,7 +154,7 @@ describe("index", () => { const label = "A label"; const url = "iosso://https://relyingParty.url/login"; - authenticationCallback(label, url); + authenticationCallback!(label, url); expect(mockNavigation.mock.calls.length).toBe(1); expect(mockNavigation.mock.calls[0].length).toBe(2); @@ -174,7 +179,7 @@ describe("index", () => { }); }); }); - it(`should call 'navigation.navigate' with proper paramters for unmatching service id and return proper service data for analytics`, () => { + it(`should call 'navigation.navigate' with proper parameters for unmatching service id and return proper service data for analytics`, () => { const hookServiceId = "01JMEZB6QNR7KKDEFRR6WZEH6F" as ServiceId; const expectedServiceData = { serviceId: hookServiceId @@ -187,7 +192,7 @@ describe("index", () => { const label = "A label"; const url = "iosso://https://relyingParty.url/login"; - authenticationCallback(label, url); + authenticationCallback!(label, url); expect(mockNavigation.mock.calls.length).toBe(1); expect(mockNavigation.mock.calls[0].length).toBe(2); @@ -224,14 +229,14 @@ describe("index", () => { pot.someUpdating(service, service), pot.someError(service, Error("")) ].forEach(servicePot => { - it(`should call 'navigation.navigate' with proper paramters for '${servicePot.kind}' service and return proper service data for analytics`, () => { + it(`should call 'navigation.navigate' with proper parameters for '${servicePot.kind}' service and return proper service data for analytics`, () => { renderFromAuthenticationFlowHook(servicePot, serviceId); expect(authenticationCallbackWithServiceId).toBeDefined(); const label = "A label"; const url = "iosso://https://relyingParty.url/login"; - authenticationCallbackWithServiceId(label, serviceId, url); + authenticationCallbackWithServiceId!(label, serviceId, url); expect(mockNavigation.mock.calls.length).toBe(1); expect(mockNavigation.mock.calls[0].length).toBe(2); @@ -256,7 +261,7 @@ describe("index", () => { }); }); }); - it(`should call 'navigation.navigate' with proper paramters for unmatching service id and return proper service data for analytics`, () => { + it(`should call 'navigation.navigate' with proper parameters for unmatching service id and return proper service data for analytics`, () => { renderFromAuthenticationFlowHook(pot.some(service), serviceId); expect(authenticationCallbackWithServiceId).toBeDefined(); @@ -264,7 +269,7 @@ describe("index", () => { const label = "A label"; const callbackServiceId = "01JMEZB6QNR7KKDEFRR6WZEH6F" as ServiceId; const url = "iosso://https://relyingParty.url/login"; - authenticationCallbackWithServiceId(label, callbackServiceId, url); + authenticationCallbackWithServiceId!(label, callbackServiceId, url); expect(mockNavigation.mock.calls.length).toBe(1); expect(mockNavigation.mock.calls[0].length).toBe(2); @@ -283,6 +288,71 @@ describe("index", () => { }); }); }); + describe("renderFromRemoteConfigurationHook", () => { + it(`should call 'navigation.navigate' with proper parameters and return proper service data for analytics`, () => { + const configurationId = "configId"; + const configuration: FimsServiceConfiguration_config = { + configuration_id: configurationId, + organization_fiscal_code: "01234567890", + organization_name: "Organization name", + service_id: "01JMF90E33B89232YER1WRYQ26" as ServiceId, + service_name: "Service name" + }; + + renderFromRemoteConfigurationHook(configuration, configurationId); + + expect(serviceData).toEqual({ + serviceId: configuration.service_id, + organizationFiscalCode: configuration.organization_fiscal_code, + organizationName: configuration.organization_name, + serviceName: configuration.service_name + }); + expect(authenticationCallback).toBeDefined(); + + const label = "FIMS label"; + const url = "iosso://https://relyingParty.url/login"; + authenticationCallback!(label, url); + + expect(mockNavigation.mock.calls.length).toBe(1); + expect(mockNavigation.mock.calls[0].length).toBe(2); + expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigation.mock.calls[0][1]).toEqual({ + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: "https://relyingParty.url/login", + ctaText: label, + organizationFiscalCode: configuration.organization_fiscal_code, + organizationName: configuration.organization_name, + serviceId: configuration.service_id, + serviceName: configuration.service_name, + source: MESSAGES_ROUTES.MESSAGE_DETAIL + } + }); + }); + it(`should not call 'navigation.navigate' and return undefined serviceData when configuration id does not match`, () => { + const configuration: FimsServiceConfiguration_config = { + configuration_id: "configId", + organization_fiscal_code: "01234567890", + organization_name: "Organization name", + service_id: "01JMF90E33B89232YER1WRYQ26" as ServiceId, + service_name: "Service name" + }; + + renderFromRemoteConfigurationHook( + configuration, + "unmatchingConfiguration" + ); + + expect(serviceData).toBeUndefined(); + expect(authenticationCallback).toBeDefined(); + + const label = "FIMS label"; + const url = "iosso://https://relyingParty.url/login"; + authenticationCallback!(label, url); + + expect(mockNavigation.mock.calls.length).toBe(0); + }); + }); }); const renderAutoFetchHook = ( @@ -341,6 +411,32 @@ const renderFromAuthenticationFlowHook = ( return genericRender(() => AuthenticationFlowHookWrapper(), appState); }; +const renderFromRemoteConfigurationHook = ( + configuration: FimsServiceConfiguration_config, + hookConfigurationId: string +) => { + const appState = { + remoteConfig: O.some({ + cgn: { + enabled: false + }, + fims: { + services: [configuration] + }, + itw: { + min_app_version: { + android: "0.0.0.0", + ios: "0.0.0.0" + } + } + }) + } as GlobalState; + return genericRender( + () => FromRemoteConfigurationHookWrapper(hookConfigurationId), + appState + ); +}; + const AutoFetchHookWrapper = (serviceId: ServiceId) => { serviceDataPot = useAutoFetchingServiceByIdPot(serviceId); return undefined; @@ -355,6 +451,12 @@ const AuthenticationFlowHookWrapper = () => { authenticationCallbackWithServiceId = useFIMSAuthenticationFlow(); return undefined; }; +const FromRemoteConfigurationHookWrapper = (configurationId: string) => { + const hookData = useFIMSRemoteServiceConfiguration(configurationId); + serviceData = hookData.serviceData; + authenticationCallback = hookData.startFIMSAuthenticationFlow; + return undefined; +}; const genericRender = ( hookWrapper: ComponentType, From d2d613d578ef1cefff47d4a17393424b1b1a12f6 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Wed, 19 Feb 2025 17:28:30 +0100 Subject: [PATCH 07/13] Tests for useFIMSFromServiceData --- .../fims/common/hooks/__test__/index.test.tsx | 296 +++++++++++++++--- ts/features/fims/common/hooks/index.tsx | 2 +- 2 files changed, 262 insertions(+), 36 deletions(-) diff --git a/ts/features/fims/common/hooks/__test__/index.test.tsx b/ts/features/fims/common/hooks/__test__/index.test.tsx index 0c1b211442b..e3d3c58eb13 100644 --- a/ts/features/fims/common/hooks/__test__/index.test.tsx +++ b/ts/features/fims/common/hooks/__test__/index.test.tsx @@ -3,6 +3,8 @@ import * as pot from "@pagopa/ts-commons/lib/pot"; import { ComponentType } from "react"; import { createStore } from "redux"; import { + FIMSServiceData, + testable, useAutoFetchingServiceByIdPot, useFIMSAuthenticationFlow, useFIMSFromServiceId, @@ -18,6 +20,10 @@ import { ServicePublic } from "../../../../../../definitions/backend/ServicePubl import { loadServiceDetail } from "../../../../services/details/store/actions/details"; import { FIMS_ROUTES } from "../../navigation"; import { FimsServiceConfiguration_config } from "../../../../../../definitions/content/FimsServiceConfiguration"; +import { + AppParamsList, + IOStackNavigationProp +} from "../../../../../navigation/params/AppParamsList"; const mockDispatch = jest.fn(); jest.mock("react-redux", () => ({ @@ -25,29 +31,30 @@ jest.mock("react-redux", () => ({ useDispatch: () => mockDispatch })); -const mockNavigation = jest.fn(); +const mockNavigate = jest.fn(); +const mockNavigation = { + getState: () => ({ + index: 0, + routes: [ + { + name: "MESSAGE_DETAIL" + } + ] + }), + navigate: mockNavigate +} as unknown as IOStackNavigationProp; jest.mock("@react-navigation/native", () => { const actualNav = jest.requireActual("@react-navigation/native"); return { ...actualNav, - useNavigation: () => ({ - getState: () => ({ - index: 0, - routes: [ - { - name: "MESSAGE_DETAIL" - } - ] - }), - navigate: mockNavigation - }) + useNavigation: () => mockNavigation }; }); // eslint-disable-next-line functional/no-let let serviceDataPot: pot.Pot | undefined; // eslint-disable-next-line functional/no-let -let serviceData: unknown; +let serviceData: FIMSServiceData | undefined; // eslint-disable-next-line functional/no-let let authenticationCallback: ((label: string, url: string) => void) | undefined; // eslint-disable-next-line functional/no-let @@ -156,10 +163,10 @@ describe("index", () => { const url = "iosso://https://relyingParty.url/login"; authenticationCallback!(label, url); - expect(mockNavigation.mock.calls.length).toBe(1); - expect(mockNavigation.mock.calls[0].length).toBe(2); - expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); - expect(mockNavigation.mock.calls[0][1]).toEqual({ + expect(mockNavigate.mock.calls.length).toBe(1); + expect(mockNavigate.mock.calls[0].length).toBe(2); + expect(mockNavigate.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigate.mock.calls[0][1]).toEqual({ screen: FIMS_ROUTES.CONSENTS, params: { ctaUrl: "https://relyingParty.url/login", @@ -194,10 +201,10 @@ describe("index", () => { const url = "iosso://https://relyingParty.url/login"; authenticationCallback!(label, url); - expect(mockNavigation.mock.calls.length).toBe(1); - expect(mockNavigation.mock.calls[0].length).toBe(2); - expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); - expect(mockNavigation.mock.calls[0][1]).toEqual({ + expect(mockNavigate.mock.calls.length).toBe(1); + expect(mockNavigate.mock.calls[0].length).toBe(2); + expect(mockNavigate.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigate.mock.calls[0][1]).toEqual({ screen: FIMS_ROUTES.CONSENTS, params: { ctaUrl: "https://relyingParty.url/login", @@ -238,10 +245,10 @@ describe("index", () => { const url = "iosso://https://relyingParty.url/login"; authenticationCallbackWithServiceId!(label, serviceId, url); - expect(mockNavigation.mock.calls.length).toBe(1); - expect(mockNavigation.mock.calls[0].length).toBe(2); - expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); - expect(mockNavigation.mock.calls[0][1]).toEqual({ + expect(mockNavigate.mock.calls.length).toBe(1); + expect(mockNavigate.mock.calls[0].length).toBe(2); + expect(mockNavigate.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigate.mock.calls[0][1]).toEqual({ screen: FIMS_ROUTES.CONSENTS, params: { ctaUrl: "https://relyingParty.url/login", @@ -271,10 +278,10 @@ describe("index", () => { const url = "iosso://https://relyingParty.url/login"; authenticationCallbackWithServiceId!(label, callbackServiceId, url); - expect(mockNavigation.mock.calls.length).toBe(1); - expect(mockNavigation.mock.calls[0].length).toBe(2); - expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); - expect(mockNavigation.mock.calls[0][1]).toEqual({ + expect(mockNavigate.mock.calls.length).toBe(1); + expect(mockNavigate.mock.calls[0].length).toBe(2); + expect(mockNavigate.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigate.mock.calls[0][1]).toEqual({ screen: FIMS_ROUTES.CONSENTS, params: { ctaUrl: "https://relyingParty.url/login", @@ -288,7 +295,7 @@ describe("index", () => { }); }); }); - describe("renderFromRemoteConfigurationHook", () => { + describe("useFIMSRemoteServiceConfiguration", () => { it(`should call 'navigation.navigate' with proper parameters and return proper service data for analytics`, () => { const configurationId = "configId"; const configuration: FimsServiceConfiguration_config = { @@ -313,10 +320,10 @@ describe("index", () => { const url = "iosso://https://relyingParty.url/login"; authenticationCallback!(label, url); - expect(mockNavigation.mock.calls.length).toBe(1); - expect(mockNavigation.mock.calls[0].length).toBe(2); - expect(mockNavigation.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); - expect(mockNavigation.mock.calls[0][1]).toEqual({ + expect(mockNavigate.mock.calls.length).toBe(1); + expect(mockNavigate.mock.calls[0].length).toBe(2); + expect(mockNavigate.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigate.mock.calls[0][1]).toEqual({ screen: FIMS_ROUTES.CONSENTS, params: { ctaUrl: "https://relyingParty.url/login", @@ -350,7 +357,209 @@ describe("index", () => { const url = "iosso://https://relyingParty.url/login"; authenticationCallback!(label, url); - expect(mockNavigation.mock.calls.length).toBe(0); + expect(mockNavigate.mock.calls.length).toBe(0); + }); + }); + describe("navigateToFIMSAuthorizationFlow", () => { + it("should call 'navigation.navigate' with proper parameters", () => { + const label = "A label"; + const innerServiceData = { + organizationFiscalCode: "01234567891", + organizationName: "Organization name", + serviceId: "01JMFDP73MT43B4507XXQB0105" as ServiceId, + serviceName: "Service name" + }; + const url = "iosso://https://relyingParty.url/login"; + + testable!.navigateToFIMSAuthorizationFlow( + label, + mockNavigation, + innerServiceData, + url + ); + + expect(mockNavigate.mock.calls.length).toBe(1); + expect(mockNavigate.mock.calls[0].length).toBe(2); + expect(mockNavigate.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigate.mock.calls[0][1]).toEqual({ + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: "https://relyingParty.url/login", + ctaText: label, + organizationFiscalCode: innerServiceData.organizationFiscalCode, + organizationName: innerServiceData.organizationName, + serviceId: innerServiceData.serviceId, + serviceName: innerServiceData.serviceName, + source: MESSAGES_ROUTES.MESSAGE_DETAIL + } + }); + }); + }); + describe("serviceDataFromConfigurationId", () => { + it(`should return service data when the configuration id matches`, () => { + const configuration: FimsServiceConfiguration_config = { + configuration_id: "configId", + organization_fiscal_code: "01234567890", + organization_name: "Organization name", + service_id: "01JMF90E33B89232YER1WRYQ26" as ServiceId, + service_name: "Service name" + }; + const appState = { + remoteConfig: O.some({ + fims: { + services: [configuration] + } + }) + } as GlobalState; + + const serviceConfiguration = testable!.serviceDataFromConfigurationId( + configuration.configuration_id, + appState + ); + expect(serviceConfiguration).toEqual({ + organizationFiscalCode: configuration.organization_fiscal_code, + organizationName: configuration.organization_name, + serviceId: configuration.service_id, + serviceName: configuration.service_name + }); + }); + it(`should return 'undefined' when the configuration id does not match`, () => { + const configuration: FimsServiceConfiguration_config = { + configuration_id: "configId", + organization_fiscal_code: "01234567890", + organization_name: "Organization name", + service_id: "01JMF90E33B89232YER1WRYQ26" as ServiceId, + service_name: "Service name" + }; + const appState = { + remoteConfig: O.some({ + fims: { + services: [configuration] + } + }) + } as GlobalState; + + const serviceConfiguration = testable!.serviceDataFromConfigurationId( + "unmatchingConfigurationId", + appState + ); + expect(serviceConfiguration).toBeUndefined(); + }); + }); + describe("serviceDataFromServiceId", () => { + const serviceId = "01JMFEZR305XG9VSAB9RYX6X6B" as ServiceId; + const service = { + service_id: serviceId, + organization_fiscal_code: "01234567890", + organization_name: "Organization name", + service_name: "Service name" + } as ServicePublic; + [ + pot.none, + pot.noneLoading, + pot.noneUpdating(service), + pot.noneError(Error("")), + pot.some(service), + pot.someLoading(service), + pot.someUpdating(service, service), + pot.someError(service, Error("")) + ].forEach(servicePot => { + it(`should return proper service data for matching service id and service data of type ${servicePot.kind}`, () => { + const state = { + features: { + services: { + details: { + byId: { + [serviceId]: servicePot + } + } + } + } + } as GlobalState; + + const internalServiceData = testable!.serviceDataFromServiceId( + serviceId, + state + ); + + expect(internalServiceData).toEqual({ + organizationFiscalCode: pot.isSome(servicePot) + ? service.organization_fiscal_code + : undefined, + organizationName: pot.isSome(servicePot) + ? service.organization_name + : undefined, + serviceId: service.service_id, + serviceName: pot.isSome(servicePot) ? service.service_name : undefined + }); + }); + }); + it(`should return proper service data for unmatching service id`, () => { + const callbackServiceId = "01JMFFSRBHTN09A6CFM0MTXFP6" as ServiceId; + const state = { + features: { + services: { + details: { + byId: { + [serviceId]: pot.some(service) + } + } + } + } + } as GlobalState; + + const internalServiceData = testable!.serviceDataFromServiceId( + callbackServiceId, + state + ); + + expect(internalServiceData).toEqual({ + serviceId: callbackServiceId + }); + }); + }); + describe("useFIMSFromServiceData", () => { + it(`should return authentication callback that calls navigation.navigate with proper parameters when input data is defined`, () => { + const internalServiceData = { + organizationFiscalCode: "01234567891", + organizationName: "Organization name", + serviceId: "01JMFG3E20JFQH6HAQD9BDRB19" as ServiceId, + serviceName: "Service name" + }; + + renderFromServiceDataHook(internalServiceData); + + expect(authenticationCallback).toBeDefined(); + + const label = "A label"; + const url = "iosso://https://relyingParty.url/login"; + authenticationCallback!(label, url); + expect(mockNavigate.mock.calls.length).toBe(1); + expect(mockNavigate.mock.calls[0].length).toBe(2); + expect(mockNavigate.mock.calls[0][0]).toBe(FIMS_ROUTES.MAIN); + expect(mockNavigate.mock.calls[0][1]).toEqual({ + screen: FIMS_ROUTES.CONSENTS, + params: { + ctaUrl: "https://relyingParty.url/login", + ctaText: label, + organizationFiscalCode: internalServiceData.organizationFiscalCode, + organizationName: internalServiceData.organizationName, + serviceId: internalServiceData.serviceId, + serviceName: internalServiceData.serviceName, + source: MESSAGES_ROUTES.MESSAGE_DETAIL + } + }); + }); + it(`should return authentication callback that does nothing when input data is undefined`, () => { + renderFromServiceDataHook(undefined); + + expect(authenticationCallback).toBeDefined(); + + authenticationCallback!( + "a label", + "iosso://https://relyingParty.url/login" + ); + expect(mockNavigate.mock.calls.length).toBe(0); }); }); }); @@ -437,6 +646,16 @@ const renderFromRemoteConfigurationHook = ( ); }; +const renderFromServiceDataHook = ( + internalServiceData: FIMSServiceData | undefined +) => { + const appState = {} as GlobalState; + return genericRender( + () => FromServiceDataHookWrapper(internalServiceData), + appState + ); +}; + const AutoFetchHookWrapper = (serviceId: ServiceId) => { serviceDataPot = useAutoFetchingServiceByIdPot(serviceId); return undefined; @@ -457,6 +676,13 @@ const FromRemoteConfigurationHookWrapper = (configurationId: string) => { authenticationCallback = hookData.startFIMSAuthenticationFlow; return undefined; }; +const FromServiceDataHookWrapper = ( + internalServiceData: FIMSServiceData | undefined +) => { + authenticationCallback = + testable!.useFIMSFromServiceData(internalServiceData); + return undefined; +}; const genericRender = ( hookWrapper: ComponentType, diff --git a/ts/features/fims/common/hooks/index.tsx b/ts/features/fims/common/hooks/index.tsx index e12cbf44581..7ab51b4dc13 100644 --- a/ts/features/fims/common/hooks/index.tsx +++ b/ts/features/fims/common/hooks/index.tsx @@ -22,7 +22,7 @@ import { isTestEnv } from "../../../../utils/environment"; import { fimsServiceConfiguration } from "../../../../store/reducers/backendStatus/remoteConfig"; import { GlobalState } from "../../../../store/reducers/types"; -type FIMSServiceData = { +export type FIMSServiceData = { organizationFiscalCode?: string; organizationName?: string; serviceId: ServiceId; From b99bbb4f64bcf50005ed0ff9a83f194d165481c7 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Thu, 20 Feb 2025 09:58:48 +0100 Subject: [PATCH 08/13] Tests for FimsFlowHandlerScreen --- .../__test__/FimsFlowHandlerScreen.test.tsx | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 ts/features/fims/singleSignOn/screens/__test__/FimsFlowHandlerScreen.test.tsx diff --git a/ts/features/fims/singleSignOn/screens/__test__/FimsFlowHandlerScreen.test.tsx b/ts/features/fims/singleSignOn/screens/__test__/FimsFlowHandlerScreen.test.tsx new file mode 100644 index 00000000000..9127e64f277 --- /dev/null +++ b/ts/features/fims/singleSignOn/screens/__test__/FimsFlowHandlerScreen.test.tsx @@ -0,0 +1,115 @@ +import * as O from "fp-ts/lib/Option"; +import { createStore } from "redux"; +import { applicationChangeState } from "../../../../../store/actions/application"; +import { appReducer } from "../../../../../store/reducers"; +import { renderScreenWithNavigationStoreContext } from "../../../../../utils/testWrapper"; +import { FimsFlowHandlerScreen } from "../FimsFlowHandlerScreen"; +import { FIMS_ROUTES } from "../../../common/navigation"; +import { GlobalState } from "../../../../../store/reducers/types"; +import * as ANALYTICS from "../../../common/analytics"; +import { ServiceId } from "../../../../../../definitions/backend/ServiceId"; +import { fimsGetConsentsListAction } from "../../store/actions"; +import { ToolEnum } from "../../../../../../definitions/content/AssistanceToolConfig"; +import * as APPVERSION from "../../../../../utils/appVersion"; + +const ctaUrl = "https://relyingParty.url/login"; +const label = "A label"; +const organizationFiscalCode = "01234567891"; +const organizationName = "Organization name"; +const serviceId = "01JMFHJBNP8R55CJZX2G52Q1P2" as ServiceId; +const serviceName = "Service name"; +const source = "MESSAGE_DETAIL"; + +const mockDispatch = jest.fn(); +jest.mock("react-redux", () => ({ + ...jest.requireActual("react-redux"), + useDispatch: () => mockDispatch +})); + +describe("FimsFlowHandlerScreen", () => { + afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); + }); + it("should call 'trackAuthenticationStart' and dispatch action upon first rendering", () => { + jest.spyOn(APPVERSION, "getAppVersion").mockReturnValue("2.0.0.0"); + const spyOnTrackAuthenticationStart = jest.spyOn( + ANALYTICS, + "trackAuthenticationStart" + ); + renderComponent("1.0.0.0"); + + expect(spyOnTrackAuthenticationStart).toHaveBeenCalledWith( + serviceId, + serviceName, + organizationName, + organizationFiscalCode, + label, + source + ); + expect(mockDispatch.mock.calls.length).toBe(1); + expect(mockDispatch.mock.calls[0].length).toBe(1); + expect(mockDispatch.mock.calls[0][0]).toEqual( + fimsGetConsentsListAction.request({ ctaText: label, ctaUrl }) + ); + }); + it("should call 'trackAuthenticationError' upon first rendering if an app update is required", () => { + jest.spyOn(APPVERSION, "getAppVersion").mockReturnValue("2.0.0.0"); + const spyOnTrackAuthenticationError = jest.spyOn( + ANALYTICS, + "trackAuthenticationError" + ); + renderComponent("0.0.0.0"); + + expect(spyOnTrackAuthenticationError).toHaveBeenCalledWith( + undefined, + undefined, + undefined, + undefined, + "update_required" + ); + expect(mockDispatch.mock.calls.length).toBe(0); + }); +}); + +const renderComponent = (minAppVersion: string) => { + const baseState = appReducer(undefined, applicationChangeState("active")); + const testState = { + ...baseState, + remoteConfig: O.some({ + assistanceTool: { + tool: ToolEnum.none + }, + cgn: { + enabled: false + }, + fims: { + min_app_version: { + android: minAppVersion, + ios: minAppVersion + } + }, + itw: { + min_app_version: { + android: "0.0.0.0", + ios: "0.0.0.0" + } + } + }) + } as GlobalState; + const store = createStore(appReducer, testState as any); + return renderScreenWithNavigationStoreContext( + FimsFlowHandlerScreen, + FIMS_ROUTES.CONSENTS, + { + ctaText: label, + ctaUrl, + organizationFiscalCode, + organizationName, + serviceId, + serviceName, + source + }, + store + ); +}; From c601e91ebfb3fadc4e9030139e285c54b96da776 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Thu, 20 Feb 2025 10:31:43 +0100 Subject: [PATCH 09/13] Tests for handleCtaAction --- .../messages/utils/__tests__/ctas.test.ts | 89 ++++++++++++++++++- ts/utils/url.ts | 2 +- 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/ts/features/messages/utils/__tests__/ctas.test.ts b/ts/features/messages/utils/__tests__/ctas.test.ts index 2c7f7a49532..b0736b9f756 100644 --- a/ts/features/messages/utils/__tests__/ctas.test.ts +++ b/ts/features/messages/utils/__tests__/ctas.test.ts @@ -1,4 +1,5 @@ import { OrganizationFiscalCode } from "@pagopa/ts-commons/lib/strings"; +import { Linking } from "react-native"; import { CreatedMessageWithContent } from "../../../../../definitions/backend/CreatedMessageWithContent"; import { FiscalCode } from "../../../../../definitions/backend/FiscalCode"; import { MessageBodyMarkdown } from "../../../../../definitions/backend/MessageBodyMarkdown"; @@ -13,6 +14,7 @@ import { getMessageCTA, getRemoteLocale, getServiceCTA, + handleCtaAction, testable, unsafeMessageCTAFromInput } from "../ctas"; @@ -70,7 +72,10 @@ const messageWithContent = { // test "it" as default language beforeAll(() => setLocale("it" as Locales)); -afterEach(() => jest.restoreAllMocks()); +afterEach(() => { + jest.restoreAllMocks(); + jest.clearAllMocks(); +}); describe("getRemoteLocale", () => { it("should return it if locale is it", () => { @@ -1665,3 +1670,85 @@ describe("getServiceCTA", () => { }); }); }); + +describe("handleCtaAction", () => { + const mockedLinkTo = jest.fn(); // (path: string) => undefined; + const mockedFimsCallback = jest.fn(); // (label: string, url: string) => undefined; + [ + "ioit://messages", + "iOiT://messages", + "IOIT://messages", + "iosso://https://relyingParty.url/login", + "iOsSo://https://relyingParty.url/login", + "IOSSO://https://relyingParty.url/login", + "iohandledlink://mailto:johnsmith@gmail.com", + "iOhAnDlEdLiNk://mailto:johnsmith@gmail.com", + "IOHANDLEDLINK://mailto:johnsmith@gmail.com", + "ioit:/messages", + "ioit:messages", + "ioitmessages", + "iosso:/https://relyingParty.url/login", + "iosso:https://relyingParty.url/login", + "iossohttps://relyingParty.url/login", + "iohandledlink:/mailto:johnsmith@gmail.com", + "iohandledlink:mailto:johnsmith@gmail.com", + "iohandledlinkmailto:johnsmith@gmail.com", + "https://www.google.com", + "https://google.com", + "http://www.google.com", + "http://google.com", + "clipboard://prova", + "clipboard:prova", + "sms://3331234567", + "sms:3331234567", + "tel://3331234567", + "tel:3331234567", + "mailto://johnsmith@gmail.com", + "mailto:johnsmith@gmail.com", + "copy://aValue", + "copy:aValue" + ].forEach((anUri, index) => { + const linkToCalled = index < 3; + const fimsCalled = index > 2 && index < 6; + const openUrlCalled = index > 5 && index < 9; + it(`should call '${ + linkToCalled + ? "linkTo" + : fimsCalled + ? "fimsCallback" + : openUrlCalled + ? "Linking.openUrl" + : "nothing" + }' when the CTA's action is ${anUri}`, () => { + const spiedOnMockedOpenURL = jest + .spyOn(Linking, "openURL") + .mockImplementation( + _anUrl => new Promise(resolve => resolve(undefined)) + ); + const cta: CTA = { + action: anUri, + text: "A text" + }; + + handleCtaAction(cta, mockedLinkTo, mockedFimsCallback); + + if (linkToCalled) { + expect(mockedLinkTo).toHaveBeenCalledWith(anUri.substring(6)); + } else { + expect(mockedLinkTo).not.toHaveBeenCalled(); + } + + if (fimsCalled) { + expect(mockedFimsCallback).toHaveBeenCalledWith(cta.text, cta.action); + } else { + expect(mockedFimsCallback).not.toHaveBeenCalled(); + } + + if (openUrlCalled) { + expect(spiedOnMockedOpenURL).toHaveBeenCalledWith(anUri.substring(16)); + } else { + expect(spiedOnMockedOpenURL).not.toHaveBeenCalled(); + } + }); + }); +}); diff --git a/ts/utils/url.ts b/ts/utils/url.ts index 992deaf1d28..694fb0b4eef 100644 --- a/ts/utils/url.ts +++ b/ts/utils/url.ts @@ -149,7 +149,7 @@ export function extractPathFromURL( .join("\\.")}` ); - const normalizedURL = url.replace(/\/+/g, "/"); + const normalizedURL = url.replace(/\/+/g, "/").toLowerCase(); if (prefixRegex.test(normalizedURL)) { return normalizedURL.replace(prefixRegex, ""); From 0c53239524abd974ae663b329523ae0715d768e5 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Thu, 20 Feb 2025 11:00:52 +0100 Subject: [PATCH 10/13] Case insensitive extractPathFromURL --- ts/features/messages/utils/__tests__/ctas.test.ts | 1 + ts/utils/url.ts | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/ts/features/messages/utils/__tests__/ctas.test.ts b/ts/features/messages/utils/__tests__/ctas.test.ts index b0736b9f756..6867ac8a5c6 100644 --- a/ts/features/messages/utils/__tests__/ctas.test.ts +++ b/ts/features/messages/utils/__tests__/ctas.test.ts @@ -1707,6 +1707,7 @@ describe("handleCtaAction", () => { "mailto:johnsmith@gmail.com", "copy://aValue", "copy:aValue" + // eslint-disable-next-line sonarjs/cognitive-complexity ].forEach((anUri, index) => { const linkToCalled = index < 3; const fimsCalled = index > 2 && index < 6; diff --git a/ts/utils/url.ts b/ts/utils/url.ts index 694fb0b4eef..7bde96a8050 100644 --- a/ts/utils/url.ts +++ b/ts/utils/url.ts @@ -146,10 +146,11 @@ export function extractPathFromURL( `^${escapeStringRegexp(protocol)}(/)*${host .split(".") .map(it => (it === "*" ? "[^/]+" : escapeStringRegexp(it))) - .join("\\.")}` + .join("\\.")}`, + "i" ); - const normalizedURL = url.replace(/\/+/g, "/").toLowerCase(); + const normalizedURL = url.replace(/\/+/g, "/"); if (prefixRegex.test(normalizedURL)) { return normalizedURL.replace(prefixRegex, ""); From 791633ee5420a89a77db3895039a325efb5bed74 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Thu, 20 Feb 2025 11:27:44 +0100 Subject: [PATCH 11/13] Better tests for isCtaActionValid --- .../messages/utils/__tests__/ctas.test.ts | 96 ++++++------------- 1 file changed, 29 insertions(+), 67 deletions(-) diff --git a/ts/features/messages/utils/__tests__/ctas.test.ts b/ts/features/messages/utils/__tests__/ctas.test.ts index 6867ac8a5c6..15a88d324a4 100644 --- a/ts/features/messages/utils/__tests__/ctas.test.ts +++ b/ts/features/messages/utils/__tests__/ctas.test.ts @@ -570,78 +570,40 @@ describe("internalRoutePredicates", () => { }); describe("isCtaActionValid", () => { - it("should return true for ioit://whatever", () => { - const cta: CTA = { - action: "ioit://whatever", - text: "CTA text" - }; - - const isValid = testable!.isCtaActionValid(cta); - - expect(isValid).toBe(true); - }); - it("should return false for ioit://services/webview with undefined metadata", () => { - const cta: CTA = { - action: "ioit://services/webview", - text: "CTA text" - }; - - const isValid = testable!.isCtaActionValid(cta); - - expect(isValid).toBe(false); - }); - it("should return false for ioit://services/webview with metadata with undefined token_name", () => { - const cta: CTA = { - action: "ioit://services/webview", - text: "CTA text" - }; - const metadata = {} as ServiceMetadata; - - const isValid = testable!.isCtaActionValid(cta, metadata); - - expect(isValid).toBe(false); - }); - it("should return true for iosso://https://relyingParty.url", () => { - const cta: CTA = { - action: "iosso://https://relyingParty.url", - text: "CTA text" - }; - - const isValid = testable!.isCtaActionValid(cta); - - expect(isValid).toBe(true); - }); - ioHandledLinks.forEach(ioHandledLink => - it(`should return true for ${ioHandledLink}`, () => { + const inputData: ReadonlyArray<[string, boolean]> = [ + ["ioit://whatever", true], + ["iOiT://whatever", true], + ["IOIT://whatever", true], + ["ioit://services/webview", false], + ["iosso://https://relyingParty.url", true], + ["iOsSo://https://relyingParty.url", true], + ["IOSSO://https://relyingParty.url", true], + ["iohandledlink://http://whateverHere", true], + ["iohandledlink://https://whateverHere", true], + ["iOhAnDlEdLiNk://https://whateverHere", true], + ["IOHANDLEDLINK://https://whateverHere", true], + ["iohandledlink://sms://whateverHere", true], + ["iohandledlink://tel://whateverHere", true], + ["iohandledlink://mailto://whateverHere", true], + ["iohandledlink://copy://whateverHere", true], + ["iohandledlink://whatever", false], + ["https://www.google.com", false], + ["https://google.com", false], + ["http://www.google.com", false], + ["http://google.com", false], + ["invalid", false] + ]; + inputData.forEach(tuple => { + const [action, validity] = tuple; + it(`should return '${validity}' for '${action}'`, () => { const cta: CTA = { - action: ioHandledLink, + action, text: "CTA text" }; - const isValid = testable!.isCtaActionValid(cta); - expect(isValid).toBe(true); - }) - ); - it(`should return false for iohandledlink://whatever`, () => { - const cta: CTA = { - action: "iohandledlink://whatever", - text: "CTA text" - }; - - const isValid = testable!.isCtaActionValid(cta); - - expect(isValid).toBe(false); - }); - it(`should return false for invalid action`, () => { - const cta: CTA = { - action: "invalid", - text: "CTA text" - }; - - const isValid = testable!.isCtaActionValid(cta); - - expect(isValid).toBe(false); + expect(isValid).toBe(validity); + }); }); }); From 659c3cf4abadd2a72ba794166a6fac158bfbc857 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Thu, 20 Feb 2025 15:40:12 +0100 Subject: [PATCH 12/13] Tests for fimsServiceConfiguration --- .../__tests__/remoteConfig.test.ts | 274 +++++++++++------- 1 file changed, 171 insertions(+), 103 deletions(-) diff --git a/ts/store/reducers/backendStatus/__tests__/remoteConfig.test.ts b/ts/store/reducers/backendStatus/__tests__/remoteConfig.test.ts index 9e12572950a..e2a1f863ed1 100644 --- a/ts/store/reducers/backendStatus/__tests__/remoteConfig.test.ts +++ b/ts/store/reducers/backendStatus/__tests__/remoteConfig.test.ts @@ -5,14 +5,18 @@ import { GlobalState } from "../../types"; import { absolutePortalLinksSelector, barcodesScannerConfigSelector, + fimsServiceConfiguration, generateDynamicUrlSelector, isPnAppVersionSupportedSelector, isPremiumMessagesOptInOutEnabledSelector, landingScreenBannerOrderSelector } from "../remoteConfig"; import * as appVersion from "../../../../utils/appVersion"; +import { ServiceId } from "../../../../../definitions/backend/ServiceId"; + +describe("remoteConfig", () => { + afterEach(() => jest.restoreAllMocks()); -describe("test selectors", () => { // smoke tests: valid / invalid const noneStore = { remoteConfig: O.none @@ -136,115 +140,179 @@ describe("test selectors", () => { expect(output).toBe("https://ioapp.it/path"); }); }); -}); -describe("isPnAppVersionSupportedSelector", () => { - it("should return false, when 'backendStatus' is O.none", () => { - const state = { - remoteConfig: O.none - } as GlobalState; - const isSupported = isPnAppVersionSupportedSelector(state); - expect(isSupported).toBe(false); - }); - it("should return false, when min_app_version is greater than `getAppVersion`", () => { - const state = { - remoteConfig: O.some({ - pn: { - min_app_version: { - android: "2.0.0.0", - ios: "2.0.0.0" + describe("isPnAppVersionSupportedSelector", () => { + it("should return false, when 'backendStatus' is O.none", () => { + const state = { + remoteConfig: O.none + } as GlobalState; + const isSupported = isPnAppVersionSupportedSelector(state); + expect(isSupported).toBe(false); + }); + it("should return false, when min_app_version is greater than `getAppVersion`", () => { + const state = { + remoteConfig: O.some({ + pn: { + min_app_version: { + android: "2.0.0.0", + ios: "2.0.0.0" + } } - } - }) - } as GlobalState; - jest.spyOn(appVersion, "getAppVersion").mockImplementation(() => "1.0.0.0"); - const isSupported = isPnAppVersionSupportedSelector(state); - expect(isSupported).toBe(false); - }); - it("should return true, when min_app_version is equal to `getAppVersion`", () => { - const state = { - remoteConfig: O.some({ - pn: { - min_app_version: { - android: "2.0.0.0", - ios: "2.0.0.0" + }) + } as GlobalState; + jest + .spyOn(appVersion, "getAppVersion") + .mockImplementation(() => "1.0.0.0"); + const isSupported = isPnAppVersionSupportedSelector(state); + expect(isSupported).toBe(false); + }); + it("should return true, when min_app_version is equal to `getAppVersion`", () => { + const state = { + remoteConfig: O.some({ + pn: { + min_app_version: { + android: "2.0.0.0", + ios: "2.0.0.0" + } } - } - }) - } as GlobalState; - jest.spyOn(appVersion, "getAppVersion").mockImplementation(() => "2.0.0.0"); - const isSupported = isPnAppVersionSupportedSelector(state); - expect(isSupported).toBe(true); + }) + } as GlobalState; + jest + .spyOn(appVersion, "getAppVersion") + .mockImplementation(() => "2.0.0.0"); + const isSupported = isPnAppVersionSupportedSelector(state); + expect(isSupported).toBe(true); + }); + it("should return true, when min_app_version is less than `getAppVersion`", () => { + const state = { + remoteConfig: O.some({ + pn: { + min_app_version: { + android: "2.0.0.0", + ios: "2.0.0.0" + } + } + }) + } as GlobalState; + jest + .spyOn(appVersion, "getAppVersion") + .mockImplementation(() => "3.0.0.0"); + const isSupported = isPnAppVersionSupportedSelector(state); + expect(isSupported).toBe(true); + }); }); - it("should return true, when min_app_version is less than `getAppVersion`", () => { - const state = { - remoteConfig: O.some({ - pn: { - min_app_version: { - android: "2.0.0.0", - ios: "2.0.0.0" + describe("landingScreenBannerOrderSelector", () => { + const getMock = (priority_order: Array | undefined) => + ({ + remoteConfig: O.some({ + landing_banners: { + priority_order } - } - }) + }) + } as GlobalState); + + const some_priorityOrder = ["id1", "id2", "id3"]; + const customNoneStore = { + remoteConfig: O.none } as GlobalState; - jest.spyOn(appVersion, "getAppVersion").mockImplementation(() => "3.0.0.0"); - const isSupported = isPnAppVersionSupportedSelector(state); - expect(isSupported).toBe(true); - }); -}); -describe("landingScreenBannerOrderSelector", () => { - const getMock = (priority_order: Array | undefined) => - ({ - remoteConfig: O.some({ - landing_banners: { - priority_order - } - }) - } as GlobalState); + const undefinedLandingBannersStore = { + remoteConfig: O.some({}) + } as GlobalState; + const testCases = [ + { + selectorInput: getMock(some_priorityOrder), + expected: some_priorityOrder + }, + { + selectorInput: getMock(undefined), + expected: [] + }, + { + selectorInput: getMock([]), + expected: [] + }, + { + selectorInput: customNoneStore, + expected: [] + }, + { + selectorInput: undefinedLandingBannersStore, + expected: [] + } + ]; - const some_priorityOrder = ["id1", "id2", "id3"]; - const customNoneStore = { - remoteConfig: O.none - } as GlobalState; - const undefinedLandingBannersStore = { - remoteConfig: O.some({}) - } as GlobalState; - const testCases = [ - { - selectorInput: getMock(some_priorityOrder), - expected: some_priorityOrder - }, - { - selectorInput: getMock(undefined), - expected: [] - }, - { - selectorInput: getMock([]), - expected: [] - }, - { - selectorInput: customNoneStore, - expected: [] - }, - { - selectorInput: undefinedLandingBannersStore, - expected: [] - } - ]; - - for (const testCase of testCases) { - it(`should return [${testCase.expected}] for ${JSON.stringify( - pipe( - testCase.selectorInput.remoteConfig, - O.fold( - // eslint-disable-next-line no-underscore-dangle - () => testCase.selectorInput.remoteConfig._tag, - identity + for (const testCase of testCases) { + it(`should return [${testCase.expected}] for ${JSON.stringify( + pipe( + testCase.selectorInput.remoteConfig, + O.fold( + // eslint-disable-next-line no-underscore-dangle + () => testCase.selectorInput.remoteConfig._tag, + identity + ) ) - ) - )}`, () => { - const output = landingScreenBannerOrderSelector(testCase.selectorInput); - expect(output).toStrictEqual(testCase.expected); + )}`, () => { + const output = landingScreenBannerOrderSelector(testCase.selectorInput); + expect(output).toStrictEqual(testCase.expected); + }); + } + }); + + describe("fimsServiceConfiguration", () => { + it("should retrieve configuration for matching id", () => { + const configurationId = "aConfId"; + const organizationFiscalCode = "12345678901"; + const organizationName = "Organization name"; + const serviceId = "01JMHVSD7JGCNJF36TX0JM0JF3" as ServiceId; + const serviceName = "Service name"; + const state = { + remoteConfig: O.some({ + fims: { + services: [ + { + configuration_id: configurationId, + service_id: serviceId, + organization_fiscal_code: organizationFiscalCode, + organization_name: "Organization name", + service_name: "Service name" + } + ] + } + }) + } as GlobalState; + const serviceConfiguration = fimsServiceConfiguration( + state, + configurationId + ); + expect(serviceConfiguration).toEqual({ + configuration_id: configurationId, + service_id: serviceId, + organization_fiscal_code: organizationFiscalCode, + organization_name: organizationName, + service_name: serviceName + }); }); - } + it("should return 'undefined' for unmatching id", () => { + const state = { + remoteConfig: O.some({ + fims: { + services: [ + { + configuration_id: "aConfId", + service_id: "01JMHVSD7JGCNJF36TX0JM0JF3", + organization_fiscal_code: "12345678901", + organization_name: "Organization name", + service_name: "Service name" + } + ] + } + }) + } as GlobalState; + const serviceConfiguration = fimsServiceConfiguration( + state, + "unmatchingConfId" + ); + expect(serviceConfiguration).toBeUndefined(); + }); + }); }); From 424e725ea91b89d926c11f434acf7ac1fcbb95d2 Mon Sep 17 00:00:00 2001 From: Andrea Piai Date: Thu, 27 Feb 2025 15:10:07 +0100 Subject: [PATCH 13/13] Proper io-services-metadata spec --- scripts/generate-api-models.sh | 2 +- .../fims/common/hooks/__test__/index.test.tsx | 12 ++++++------ ts/store/reducers/backendStatus/remoteConfig.ts | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/scripts/generate-api-models.sh b/scripts/generate-api-models.sh index 43d8e461f1f..b603e1d2736 100755 --- a/scripts/generate-api-models.sh +++ b/scripts/generate-api-models.sh @@ -2,7 +2,7 @@ IO_BACKEND_VERSION=v16.7.4-RELEASE # need to change after merge on io-services-metadata -IO_SERVICES_METADATA_VERSION=1.0.59 +IO_SERVICES_METADATA_VERSION=1.0.60 # Session manager version IO_SESSION_MANAGER_VERSION=1.4.0 diff --git a/ts/features/fims/common/hooks/__test__/index.test.tsx b/ts/features/fims/common/hooks/__test__/index.test.tsx index e3d3c58eb13..e9fd7b98709 100644 --- a/ts/features/fims/common/hooks/__test__/index.test.tsx +++ b/ts/features/fims/common/hooks/__test__/index.test.tsx @@ -19,7 +19,7 @@ import { GlobalState } from "../../../../../store/reducers/types"; import { ServicePublic } from "../../../../../../definitions/backend/ServicePublic"; import { loadServiceDetail } from "../../../../services/details/store/actions/details"; import { FIMS_ROUTES } from "../../navigation"; -import { FimsServiceConfiguration_config } from "../../../../../../definitions/content/FimsServiceConfiguration"; +import { FimsServiceConfiguration } from "../../../../../../definitions/content/FimsServiceConfiguration"; import { AppParamsList, IOStackNavigationProp @@ -298,7 +298,7 @@ describe("index", () => { describe("useFIMSRemoteServiceConfiguration", () => { it(`should call 'navigation.navigate' with proper parameters and return proper service data for analytics`, () => { const configurationId = "configId"; - const configuration: FimsServiceConfiguration_config = { + const configuration: FimsServiceConfiguration = { configuration_id: configurationId, organization_fiscal_code: "01234567890", organization_name: "Organization name", @@ -337,7 +337,7 @@ describe("index", () => { }); }); it(`should not call 'navigation.navigate' and return undefined serviceData when configuration id does not match`, () => { - const configuration: FimsServiceConfiguration_config = { + const configuration: FimsServiceConfiguration = { configuration_id: "configId", organization_fiscal_code: "01234567890", organization_name: "Organization name", @@ -397,7 +397,7 @@ describe("index", () => { }); describe("serviceDataFromConfigurationId", () => { it(`should return service data when the configuration id matches`, () => { - const configuration: FimsServiceConfiguration_config = { + const configuration: FimsServiceConfiguration = { configuration_id: "configId", organization_fiscal_code: "01234567890", organization_name: "Organization name", @@ -424,7 +424,7 @@ describe("index", () => { }); }); it(`should return 'undefined' when the configuration id does not match`, () => { - const configuration: FimsServiceConfiguration_config = { + const configuration: FimsServiceConfiguration = { configuration_id: "configId", organization_fiscal_code: "01234567890", organization_name: "Organization name", @@ -621,7 +621,7 @@ const renderFromAuthenticationFlowHook = ( }; const renderFromRemoteConfigurationHook = ( - configuration: FimsServiceConfiguration_config, + configuration: FimsServiceConfiguration, hookConfigurationId: string ) => { const appState = { diff --git a/ts/store/reducers/backendStatus/remoteConfig.ts b/ts/store/reducers/backendStatus/remoteConfig.ts index 8909f97bf31..ccad51d6426 100644 --- a/ts/store/reducers/backendStatus/remoteConfig.ts +++ b/ts/store/reducers/backendStatus/remoteConfig.ts @@ -22,7 +22,7 @@ import { Action } from "../../actions/types"; import { isPropertyWithMinAppVersionEnabled } from "../featureFlagWithMinAppVersionStatus"; import { isIdPayLocallyEnabledSelector } from "../persistedPreferences"; import { GlobalState } from "../types"; -import { FimsServiceConfiguration_config } from "../../../../definitions/content/FimsServiceConfiguration"; +import { FimsServiceConfiguration } from "../../../../definitions/content/FimsServiceConfiguration"; export type RemoteConfigState = O.Option; @@ -129,7 +129,7 @@ export const fimsServiceConfiguration = createSelector( ( remoteConfig, configurationId: string - ): FimsServiceConfiguration_config | undefined => + ): FimsServiceConfiguration | undefined => pipe( remoteConfig, O.chainNullableK(config => config.fims.services),